feat: add keyboard shortcuts and Tab link traversal on pointer hosts

This commit is contained in:
randogoth 2026-10-11 22:32:44 +03:00
parent fa7aa5557c
commit 56db479df0
6 changed files with 798 additions and 272 deletions

View file

@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:flutter/services.dart' show LogicalKeyboardKey;
import 'package:signals/signals_flutter.dart' show SignalBuilder;
import '../../application/controllers/browser_controller.dart'
@ -32,13 +35,22 @@ class ReaderScreen extends StatefulWidget {
class _ReaderScreenState extends State<ReaderScreen> {
final _scroll = ScrollController();
final _addressFocus = FocusNode();
/// The address bar is on screen from the start, hidden while the reader
/// scrolls down, back on any upward scroll.
/// scrolls down, back on any upward scroll. A pointer host with a keyboard
/// has room to spare and keys to reach the bar, so it never hides.
bool _chromeVisible = true;
bool _showBrokenText = false;
final _checkedSubscriptions = <String>{};
/// Touch devices navigate with gestures; the others are pointer hosts
/// whose readers reach the chrome with the mouse and the keyboard.
static bool get _isTouch => switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS => true,
_ => false,
};
@override
void initState() {
super.initState();
@ -49,10 +61,14 @@ class _ReaderScreenState extends State<ReaderScreen> {
void dispose() {
_scroll.removeListener(_onScroll);
_scroll.dispose();
_addressFocus.dispose();
super.dispose();
}
void _onScroll() {
// On a pointer host the bar stays on screen, so scrolling changes
// nothing about it.
if (!_isTouch) return;
if (!_scroll.hasClients) return;
final position = _scroll.position;
@ -70,7 +86,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
Widget build(BuildContext context) {
final deps = AppScope.of(context).deps;
return Scaffold(
final reader = Scaffold(
body: Column(
children: [
AnimatedSlide(
@ -82,7 +98,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
curve: Curves.easeOut,
alignment: Alignment.topCenter,
child: _chromeVisible
? const AddressBar()
? AddressBar(focusNode: _addressFocus)
: const SizedBox(width: double.infinity),
),
),
@ -134,6 +150,43 @@ class _ReaderScreenState extends State<ReaderScreen> {
],
),
);
// A pointer host drives the reader from the keyboard as well: the bar
// takes an address (Ctrl or Cmd+L, which selects what is in it), the page
// reloads (Ctrl or Cmd+R), and the walk goes back and forward
// (Alt+Left/Right, or Cmd+Left/Right and Cmd+[ and Cmd+] on a Mac).
// Touch hosts navigate with gestures instead.
if (_isTouch) return reader;
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyL, control: true):
_addressFocus.requestFocus,
const SingleActivator(LogicalKeyboardKey.keyL, meta: true):
_addressFocus.requestFocus,
const SingleActivator(LogicalKeyboardKey.keyR, control: true):
deps.browser.reload,
const SingleActivator(LogicalKeyboardKey.keyR, meta: true):
deps.browser.reload,
const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true):
deps.browser.back,
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true):
deps.browser.forward,
// The same walk, with the keys a Mac reader reaches for.
const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true):
deps.browser.back,
const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true):
deps.browser.forward,
const SingleActivator(LogicalKeyboardKey.bracketLeft, meta: true):
deps.browser.back,
const SingleActivator(LogicalKeyboardKey.bracketRight, meta: true):
deps.browser.forward,
},
// Key events reach only the focused branch, and a session starts with
// nothing focused; this node gives them a place to arrive. Tapping the
// address field or a control moves the focus deeper in, where the
// shortcuts still apply.
child: Focus(autofocus: true, skipTraversal: true, child: reader),
);
}
Widget _body(BuildContext context, MewsPage? page) {