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