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