ltk/input/keyboard/nav.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
impl<A: App> AppData<A>
{
/// Step the topmost scroll's `hovered_idx` one item up or down with
/// the keyboard. Returns `true` when the move was applied (a scroll
/// with at least one navigable item was on screen and the new item
/// is different from the current hover) so the caller can fall
/// through to the application's own `on_key` only on a no-op.
///
/// The "topmost scroll" is the last entry in `scroll_rects`, which
/// matches Stack-overlay layout order: a popup pushed after the
/// main content sits above it. Auto-scrolls the new item into view
/// by adjusting `scroll_offsets[scroll_idx]`.
pub( crate ) fn move_keyboard_hover( &mut self, focus: SurfaceFocus, reverse: bool ) -> bool
{
// Find the topmost scroll that has a navigable item list.
let scroll_meta = {
let ss = self.surface( focus );
ss.scroll_rects.iter().rev()
.find_map( |( rect, idx, _ax )|
{
ss.scroll_navigable_items.get( idx )
.filter( |list| !list.is_empty() )
.map( |list| ( *rect, *idx, list.clone() ) )
} )
};
let Some( ( scroll_rect, scroll_idx, items ) ) = scroll_meta else { return false; };
let current = self.surface( focus ).hovered_idx;
let pos = current.and_then( |h| items.iter().position( |( i, _, _ )| *i == h ) );
let next_pos = match ( reverse, pos )
{
( false, None ) => 0,
( false, Some( p ) ) => ( p + 1 ).min( items.len() - 1 ),
( true, None ) => items.len() - 1,
( true, Some( p ) ) => p.saturating_sub( 1 ),
};
let ( new_idx, content_y, content_h ) = items[ next_pos ];
// Auto-scroll on the Y axis to bring the new item fully into view.
// Item Y is in pre-offset content coordinates, so the offset that
// places the item flush with the top of the viewport is `content_y`,
// and flush with the bottom is `content_y + content_h - viewport_h`.
// Keyboard navigation only steps on the navigable-item axis (vertical
// list-style); the X offset is preserved as-is.
let viewport_h = scroll_rect.height;
let ( current_x, current_y ) = self.surface( focus )
.scroll_offsets.get( &scroll_idx )
.copied().unwrap_or( ( 0.0, 0.0 ) );
let new_y = if content_y < current_y
{
content_y
}
else if content_y + content_h > current_y + viewport_h
{
( content_y + content_h - viewport_h ).max( 0.0 )
}
else
{
current_y
};
let ss = self.surface_mut( focus );
ss.hovered_idx = Some( new_idx );
ss.scroll_offsets.insert( scroll_idx, ( current_x, new_y ) );
ss.request_redraw();
true
}
}