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

use std::hash::{ Hash, Hasher };
use std::rc::Rc;

use crate::render::Canvas;

use super::tabs::{ next_stop, tab_width };
use super::TextFont;

/// 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,
}

/// What a wrap layout depends on: the value's length and hash, the row width, and the font and its size.
#[ derive( Clone, Copy, PartialEq ) ]
struct LayoutKey
{
	hash:  u64,
	len:   usize,
	width: u32,
	px:    u32,
	font:  usize,
}

const LAYOUT_CACHE_CAP: usize = 4;

thread_local!
{
	static LAYOUTS: std::cell::RefCell<Vec<( LayoutKey, Rc<Vec<VisualLine>> )>> = const { std::cell::RefCell::new( Vec::new() ) };
}

/// 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.
/// The last layouts are kept, so drawing, hit testing and caret
/// motion over the same value wrap it once.
pub( crate ) fn compute_visual_lines(
	canvas:      &Canvas,
	font:        &TextFont,
	value:       &str,
	inner_width: f32,
	font_size:   f32,
) -> Rc<Vec<VisualLine>>
{
	let mut hasher = std::collections::hash_map::DefaultHasher::new();
	value.hash( &mut hasher );
	let key = LayoutKey
	{
		hash:  hasher.finish(),
		len:   value.len(),
		width: inner_width.to_bits(),
		px:    ( font_size * canvas.dpi_scale() ).to_bits(),
		font:  font.face_key( canvas ),
	};
	if let Some( lines ) = LAYOUTS.with( |c| c.borrow().iter().find( |( k, _ )| *k == key ).map( |( _, l )| Rc::clone( l ) ) )
	{
		return lines;
	}
	let lines = Rc::new( wrap( canvas, font, value, inner_width, font_size ) );
	LAYOUTS.with( |c|
	{
		let mut c = c.borrow_mut();
		if c.len() >= LAYOUT_CACHE_CAP { c.remove( 0 ); }
		c.push( ( key, Rc::clone( &lines ) ) );
	} );
	lines
}

fn wrap( canvas: &Canvas, font: &TextFont, 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 tab_w    = tab_width( canvas, font, font_size );
	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
		{
			out.push( VisualLine { start: line_start, end: line_end } );
		} else {
			wrap_logical_line( canvas, font, value, line_start, line_end, max_w, font_size, tab_w, &mut out );
		}
		byte_pos = line_end + 1;
	}
	out
}

#[ allow( clippy::too_many_arguments ) ]
fn wrap_logical_line(
	canvas:    &Canvas,
	font:      &TextFont,
	value:     &str,
	start:     usize,
	end:       usize,
	max_width: f32,
	font_size: f32,
	tab_w:     f32,
	out:       &mut Vec<VisualLine>,
)
{
	let mut cur = start;
	while cur < end
	{
		let segment   = &value[cur..end];
		let break_at  = find_wrap_offset( canvas, font, segment, max_width, font_size, tab_w );
		// 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 mut next  = raw_next.max( cur + 1 ).min( end );
		while !value.is_char_boundary( next ) { next += 1; }
		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,
	font:      &TextFont,
	segment:   &str,
	max_width: f32,
	font_size: f32,
	tab_w:     f32,
) -> usize
{
	let mut acc_w            = 0.0f32;
	let mut last_break_after: Option<usize> = None;
	for ( i, ch ) in segment.char_indices()
	{
		let w = if ch == '\t'
		{
			next_stop( acc_w, tab_w ) - acc_w
		} else {
			font.measure_char( canvas, ch, 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()
}