1use std::collections::BTreeMap;
2
3use crate::{
4 grapheme::StyledGraphemes,
5 widget::{
6 ContentPosition, CreatedGraphemes, ScreenPosition, VisualPosition, WidgetViewport,
7 WidthMode,
8 },
9};
10
11#[derive(Debug)]
17pub struct RendererLayout<K> {
18 viewport_rows: BTreeMap<K, usize>,
19}
20
21impl<K> Default for RendererLayout<K> {
22 fn default() -> Self {
23 Self {
24 viewport_rows: BTreeMap::new(),
25 }
26 }
27}
28
29#[derive(Clone, Debug)]
30pub(super) struct VisualRow {
31 pub(super) content_row: usize,
32 pub(super) content_column: usize,
33 pub(super) graphemes: StyledGraphemes,
34}
35
36#[derive(Clone, Debug)]
37pub(super) struct LayoutEntry<K> {
38 pub(super) index: K,
39 pub(super) viewport: WidgetViewport,
40 pub(super) rows: Vec<VisualRow>,
41}
42
43#[derive(Clone, Debug)]
44pub(super) struct LayoutSnapshot<K> {
45 pub(super) origin: ScreenPosition,
46 pub(super) terminal_width: u16,
47 pub(super) entries: Vec<LayoutEntry<K>>,
48}
49
50#[derive(Debug)]
57pub struct PreparedLayout<K> {
58 pub(super) terminal_width: u16,
59 pub(super) entries: Vec<LayoutEntry<K>>,
60}
61
62impl<K> PreparedLayout<K> {
63 pub fn panes(&self) -> Vec<Vec<&StyledGraphemes>> {
65 self.entries
66 .iter()
67 .map(|entry| {
68 entry
69 .rows
70 .iter()
71 .skip(entry.viewport.content_row)
72 .take(entry.viewport.height as usize)
73 .map(|row| &row.graphemes)
74 .collect()
75 })
76 .collect()
77 }
78
79 pub fn pane_count(&self) -> usize {
81 self.entries.len()
82 }
83
84 pub fn visual_row_count(&self) -> usize {
86 self.entries.iter().map(|entry| entry.rows.len()).sum()
87 }
88
89 pub fn visible_row_count(&self) -> usize {
91 self.entries
92 .iter()
93 .map(|entry| {
94 entry
95 .rows
96 .len()
97 .saturating_sub(entry.viewport.content_row)
98 .min(entry.viewport.height as usize)
99 })
100 .sum()
101 }
102
103 pub(super) fn into_snapshot(mut self, origin: ScreenPosition) -> LayoutSnapshot<K> {
104 let mut screen_row = origin.row;
105 for entry in &mut self.entries {
106 entry.viewport.screen_row = screen_row;
107 screen_row = screen_row.saturating_add(entry.viewport.height);
108 }
109
110 LayoutSnapshot {
111 origin,
112 terminal_width: self.terminal_width,
113 entries: self.entries,
114 }
115 }
116}
117
118impl<K: Clone + Ord> RendererLayout<K> {
119 pub fn layout<I>(
125 &mut self,
126 contents: I,
127 terminal_width: u16,
128 terminal_height: u16,
129 ) -> anyhow::Result<PreparedLayout<K>>
130 where
131 I: IntoIterator<Item = (K, CreatedGraphemes)>,
132 {
133 let laid_out = contents
134 .into_iter()
135 .map(|(index, created)| {
136 let CreatedGraphemes {
137 graphemes,
138 layout,
139 cursor,
140 } = created;
141 let rows = layout_content(graphemes, layout.width_mode, terminal_width as usize);
142 (index, layout, cursor, rows)
143 })
144 .filter(|(_, layout, _, rows)| !rows.is_empty() && layout.max_height != Some(0))
145 .collect::<Vec<_>>();
146
147 if laid_out.len() > terminal_height as usize {
148 return Err(anyhow::anyhow!("Insufficient space to display all panes"));
149 }
150
151 let mut entries = Vec::with_capacity(laid_out.len());
152 let mut used_height = 0usize;
153 let pane_count = laid_out.len();
154
155 for (pane_index, (index, layout, cursor, rows)) in laid_out.into_iter().enumerate() {
156 let panes_after = pane_count.saturating_sub(pane_index + 1);
157 let available = (terminal_height as usize)
158 .saturating_sub(used_height)
159 .saturating_sub(panes_after);
160 let desired = layout.max_height.unwrap_or(rows.len()).min(rows.len());
161 let height = desired.min(available).max(1);
162 used_height = used_height.saturating_add(height);
163
164 let mut viewport = WidgetViewport {
165 height: height as u16,
166 content_row: self.viewport_rows.get(&index).copied().unwrap_or_default(),
167 ..Default::default()
168 };
169
170 let max_content_row = rows.len().saturating_sub(height);
171 viewport.content_row = viewport.content_row.min(max_content_row);
172
173 if let Some(cursor) = cursor
174 && let Some(position) = visual_position(&rows, cursor)
175 {
176 viewport.scroll_to_include(position);
177 viewport.content_row = viewport.content_row.min(max_content_row);
178 }
179
180 self.viewport_rows
181 .insert(index.clone(), viewport.content_row);
182 entries.push(LayoutEntry {
183 index,
184 viewport,
185 rows,
186 });
187 }
188
189 Ok(PreparedLayout {
190 terminal_width,
191 entries,
192 })
193 }
194
195 pub(super) fn remove(&mut self, index: &K) {
196 self.viewport_rows.remove(index);
197 }
198}
199
200fn layout_content(
201 graphemes: StyledGraphemes,
202 width_mode: WidthMode,
203 width: usize,
204) -> Vec<VisualRow> {
205 if width == 0 {
206 return Vec::new();
207 }
208
209 into_logical_lines(graphemes)
210 .into_iter()
211 .enumerate()
212 .flat_map(|(content_row, line)| match width_mode {
213 WidthMode::Wrap => wrap_line(content_row, line, width),
214 WidthMode::Truncate => vec![VisualRow {
215 content_row,
216 content_column: 0,
217 graphemes: truncate_line(line, width),
218 }],
219 })
220 .collect()
221}
222
223fn into_logical_lines(graphemes: StyledGraphemes) -> Vec<StyledGraphemes> {
224 if graphemes.is_empty() {
225 return Vec::new();
226 }
227
228 let mut lines = Vec::new();
229 let mut line = StyledGraphemes::default();
230 let mut last_was_newline = false;
231
232 for styled in graphemes.0 {
233 if styled.character() == '\n' {
234 lines.push(line);
235 line = StyledGraphemes::default();
236 last_was_newline = true;
237 } else {
238 line.push_back(styled);
239 last_was_newline = false;
240 }
241 }
242
243 if !line.is_empty() || last_was_newline {
244 lines.push(line);
245 }
246
247 lines
248}
249
250fn wrap_line(content_row: usize, line: StyledGraphemes, width: usize) -> Vec<VisualRow> {
251 if line.is_empty() {
252 return vec![VisualRow {
253 content_row,
254 content_column: 0,
255 graphemes: line,
256 }];
257 }
258
259 let mut rows = Vec::new();
260 let mut row = StyledGraphemes::default();
261 let mut row_width = 0usize;
262 let mut content_column = 0usize;
263 let mut row_column = 0usize;
264
265 for grapheme in line.0 {
266 let grapheme_width = grapheme.width();
267 if grapheme_width > width {
268 if !row.is_empty() {
269 rows.push(VisualRow {
270 content_row,
271 content_column: row_column,
272 graphemes: row,
273 });
274 row = StyledGraphemes::default();
275 row_width = 0;
276 }
277
278 rows.push(VisualRow {
281 content_row,
282 content_column,
283 graphemes: StyledGraphemes::from("…"),
284 });
285 content_column = content_column.saturating_add(grapheme_width);
286 row_column = content_column;
287 continue;
288 }
289
290 if !row.is_empty() && row_width.saturating_add(grapheme_width) > width {
291 rows.push(VisualRow {
292 content_row,
293 content_column: row_column,
294 graphemes: row,
295 });
296 row = StyledGraphemes::default();
297 row_width = 0;
298 row_column = content_column;
299 }
300
301 row.push_back(grapheme);
302 row_width = row_width.saturating_add(grapheme_width);
303 content_column = content_column.saturating_add(grapheme_width);
304 }
305
306 if !row.is_empty() {
307 rows.push(VisualRow {
308 content_row,
309 content_column: row_column,
310 graphemes: row,
311 });
312 }
313
314 rows
315}
316
317fn truncate_line(line: StyledGraphemes, width: usize) -> StyledGraphemes {
318 if line.widths() <= width {
319 return line;
320 }
321
322 if width == 0 {
323 return StyledGraphemes::default();
324 }
325
326 let mut ellipsis = StyledGraphemes::from("…");
327 let ellipsis_width = ellipsis.widths();
328 if width <= ellipsis_width {
329 return ellipsis;
330 }
331
332 let mut truncated = StyledGraphemes::default();
333 let mut current_width = 0usize;
334 for grapheme in line.0 {
335 if current_width
336 .saturating_add(grapheme.width())
337 .saturating_add(ellipsis_width)
338 > width
339 {
340 break;
341 }
342 current_width = current_width.saturating_add(grapheme.width());
343 truncated.push_back(grapheme);
344 }
345 truncated.append(&mut ellipsis);
346 truncated
347}
348
349pub(super) fn visual_position(
350 rows: &[VisualRow],
351 position: ContentPosition,
352) -> Option<VisualPosition> {
353 let matching = rows
354 .iter()
355 .enumerate()
356 .filter(|(_, row)| row.content_row == position.row)
357 .collect::<Vec<_>>();
358
359 let (row_index, row) = matching
360 .iter()
361 .copied()
362 .find(|(_, row)| {
363 let end = row.content_column.saturating_add(row.graphemes.widths());
364 position.column >= row.content_column && position.column < end
365 })
366 .or_else(|| matching.last().copied())?;
367
368 Some(VisualPosition {
369 row: row_index,
370 column: position.column.saturating_sub(row.content_column),
371 })
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::widget::WidgetLayout;
378
379 mod visual_position {
380 use super::*;
381
382 #[test]
383 fn maps_a_logical_cursor_to_its_wrapped_row() {
384 let created = CreatedGraphemes {
385 graphemes: StyledGraphemes::from("abcdefghij"),
386 cursor: Some(ContentPosition { row: 0, column: 8 }),
387 ..Default::default()
388 };
389 let cursor = created.cursor.unwrap();
390 let rows = layout_content(created.graphemes, created.layout.width_mode, 4);
391
392 assert_eq!(rows.len(), 3);
393 assert_eq!(
394 visual_position(&rows, cursor),
395 Some(VisualPosition { row: 2, column: 0 })
396 );
397 }
398 }
399
400 mod wrap_line {
401 use super::*;
402
403 #[test]
404 fn preserves_a_grapheme_wider_than_the_terminal() {
405 let created = CreatedGraphemes {
406 graphemes: StyledGraphemes::from("界"),
407 cursor: Some(ContentPosition { row: 0, column: 0 }),
408 ..Default::default()
409 };
410
411 let cursor = created.cursor.unwrap();
412 let rows = layout_content(created.graphemes, created.layout.width_mode, 1);
413
414 assert_eq!(rows.len(), 1);
415 assert_eq!(rows[0].content_row, 0);
416 assert_eq!(rows[0].content_column, 0);
417 assert_eq!(rows[0].graphemes.to_string(), "…");
418 assert_eq!(
419 visual_position(&rows, cursor),
420 Some(VisualPosition { row: 0, column: 0 })
421 );
422 }
423
424 #[test]
425 fn preserves_columns_after_a_grapheme_wider_than_the_terminal() {
426 let created = CreatedGraphemes {
427 graphemes: StyledGraphemes::from("界a"),
428 cursor: Some(ContentPosition { row: 0, column: 2 }),
429 ..Default::default()
430 };
431
432 let cursor = created.cursor.unwrap();
433 let rows = layout_content(created.graphemes, created.layout.width_mode, 1);
434
435 assert_eq!(rows.len(), 2);
436 assert_eq!(rows[0].content_column, 0);
437 assert_eq!(rows[0].graphemes.to_string(), "…");
438 assert_eq!(rows[1].content_column, 2);
439 assert_eq!(rows[1].graphemes.to_string(), "a");
440 assert_eq!(
441 visual_position(&rows, cursor),
442 Some(VisualPosition { row: 1, column: 0 })
443 );
444 }
445 }
446
447 mod truncate_line {
448 use super::*;
449
450 #[test]
451 fn keeps_one_visual_row_per_logical_row() {
452 let created = CreatedGraphemes {
453 graphemes: StyledGraphemes::from("abcdefghij\nsecond"),
454 layout: WidgetLayout {
455 width_mode: WidthMode::Truncate,
456 ..Default::default()
457 },
458 cursor: None,
459 };
460 let rows = layout_content(created.graphemes, created.layout.width_mode, 4);
461
462 assert_eq!(rows.len(), 2);
463 assert_eq!(rows[0].graphemes.to_string(), "abc…");
464 assert_eq!(rows[1].graphemes.to_string(), "sec…");
465 }
466 }
467
468 mod into_logical_lines {
469 use super::*;
470
471 #[test]
472 fn preserves_empty_logical_rows() {
473 let rows = layout_content(StyledGraphemes::from("first\n\n"), WidthMode::Wrap, 80);
474 let text = rows
475 .iter()
476 .map(|row| row.graphemes.to_string())
477 .collect::<Vec<_>>();
478
479 assert_eq!(text, ["first", "", ""]);
480 }
481 }
482
483 mod renderer_layout {
484 use super::*;
485
486 mod layout {
487 use super::*;
488
489 #[test]
490 fn allocates_height_and_preserves_the_viewport_between_frames() {
491 let created = CreatedGraphemes {
492 graphemes: StyledGraphemes::from("first\nsecond\nthird"),
493 layout: WidgetLayout {
494 max_height: Some(2),
495 ..Default::default()
496 },
497 cursor: Some(ContentPosition { row: 2, column: 0 }),
498 };
499 let mut layout = RendererLayout::default();
500
501 let first = layout.layout([(0, created.clone())], 80, 24).unwrap();
502 assert_eq!(first.pane_count(), 1);
503 assert_eq!(first.visual_row_count(), 3);
504 assert_eq!(first.visible_row_count(), 2);
505 let first_panes = first.panes();
506 assert_eq!(first_panes[0][0].to_string(), "second");
507
508 let second = layout.layout([(0, created)], 80, 24).unwrap();
509 let second_panes = second.panes();
510 assert_eq!(second_panes[0][0].to_string(), "second");
511 }
512 }
513 }
514}