From 1dcadeb882b9e3adad54de73817923d65d7981dc Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sun, 26 Jul 2026 10:27:04 +0200 Subject: [PATCH] Scroll to the focused preference from its unscrolled position --- src/app.rs | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8bcba00..d4880fd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1013,6 +1013,18 @@ impl App { pub(crate) fn subscription(&self) -> Subscription { let mut subscriptions = vec![ keyboard::on_key_press(shortcut), + // A focused text field takes escape for itself to drop its own + // focus, so a dialog would never see it through `on_key_press`. + iced::event::listen_with(|event, _, _| { + matches!( + event, + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::Escape), + .. + }) + ) + .then_some(Message::DismissPanel) + }), window::close_requests().map(Message::WindowClosed), window::close_events().map(Message::WindowClosed), ]; @@ -1207,7 +1219,6 @@ fn shortcut(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Option { Some(Message::OpenModelManager) } - keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Message::DismissPanel), keyboard::Key::Named(keyboard::key::Named::Tab) => Some(if modifiers.shift() { Message::FocusPrevious } else { @@ -1308,12 +1319,12 @@ fn reveal_focused() -> Task { id: Option<&Id>, bounds: Rectangle, content_bounds: Rectangle, - _translation: Vector, + translation: Vector, ) { if id != Some(&Id::from(preferences_scroll_id())) { return; } - let Some(offset) = reveal_offset(self.field, bounds) else { + let Some(offset) = reveal_offset(self.field, bounds, translation.y) else { return; }; state.scroll_by( @@ -1331,14 +1342,19 @@ fn reveal_focused() -> Task { } /// How far the scroll area has to move for `field` to sit fully inside -/// `viewport`, or `None` when it already does. A field taller than the -/// viewport lines up with its top edge. -fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle) -> Option { +/// `viewport`, or `None` when it already does. A field taller than the viewport +/// lines up with its top edge. +/// +/// Layout places the content in its own unscrolled coordinates, so `field` only +/// says where it sits once `scrolled` — the offset the area is already at — is +/// taken off it. +fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f32) -> Option { // Keep a row's worth of margin, so a revealed field never sits flush // against the edge of the viewport. const MARGIN: f32 = 12.0; - let above = field.y - MARGIN - viewport.y; - let below = field.y + field.height + MARGIN - (viewport.y + viewport.height); + let top = field.y - scrolled; + let above = top - MARGIN - viewport.y; + let below = top + field.height + MARGIN - (viewport.y + viewport.height); if above < 0.0 { Some(above) } else if below > 0.0 { @@ -1464,11 +1480,15 @@ mod tests { width: 700.0, height: 30.0, }; - assert_eq!(reveal_offset(field(200.0), viewport), None); + assert_eq!(reveal_offset(field(200.0), viewport, 0.0), None); // Above the fold: scroll back by the gap plus the margin. - assert_eq!(reveal_offset(field(80.0), viewport), Some(-32.0)); + assert_eq!(reveal_offset(field(80.0), viewport, 0.0), Some(-32.0)); // Below it: the bottom edge plus the margin comes into view. - assert_eq!(reveal_offset(field(490.0), viewport), Some(32.0)); + assert_eq!(reveal_offset(field(490.0), viewport, 0.0), Some(32.0)); + // A field the scroll already brought into view stays put, and one + // further down moves by the gap alone, not by the whole scroll. + assert_eq!(reveal_offset(field(690.0), viewport, 500.0), None); + assert_eq!(reveal_offset(field(990.0), viewport, 500.0), Some(32.0)); } #[test]