ltk/widget/text_edit/
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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use crate::render::Canvas;
use crate::types::Rect;

use super::hit_test::byte_offset_in_line;
use super::theme;
use super::wrapping::{ compute_visual_lines, VisualLine };

/// Locate the visual line containing `cursor` in the wrap layout for
/// `value` inside `rect`. When the cursor sits exactly on a soft-wrap
/// boundary (so it satisfies both the previous line's `end` and the
/// next line's `start`), prefer the *previous* line — that matches the
/// rendering convention in `draw_multiline`, which paints the caret at
/// the trailing edge of the prior visual row rather than the leading
/// edge of the new one.
fn current_visual_line(
	canvas: &Canvas,
	rect:   Rect,
	value:  &str,
	cursor: usize,
) -> Option<( Vec<VisualLine>, usize )>
{
	let inner_width  = ( rect.width - theme::PAD_H * 2.0 ).max( theme::FONT_SIZE );
	let visual_lines = compute_visual_lines( canvas, value, inner_width, theme::FONT_SIZE );
	if visual_lines.is_empty() { return None; }
	let safe_cursor  = cursor.min( value.len() );
	let idx = visual_lines.iter()
		.position( |vl| safe_cursor >= vl.start && safe_cursor <= vl.end )
		.unwrap_or( visual_lines.len() - 1 );
	Some( ( visual_lines, idx ) )
}

/// Move the text cursor one visual row up. Returns the new byte offset
/// or `None` when the cursor is already on the first visual row, so
/// the caller can fall through to sibling-widget keyboard navigation.
///
/// Visual X is preserved across the move: the new cursor lands as close
/// as possible (with snap-to-nearest-glyph) to the same horizontal
/// position the cursor had on its origin line. No "preferred column"
/// state is kept across multiple consecutive Up presses, so a long
/// short → long sequence will drift toward the end of every short
/// line — same simplification GTK / Cocoa make for plain controls.
pub( crate ) fn cursor_visual_up(
	canvas: &Canvas,
	rect:   Rect,
	value:  &str,
	cursor: usize,
) -> Option<usize>
{
	let ( lines, idx ) = current_visual_line( canvas, rect, value, cursor )?;
	if idx == 0 { return None; }
	let cur      = lines[ idx ];
	let safe     = cursor.min( value.len() );
	let target_x = canvas.measure_text( &value[cur.start..safe], theme::FONT_SIZE );
	let prev     = lines[ idx - 1 ];
	Some( byte_offset_in_line( canvas, value, prev.start, prev.end, target_x, false, theme::FONT_SIZE ) )
}

/// Mirror of [`cursor_visual_up`] for the Down arrow.
pub( crate ) fn cursor_visual_down(
	canvas: &Canvas,
	rect:   Rect,
	value:  &str,
	cursor: usize,
) -> Option<usize>
{
	let ( lines, idx ) = current_visual_line( canvas, rect, value, cursor )?;
	if idx + 1 >= lines.len() { return None; }
	let cur      = lines[ idx ];
	let safe     = cursor.min( value.len() );
	let target_x = canvas.measure_text( &value[cur.start..safe], theme::FONT_SIZE );
	let next     = lines[ idx + 1 ];
	Some( byte_offset_in_line( canvas, value, next.start, next.end, target_x, false, theme::FONT_SIZE ) )
}

/// Byte offset of the start of the cursor's current visual line.
/// Falls back to `0` when the wrap layout cannot be computed (e.g.
/// degenerate rect), so the Home key always has a sensible target.
pub( crate ) fn cursor_visual_home(
	canvas: &Canvas,
	rect:   Rect,
	value:  &str,
	cursor: usize,
) -> usize
{
	current_visual_line( canvas, rect, value, cursor )
		.map( |( lines, idx )| lines[ idx ].start )
		.unwrap_or( 0 )
}

/// Byte offset of the end of the cursor's current visual line. For a
/// hard-`\n`-terminated row this is the byte just before the newline;
/// for a soft-wrapped row it is the wrap point (which is also the
/// start of the next visual row, but the caret renders at the trailing
/// edge of *this* row by convention).
pub( crate ) fn cursor_visual_end(
	canvas: &Canvas,
	rect:   Rect,
	value:  &str,
	cursor: usize,
) -> usize
{
	current_visual_line( canvas, rect, value, cursor )
		.map( |( lines, idx )| lines[ idx ].end )
		.unwrap_or( value.len() )
}