diff --git a/crates/pstr-app/src/app.rs b/crates/pstr-app/src/app.rs index b3b52dd..e475666 100644 --- a/crates/pstr-app/src/app.rs +++ b/crates/pstr-app/src/app.rs @@ -1901,7 +1901,9 @@ fn changed_art( fn global_shortcuts(ctx: &egui::Context, page: &Page, actions: &mut Vec) { use egui::{Key, Modifiers}; - let typing = ctx.egui_wants_keyboard_input(); + // A text field, not any focused widget: a tile reached with Tab or the + // arrow keys has focus too, and "/" over it should still search. + let typing = ctx.text_edit_focused(); let mut tab = None; ctx.input_mut(|input| { if input.consume_key(Modifiers::COMMAND, Key::F) diff --git a/crates/pstr-app/src/ui/mod.rs b/crates/pstr-app/src/ui/mod.rs index 1a42506..169a199 100644 --- a/crates/pstr-app/src/ui/mod.rs +++ b/crates/pstr-app/src/ui/mod.rs @@ -514,6 +514,11 @@ pub fn card(ui: &mut egui::Ui, card: Card<'_>) -> egui::Response { let (rect, response) = ui.allocate_exact_size(Vec2::new(width, image_height + text_height), Sense::click()); + // Reached with the arrow keys or Tab: bring it into view, before the + // early return below would skip a tile that is just off screen. + if response.gained_focus() { + response.scroll_to_me(None); + } if !ui.is_rect_visible(rect) { return response; } @@ -523,9 +528,13 @@ pub fn card(ui: &mut egui::Ui, card: Card<'_>) -> egui::Response { // ride it, so they arrive together rather than each snapping on its own. // egui drives this from wall-clock time, so it is the same speed whatever // the frame rate — and it needs no state kept here. - let hover = - ui.ctx() - .animate_bool_with_time(response.id.with("hover"), response.hovered(), 0.12); + // Focus lights a tile the way the pointer does, so the keyboard can be + // followed across the grid. + let hover = ui.ctx().animate_bool_with_time( + response.id.with("hover"), + response.hovered() || response.has_focus(), + 0.12, + ); // The picture arrives whenever its download finishes, several frames after // the tile first drew. Fading it in over the placeholder turns a wall of // letters popping into stills into one settle. diff --git a/crates/pstr-app/src/ui/shortcuts.rs b/crates/pstr-app/src/ui/shortcuts.rs index 53a9233..e32d5c3 100644 --- a/crates/pstr-app/src/ui/shortcuts.rs +++ b/crates/pstr-app/src/ui/shortcuts.rs @@ -16,6 +16,8 @@ const GROUPS: &[(&str, &[(&str, &str)])] = &[ ("Ctrl 1 – 4", "Library, Shares, Downloads, Settings"), ("Ctrl ,", "Settings"), ("Ctrl V", "Paste a share link to add it"), + ("Tab or arrows", "Move between titles and episodes"), + ("Enter", "Open or play what is highlighted"), ("Alt ← or mouse back", "Back"), ("?", "This list"), ], diff --git a/crates/pstr-app/src/ui/title.rs b/crates/pstr-app/src/ui/title.rs index 0e70716..3d25d55 100644 --- a/crates/pstr-app/src/ui/title.rs +++ b/crates/pstr-app/src/ui/title.rs @@ -740,9 +740,14 @@ fn still( ); } - let hover = ui - .ctx() - .animate_bool_with_time(response.id, response.hovered(), 0.12); + if response.gained_focus() { + response.scroll_to_me(None); + } + let hover = ui.ctx().animate_bool_with_time( + response.id, + response.hovered() || response.has_focus(), + 0.12, + ); if hover > 0.0 { painter.rect_filled( rect, @@ -750,6 +755,14 @@ fn still( egui::Color32::from_black_alpha((90.0 * hover) as u8), ); } + if response.has_focus() { + painter.rect_stroke( + rect, + radius, + egui::Stroke::new(2.0, theme::accent()), + egui::StrokeKind::Inside, + ); + } // A play mark, always faintly there and full under the pointer. let centre = rect.center(); painter.circle_filled(