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
	}
}