ltk/event_loop/text_editing/
cursor.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// 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::app_data::AppData;
use crate::event_loop::surface::SurfaceFocus;

impl<A: App> AppData<A>
{
	/// Read the current text value of the focused widget — preferring
	/// the pending typed-but-not-yet-applied value when present, falling
	/// back to the value snapshot from the per-frame handler list.
	pub( crate ) fn focused_text_value( &self, focus: SurfaceFocus ) -> Option<( usize, String )>
	{
		let idx = self.surface( focus ).focused_idx?;
		let value = if let Some( pending ) = self.surface( focus ).pending_text_values.get( &idx )
		{
			pending.clone()
		} else {
			crate::tree::find_handlers( &self.surface( focus ).widget_rects, idx )?
				.current_value()
				.map( |s| s.to_string() )
				.unwrap_or_default()
		};
		Some( ( idx, value ) )
	}

	/// Move the text cursor one codepoint to the left. With `extend`,
	/// the selection anchor stays put (Shift+Left widens the
	/// selection); without it, the selection is collapsed to the new
	/// cursor position. No-op when no text input is focused.
	pub( crate ) fn handle_cursor_left( &mut self, focus: SurfaceFocus, extend: bool )
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
			.unwrap_or( cursor ).min( value.len() );
		// Without shift: if a selection exists, collapse to its start.
		if !extend && anchor != cursor
		{
			let s = cursor.min( anchor );
			let ss = self.surface_mut( focus );
			*ss.cursor_state.entry( idx ).or_insert( 0 ) = s;
			*ss.selection_anchor.entry( idx ).or_insert( 0 ) = s;
			ss.request_redraw();
			return;
		}
		if cursor == 0 { return; }
		let prev_char = value[..cursor].chars().last();
		let step = prev_char.map( |c| c.len_utf8() ).unwrap_or( 1 );
		let new_cursor = cursor.saturating_sub( step );
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
	}

	/// Move the text cursor one codepoint to the right.
	pub( crate ) fn handle_cursor_right( &mut self, focus: SurfaceFocus, extend: bool )
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
			.unwrap_or( cursor ).min( value.len() );
		if !extend && anchor != cursor
		{
			let e = cursor.max( anchor );
			let ss = self.surface_mut( focus );
			*ss.cursor_state.entry( idx ).or_insert( 0 ) = e;
			*ss.selection_anchor.entry( idx ).or_insert( 0 ) = e;
			ss.request_redraw();
			return;
		}
		if cursor >= value.len() { return; }
		let next_char = value[cursor..].chars().next();
		let step = next_char.map( |c| c.len_utf8() ).unwrap_or( 1 );
		let new_cursor = ( cursor + step ).min( value.len() );
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
	}

	/// Move the text cursor up one *visual* row. With `extend`, the
	/// selection anchor stays put.
	///
	/// In multiline mode the move follows the soft-wrapped layout — a
	/// long logical line that wraps onto N visual rows lets Up step
	/// through each of those rows in turn, instead of jumping straight
	/// over them to the previous hard `\n`. Single-line inputs and
	/// secure inputs both render as one visual row, so Up always
	/// returns `false` and the runtime falls through to sibling-widget
	/// keyboard navigation.
	pub( crate ) fn handle_cursor_up( &mut self, focus: SurfaceFocus, extend: bool ) -> bool
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx )
		{
			Some( g ) => g,
			None      => return false,
		};
		if !multiline || secure { return false; }
		let canvas = match self.surface( focus ).canvas.as_ref() { Some( c ) => c, None => return false };
		let new_cursor = match crate::widget::text_edit::cursor_visual_up( canvas, rect, &value, cursor )
		{
			Some( n ) => n,
			None      => return false,
		};
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
		true
	}

	/// Move the text cursor down one *visual* row. Mirror of
	/// [`Self::handle_cursor_up`].
	pub( crate ) fn handle_cursor_down( &mut self, focus: SurfaceFocus, extend: bool ) -> bool
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx )
		{
			Some( g ) => g,
			None      => return false,
		};
		if !multiline || secure { return false; }
		let canvas = match self.surface( focus ).canvas.as_ref() { Some( c ) => c, None => return false };
		let new_cursor = match crate::widget::text_edit::cursor_visual_down( canvas, rect, &value, cursor )
		{
			Some( n ) => n,
			None      => return false,
		};
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
		true
	}

	/// Move the cursor to the start of the current *visual* row (Home
	/// key). On a soft-wrapped logical line that lands at the wrap
	/// point of the row the caret is on — a second Home press from
	/// there does not move further, since the visual row already
	/// starts at that boundary.
	pub( crate ) fn handle_cursor_home( &mut self, focus: SurfaceFocus, extend: bool )
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let new_cursor = match self.text_input_geometry( focus, idx )
		{
			Some( ( rect, _, true, false, _, _ ) ) => match self.surface( focus ).canvas.as_ref()
			{
				Some( canvas ) => crate::widget::text_edit::cursor_visual_home( canvas, rect, &value, cursor ),
				None           => value[..cursor].rfind( '\n' ).map( |p| p + 1 ).unwrap_or( 0 ),
			},
			_ => value[..cursor].rfind( '\n' ).map( |p| p + 1 ).unwrap_or( 0 ),
		};
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
	}

	/// Move the cursor to the end of the current *visual* row (End
	/// key). For a soft-wrapped row that's the wrap point; for a row
	/// terminated by `\n` it's the byte just before the newline.
	pub( crate ) fn handle_cursor_end( &mut self, focus: SurfaceFocus, extend: bool )
	{
		let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
		let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
			.unwrap_or( value.len() ).min( value.len() );
		let new_cursor = match self.text_input_geometry( focus, idx )
		{
			Some( ( rect, _, true, false, _, _ ) ) => match self.surface( focus ).canvas.as_ref()
			{
				Some( canvas ) => crate::widget::text_edit::cursor_visual_end( canvas, rect, &value, cursor ),
				None           => value[cursor..].find( '\n' ).map( |p| cursor + p ).unwrap_or( value.len() ),
			},
			_ => value[cursor..].find( '\n' ).map( |p| cursor + p ).unwrap_or( value.len() ),
		};
		let ss = self.surface_mut( focus );
		*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
		if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
		ss.request_redraw();
	}
}