ailloli_ui_editor/layout/
visible_lines.rs1use ailloli_ui_core::{Size, TextStyle};
4use ailloli_ui_text::{TextBuffer, TextSystem};
5
6use crate::layout::layout_cache::LayoutCacheKey;
7use crate::layout::{
8 first_layout_baseline, layout_visual_height, EditorTextRun, FenwickTree, LayoutCache,
9 ParagraphMetrics, ParagraphMetricsCache, ParagraphMetricsStats,
10};
11use crate::{EditorStyle, EditorViewport, EditorWrapMode};
12
13#[derive(Debug, Clone)]
29pub struct VisibleTextLayout {
30 pub runs: Vec<EditorTextRun>,
32 pub content_size: Size,
34 pub metrics_stats: ParagraphMetricsStats,
36 pub used_fast_path: bool,
38}
39
40pub fn build_visible_text_layout(
62 buffer: &TextBuffer,
63 viewport: EditorViewport,
64 style: EditorStyle,
65 layout_cache: &mut LayoutCache,
66 metrics_cache: &mut ParagraphMetricsCache,
67 text_system: &mut TextSystem,
68) -> VisibleTextLayout {
69 build_visible_text_layout_filtered(
70 buffer,
71 viewport,
72 style,
73 layout_cache,
74 metrics_cache,
75 text_system,
76 &|_| false,
77 )
78}
79
80pub fn build_visible_text_layout_filtered(
102 buffer: &TextBuffer,
103 viewport: EditorViewport,
104 style: EditorStyle,
105 layout_cache: &mut LayoutCache,
106 metrics_cache: &mut ParagraphMetricsCache,
107 text_system: &mut TextSystem,
108 is_hidden: &dyn Fn(usize) -> bool,
109) -> VisibleTextLayout {
110 if viewport.wrap_mode == EditorWrapMode::NoWrap
111 && !(0..buffer.paragraphs().len()).any(is_hidden)
112 {
113 return build_visible_nowrap_text_layout(
114 buffer,
115 viewport,
116 style,
117 layout_cache,
118 metrics_cache,
119 text_system,
120 );
121 }
122
123 let viewport_top = viewport.scroll_y;
124 let viewport_bot = viewport_top + viewport.content_rect.h.max(0.0);
125 let text_style = TextStyle::new(style.font, style.px_size, style.fg);
126 let mut runs = Vec::new();
127 let mut para_top = 0.0;
128 let mut content_width = 0.0f32;
129 let mut stats = ParagraphMetricsStats::default();
130 let wrap_mode = viewport.text_wrap_mode();
131 let max_width = viewport.max_text_width();
132
133 for (p_idx, meta) in buffer.paragraphs().iter().enumerate() {
134 if is_hidden(p_idx) {
135 continue;
136 }
137 let Some(paragraph) = buffer.paragraph_text(p_idx) else {
138 continue;
139 };
140 let trimmed = paragraph.trim_end_matches('\n').to_string();
141 let key = LayoutCacheKey::new(
142 p_idx,
143 meta.revision,
144 &trimmed,
145 text_style,
146 wrap_mode,
147 max_width,
148 );
149 let layout = layout_cache.layout_paragraph(
150 key,
151 &trimmed,
152 text_style,
153 max_width,
154 wrap_mode,
155 text_system,
156 );
157 let metrics = if let Some(metrics) = metrics_cache.get(&key) {
158 stats.hits += 1;
159 metrics
160 } else {
161 stats.misses += 1;
162 let metrics = ParagraphMetrics {
163 width: layout.width(),
164 height: layout_visual_height(&layout, style),
165 line_count: layout.lines.len().max(1),
166 local_version: meta.revision,
167 };
168 metrics_cache.insert(key, metrics);
169 metrics
170 };
171 content_width = content_width.max(metrics.width);
172 let para_bottom = para_top + metrics.height;
173 if para_bottom >= viewport_top && para_top <= viewport_bot {
174 runs.push(EditorTextRun {
175 index: p_idx,
176 byte_range: meta.byte_range.start..(meta.byte_range.start + trimmed.len()),
177 baseline_y: para_top - viewport_top + first_layout_baseline(&layout),
178 layout,
179 });
180 }
181 para_top = para_bottom;
182 }
183
184 metrics_cache.update_metadata(
185 buffer.paragraphs().len(),
186 buffer.paragraphs().iter().map(|meta| meta.revision),
187 para_top,
188 content_width,
189 );
190
191 VisibleTextLayout {
192 runs,
193 content_size: Size::new(content_width, para_top),
194 metrics_stats: stats,
195 used_fast_path: false,
196 }
197}
198
199fn build_visible_nowrap_text_layout(
205 buffer: &TextBuffer,
206 viewport: EditorViewport,
207 style: EditorStyle,
208 layout_cache: &mut LayoutCache,
209 metrics_cache: &mut ParagraphMetricsCache,
210 text_system: &mut TextSystem,
211) -> VisibleTextLayout {
212 let text_style = TextStyle::new(style.font, style.px_size, style.fg);
213 let line_height = style.line_height.max(1.0);
214 let paragraph_count = buffer.paragraphs().len();
215 let viewport_top = viewport.scroll_y.max(0.0);
216 let viewport_bot = viewport_top + viewport.content_rect.h.max(0.0);
217 let heights = vec![line_height; paragraph_count];
218 let height_index = FenwickTree::from_values(&heights);
219 let first = height_index.lower_bound(viewport_top).min(paragraph_count);
220 let last = (height_index.lower_bound(viewport_bot) + 2).min(paragraph_count);
221 let mut runs = Vec::new();
222 let max_width = viewport.max_text_width();
223 let wrap_mode = viewport.text_wrap_mode();
224 let mut stats = ParagraphMetricsStats::default();
225
226 let mut content_width = 0.0f32;
230 for p_idx in 0..paragraph_count {
231 if let Some(paragraph) = buffer.paragraph_text(p_idx) {
232 content_width = content_width.max(estimate_nowrap_width(
233 paragraph.trim_end_matches('\n'),
234 style,
235 ));
236 }
237 }
238
239 for p_idx in first.min(paragraph_count)..last {
240 let meta = &buffer.paragraphs()[p_idx];
241 let Some(paragraph) = buffer.paragraph_text(p_idx) else {
242 continue;
243 };
244 let trimmed = paragraph.trim_end_matches('\n').to_string();
245 let key = LayoutCacheKey::new(
246 p_idx,
247 meta.revision,
248 &trimmed,
249 text_style,
250 wrap_mode,
251 max_width,
252 );
253 let layout = layout_cache.layout_paragraph(
254 key,
255 &trimmed,
256 text_style,
257 max_width,
258 wrap_mode,
259 text_system,
260 );
261 let metrics = if let Some(metrics) = metrics_cache.get(&key) {
262 stats.hits += 1;
263 metrics
264 } else {
265 stats.misses += 1;
266 let metrics = ParagraphMetrics {
267 width: layout.width(),
268 height: layout_visual_height(&layout, style),
269 line_count: 1,
270 local_version: meta.revision,
271 };
272 metrics_cache.insert(key, metrics);
273 metrics
274 };
275 content_width = content_width.max(metrics.width);
276 runs.push(EditorTextRun {
277 index: p_idx,
278 byte_range: meta.byte_range.start..(meta.byte_range.start + trimmed.len()),
279 baseline_y: p_idx as f32 * line_height - viewport_top + first_layout_baseline(&layout),
280 layout,
281 });
282 }
283 metrics_cache.update_metadata(
284 paragraph_count,
285 buffer.paragraphs().iter().map(|meta| meta.revision),
286 paragraph_count as f32 * line_height,
287 content_width,
288 );
289
290 VisibleTextLayout {
291 runs,
292 content_size: Size::new(content_width, paragraph_count as f32 * line_height),
293 metrics_stats: stats,
294 used_fast_path: true,
295 }
296}
297
298fn estimate_nowrap_width(text: &str, style: EditorStyle) -> f32 {
300 text.chars().count() as f32 * style.px_size as f32 * 0.62
301}