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