ltk/widget/text_edit/wrapping.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::render::Canvas;
/// One visual row of a wrapped multiline TextEdit. `start..end` is a
/// byte range inside the value; the renderer draws those bytes on a
/// single visual row, even if the underlying logical line (the run
/// between two `\n` characters) spans several rows. Soft wraps do
/// not mutate the buffer — the value stays a flat string with hard
/// breaks; only the rendering / hit-testing model treats wraps as
/// row boundaries.
#[ derive( Clone, Copy, Debug ) ]
pub( crate ) struct VisualLine
{
pub start: usize,
pub end: usize,
}
/// Wrap `value` into a list of visual lines that each fit within
/// `inner_width` when rendered at `font_size`. Hard `\n` breaks are
/// always row boundaries; long logical lines are additionally split
/// at the last whitespace before the row would overflow, falling
/// back to a per-character break inside a single oversized word.
///
/// O(N) per call where N is `value.len()` — `draw_multiline` runs
/// it once per frame, which is fine for the kilobyte-scale buffers a
/// `text_edit` is meant to hold (anything larger should use a
/// dedicated text-area widget with cached layout).
pub( crate ) fn compute_visual_lines(
canvas: &Canvas,
value: &str,
inner_width: f32,
font_size: f32,
) -> Vec<VisualLine>
{
let mut out = Vec::new();
if value.is_empty()
{
out.push( VisualLine { start: 0, end: 0 } );
return out;
}
let max_w = inner_width.max( 1.0 );
let mut byte_pos = 0usize;
for logical_line in value.split( '\n' )
{
let line_start = byte_pos;
let line_end = byte_pos + logical_line.len();
if line_start == line_end
{
// Empty logical line still occupies a row.
out.push( VisualLine { start: line_start, end: line_end } );
} else {
wrap_logical_line( canvas, value, line_start, line_end, max_w, font_size, &mut out );
}
byte_pos = line_end + 1; // step past the '\n'
}
out
}
fn wrap_logical_line(
canvas: &Canvas,
value: &str,
start: usize,
end: usize,
max_width: f32,
font_size: f32,
out: &mut Vec<VisualLine>,
)
{
let mut cur = start;
while cur < end
{
let segment = &value[cur..end];
let break_at = find_wrap_offset( canvas, segment, max_width, font_size );
// Always advance at least one byte so a degenerate measure
// (zero-width font, ridiculously narrow rect) still
// terminates instead of looping forever.
let raw_next = cur + break_at;
let next = raw_next.max( cur + 1 ).min( end );
out.push( VisualLine { start: cur, end: next } );
cur = next;
}
}
/// Walk `segment` left-to-right accumulating glyph widths and return
/// the byte offset (within `segment`) where the visual line should
/// break. Prefers a break *after* the most recent whitespace; falls
/// back to mid-character break if a single word is wider than
/// `max_width`.
fn find_wrap_offset(
canvas: &Canvas,
segment: &str,
max_width: f32,
font_size: f32,
) -> usize
{
let mut acc_w = 0.0f32;
let mut last_break_after: Option<usize> = None;
for ( i, ch ) in segment.char_indices()
{
let glyph = ch.to_string();
let w = canvas.measure_text( &glyph, font_size );
if acc_w + w > max_width && i > 0
{
return last_break_after.unwrap_or( i );
}
acc_w += w;
if ch == ' ' || ch == '\t'
{
last_break_after = Some( i + ch.len_utf8() );
}
}
segment.len()
}