Scroll to the focused preference from its unscrolled position

This commit is contained in:
Georg Bauer
2026-07-26 10:27:04 +02:00
parent c5e812f9b9
commit 1dcadeb882

View File

@@ -1013,6 +1013,18 @@ impl App {
pub(crate) fn subscription(&self) -> Subscription<Message> {
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<Messag
keyboard::Key::Character("m") if modifiers.command() && modifiers.shift() => {
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<Message> {
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<Message> {
}
/// 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<f32> {
/// `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<f32> {
// 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]