Skip to main content

rich/
layout.rs

1//! Screen layout — split a region into ratioed rows and columns.
2//!
3//! Port of `rich/layout.py`. A [`Layout`] is a tree: a leaf holds a
4//! renderable, a branch splits its region among its *visible* children
5//! either into rows (side by side, [`Splitter::Row`]) or columns (stacked,
6//! [`Splitter::Column`]). Region sizes come from [`ratio_resolve`], and each
7//! leaf is rendered to an exact `(width, height)` block, then tiled. A leaf
8//! with no renderable shows upstream's placeholder panel; the last render's
9//! regions are kept in [`Layout::map`].
10
11use std::sync::{Arc, Mutex};
12
13use crate::align::{Align, VerticalAlign};
14use crate::cells::cell_len;
15use crate::console::{Console, ConsoleOptions};
16use crate::highlighter::ReprHighlighter;
17use crate::measure::Measurement;
18use crate::panel::Panel;
19use crate::protocol::{Highlighter, Renderable};
20use crate::ratio::{ratio_resolve, Edge};
21use crate::region::Region;
22use crate::segment::Segment;
23use crate::style::Style;
24use crate::table::{Cell, Table};
25use crate::text::Text;
26use crate::tree::Tree;
27
28/// How a layout divides its region among its children. Mirrors upstream's
29/// `RowSplitter` / `ColumnSplitter`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum Splitter {
32    /// Children are placed side by side (`split_row`).
33    Row,
34    /// Children are stacked vertically (`split_column`, the default).
35    #[default]
36    Column,
37}
38
39impl Splitter {
40    /// The splitter's name (`Splitter.name`): `"row"` or `"column"`.
41    pub fn name(self) -> &'static str {
42        match self {
43            Splitter::Row => "row",
44            Splitter::Column => "column",
45        }
46    }
47
48    /// The icon markup `Layout.tree` shows (`Splitter.get_tree_icon`).
49    pub fn tree_icon(self) -> &'static str {
50        match self {
51            Splitter::Row => "[layout.tree.row]⬌",
52            Splitter::Column => "[layout.tree.column]⬍",
53        }
54    }
55
56    /// A splitter by name (`Layout.splitters[name]`).
57    pub fn from_name(name: &str) -> Option<Splitter> {
58        match name {
59            "row" => Some(Splitter::Row),
60            "column" => Some(Splitter::Column),
61            _ => None,
62        }
63    }
64
65    /// `Splitter.divide`: `region` shared among `children`, in order.
66    fn divide(self, children: &[&Layout], region: Region) -> Vec<Region> {
67        let edges: Vec<Edge> = children.iter().map(|child| child.edge()).collect();
68        let Region {
69            x,
70            y,
71            width,
72            height,
73        } = region;
74        let mut offset = 0;
75        match self {
76            Splitter::Row => ratio_resolve(width, &edges)
77                .into_iter()
78                .map(|child_width| {
79                    let region = Region::new(x.saturating_add(offset), y, child_width, height);
80                    offset = offset.saturating_add(child_width);
81                    region
82                })
83                .collect(),
84            Splitter::Column => ratio_resolve(height, &edges)
85                .into_iter()
86                .map(|child_height| {
87                    let region = Region::new(x, y.saturating_add(offset), width, child_height);
88                    offset = offset.saturating_add(child_height);
89                    region
90                })
91                .collect(),
92        }
93    }
94}
95
96/// One leaf of a render. Mirrors upstream's `LayoutRender`, keyed by the
97/// layout's `name` and its `path` (child indexes from the root, counting
98/// hidden children) since the port has no object identity to key by.
99#[derive(Debug, Clone, PartialEq)]
100pub struct LayoutRender {
101    /// The leaf's name, if it has one.
102    pub name: Option<String>,
103    /// Child indexes from the root layout to the leaf.
104    pub path: Vec<usize>,
105    /// Where the leaf was drawn.
106    pub region: Region,
107    /// Its rendered lines.
108    pub render: Vec<Vec<Segment>>,
109}
110
111/// A node in a layout tree. Mirrors `rich.layout.Layout`.
112pub struct Layout {
113    renderable: Option<Box<dyn Renderable>>,
114    name: Option<String>,
115    visible: bool,
116    children: Vec<Layout>,
117    splitter: Splitter,
118    /// A fixed size along the parent's split axis, if pinned.
119    size: Option<usize>,
120    /// Flex weight when unsized (defaults to 1).
121    ratio: usize,
122    /// The smallest size this region may shrink to.
123    minimum_size: usize,
124    /// The last render (upstream `_render_map`).
125    render_map: Mutex<Vec<LayoutRender>>,
126}
127
128impl Default for Layout {
129    fn default() -> Self {
130        Layout::new()
131    }
132}
133
134impl Layout {
135    /// An empty layout (no renderable, so it shows the placeholder; no
136    /// children).
137    pub fn new() -> Self {
138        Layout {
139            renderable: None,
140            name: None,
141            visible: true,
142            children: Vec::new(),
143            splitter: Splitter::Column,
144            size: None,
145            ratio: 1,
146            minimum_size: 1,
147            render_map: Mutex::new(Vec::new()),
148        }
149    }
150
151    /// A leaf layout wrapping `renderable`.
152    pub fn with_renderable(renderable: Box<dyn Renderable>) -> Self {
153        let mut layout = Layout::new();
154        layout.renderable = Some(renderable);
155        layout
156    }
157
158    /// Name this layout, for [`get`](Self::get) (upstream `name`).
159    pub fn name(mut self, name: impl Into<String>) -> Self {
160        self.name = Some(name.into());
161        self
162    }
163
164    /// Show or hide this layout (upstream `visible`, default on). A hidden
165    /// child takes no space in its parent's split.
166    pub fn visible(mut self, visible: bool) -> Self {
167        self.visible = visible;
168        self
169    }
170
171    /// Pin this region to a fixed size along its parent's split axis.
172    pub fn size(mut self, size: usize) -> Self {
173        self.size = Some(size);
174        self
175    }
176
177    /// Set the flex weight used when this region is unsized.
178    pub fn ratio(mut self, ratio: usize) -> Self {
179        self.ratio = ratio;
180        self
181    }
182
183    /// Set the minimum size this region may shrink to.
184    pub fn minimum_size(mut self, minimum_size: usize) -> Self {
185        self.minimum_size = minimum_size;
186        self
187    }
188
189    /// Set (or clear) the name in place.
190    pub fn set_name(&mut self, name: Option<String>) -> &mut Self {
191        self.name = name;
192        self
193    }
194
195    /// Set the visibility in place.
196    pub fn set_visible(&mut self, visible: bool) -> &mut Self {
197        self.visible = visible;
198        self
199    }
200
201    /// Set (or clear) the fixed size in place.
202    pub fn set_size(&mut self, size: Option<usize>) -> &mut Self {
203        self.size = size;
204        self
205    }
206
207    /// Set the ratio in place.
208    pub fn set_ratio(&mut self, ratio: usize) -> &mut Self {
209        self.ratio = ratio;
210        self
211    }
212
213    /// Set the minimum size in place.
214    pub fn set_minimum_size(&mut self, minimum_size: usize) -> &mut Self {
215        self.minimum_size = minimum_size;
216        self
217    }
218
219    /// The name, if any.
220    pub fn get_name(&self) -> Option<&str> {
221        self.name.as_deref()
222    }
223
224    /// Whether this layout is visible.
225    pub fn is_visible(&self) -> bool {
226        self.visible
227    }
228
229    /// The fixed size, if any.
230    pub fn get_size(&self) -> Option<usize> {
231        self.size
232    }
233
234    /// The ratio.
235    pub fn get_ratio(&self) -> usize {
236        self.ratio
237    }
238
239    /// The minimum size.
240    pub fn get_minimum_size(&self) -> usize {
241        self.minimum_size
242    }
243
244    /// The splitter dividing this layout among its children.
245    pub fn splitter(&self) -> Splitter {
246        self.splitter
247    }
248
249    /// The leaf renderable, if one is set (none shows the placeholder).
250    pub fn renderable(&self) -> Option<&dyn Renderable> {
251        self.renderable.as_deref()
252    }
253
254    /// Replace the leaf renderable. Port of `Layout.update`.
255    pub fn update(&mut self, renderable: Box<dyn Renderable>) {
256        self.renderable = Some(renderable);
257    }
258
259    /// The visible children (upstream's `children` property).
260    pub fn children(&self) -> Vec<&Layout> {
261        self.children.iter().filter(|child| child.visible).collect()
262    }
263
264    /// Every child, hidden ones included (upstream `_children`).
265    pub fn all_children(&self) -> &[Layout] {
266        &self.children
267    }
268
269    /// Every child, mutably.
270    pub fn all_children_mut(&mut self) -> &mut Vec<Layout> {
271        &mut self.children
272    }
273
274    /// Split into `children` with `splitter`. Port of `Layout.split`.
275    pub fn split(&mut self, children: Vec<Layout>, splitter: Splitter) {
276        self.splitter = splitter;
277        self.children = children;
278    }
279
280    /// Split into children stacked vertically. Port of `Layout.split_column`.
281    pub fn split_column(&mut self, children: Vec<Layout>) {
282        self.split(children, Splitter::Column);
283    }
284
285    /// Split into children placed side by side. Port of `Layout.split_row`.
286    pub fn split_row(&mut self, children: Vec<Layout>) {
287        self.split(children, Splitter::Row);
288    }
289
290    /// Add children to the existing split. Port of `Layout.add_split`.
291    pub fn add_split(&mut self, children: Vec<Layout>) {
292        self.children.extend(children);
293    }
294
295    /// Remove every child. Port of `Layout.unsplit`.
296    pub fn unsplit(&mut self) {
297        self.children.clear();
298    }
299
300    /// The first layout named `name`, depth first from this one. Port of
301    /// `Layout.get`.
302    pub fn get(&self, name: &str) -> Option<&Layout> {
303        if self.name.as_deref() == Some(name) {
304            return Some(self);
305        }
306        self.children.iter().find_map(|child| child.get(name))
307    }
308
309    /// [`get`](Self::get), mutably.
310    pub fn get_mut(&mut self, name: &str) -> Option<&mut Layout> {
311        if self.name.as_deref() == Some(name) {
312            return Some(self);
313        }
314        self.children
315            .iter_mut()
316            .find_map(|child| child.get_mut(name))
317    }
318
319    /// The layout at `path` (child indexes, hidden children counted), as
320    /// [`LayoutRender::path`] records it.
321    pub fn at_path(&self, path: &[usize]) -> Option<&Layout> {
322        path.iter()
323            .try_fold(self, |layout, &index| layout.children.get(index))
324    }
325
326    fn edge(&self) -> Edge {
327        Edge::new(self.size, self.ratio, self.minimum_size)
328    }
329
330    /// Upstream's `__rich_repr__` fields that differ from their defaults, as
331    /// `key=value` reprs.
332    fn repr_fields(&self) -> Vec<String> {
333        let mut fields = Vec::new();
334        if let Some(name) = &self.name {
335            fields.push(format!("name={}", py_repr_str(name)));
336        }
337        if let Some(size) = self.size {
338            fields.push(format!("size={size}"));
339        }
340        if self.minimum_size != 1 {
341            fields.push(format!("minimum_size={}", self.minimum_size));
342        }
343        if self.ratio != 1 {
344            fields.push(format!("ratio={}", self.ratio));
345        }
346        fields
347    }
348
349    /// A tree renderable showing the layout's structure. Port of
350    /// `Layout.tree`.
351    pub fn tree(&self) -> Tree {
352        fn summary(layout: &Layout) -> Cell {
353            let mut table = Table::grid().padding(0, 1, 0, 0);
354            table.add_column("").add_column("");
355            table.add_row_cells(vec![
356                Cell::Markup(layout.splitter.tree_icon().to_string()),
357                Cell::Renderable(Arc::new(LayoutRepr {
358                    fields: layout.repr_fields(),
359                    dim: !layout.visible,
360                })),
361            ]);
362            Cell::Renderable(Arc::new(table))
363        }
364        fn recurse(tree: &mut Tree, layout: &Layout) {
365            for child in &layout.children {
366                let node = tree.add(summary(child));
367                node.set_guide_style(format!("layout.tree.{}", child.splitter.name()));
368                recurse(node, child);
369            }
370        }
371        let mut tree = Tree::new(summary(self))
372            .guide_style(format!("layout.tree.{}", self.splitter.name()))
373            .highlight(true);
374        recurse(&mut tree, self);
375        tree
376    }
377
378    /// Every layout's region for a `width` by `height` render, as `(path,
379    /// region)` sorted by region. Port of `Layout._make_region_map`.
380    pub fn region_map(&self, width: usize, height: usize) -> Vec<(Vec<usize>, Region)> {
381        let mut stack: Vec<(Vec<usize>, &Layout, Region)> =
382            vec![(Vec::new(), self, Region::new(0, 0, width, height))];
383        let mut regions: Vec<(Vec<usize>, &Layout, Region)> = Vec::new();
384        while let Some(entry) = stack.pop() {
385            let (path, layout, region) = &entry;
386            let visible: Vec<(usize, &Layout)> = layout
387                .children
388                .iter()
389                .enumerate()
390                .filter(|(_, child)| child.visible)
391                .collect();
392            if !visible.is_empty() {
393                let children: Vec<&Layout> = visible.iter().map(|(_, child)| *child).collect();
394                for ((index, child), child_region) in visible
395                    .iter()
396                    .zip(layout.splitter.divide(&children, *region))
397                {
398                    let mut child_path = path.clone();
399                    child_path.push(*index);
400                    stack.push((child_path, child, child_region));
401                }
402            }
403            regions.push(entry);
404        }
405        regions.sort_by_key(|(_, _, region)| *region);
406        regions
407            .into_iter()
408            .map(|(path, _, region)| (path, region))
409            .collect()
410    }
411
412    /// Render every leaf into its region. Port of `Layout.render`: `options`
413    /// give the width and (else the console's) height.
414    pub fn render_regions(&self, console: &Console, options: &ConsoleOptions) -> Vec<LayoutRender> {
415        let width = options.max_width;
416        let height = options
417            .height
418            .filter(|&height| height > 0)
419            .unwrap_or_else(|| console.height());
420        self.region_map(width, height)
421            .into_iter()
422            .filter_map(|(path, region)| {
423                let layout = self.at_path(&path)?;
424                if !layout.children().is_empty() {
425                    return None;
426                }
427                let leaf_options = options.update_dimensions(region.width, region.height);
428                let render = match &layout.renderable {
429                    Some(renderable) => {
430                        console.render_lines(renderable.as_ref(), &leaf_options, true)
431                    }
432                    None => console.render_lines(&Placeholder { layout }, &leaf_options, true),
433                };
434                Some(LayoutRender {
435                    name: layout.name.clone(),
436                    path,
437                    region,
438                    render,
439                })
440            })
441            .collect()
442    }
443
444    /// Child indexes from this layout to the first layout named `name`, in
445    /// [`get`](Self::get)'s order.
446    fn path_of(&self, name: &str) -> Option<Vec<usize>> {
447        if self.name.as_deref() == Some(name) {
448            return Some(Vec::new());
449        }
450        self.children.iter().enumerate().find_map(|(index, child)| {
451            child.path_of(name).map(|mut path| {
452                path.insert(0, index);
453                path
454            })
455        })
456    }
457
458    /// Render the layout named `layout_name` again and write it over its
459    /// region of the alternate screen. Port of `Layout.refresh_screen`: the
460    /// layout must be a leaf of the last render (upstream raises `KeyError`
461    /// otherwise; here, `Ok(false)` and nothing is written), and the console
462    /// must be in the alternate screen ([`RichError::NoAltScreen`] otherwise).
463    ///
464    /// [`RichError::NoAltScreen`]: crate::errors::RichError::NoAltScreen
465    pub fn refresh_screen(
466        &self,
467        console: &Console,
468        layout_name: &str,
469    ) -> crate::errors::Result<bool> {
470        let Some(path) = self.path_of(layout_name) else {
471            return Ok(false);
472        };
473        let Some(layout) = self.at_path(&path) else {
474            return Ok(false);
475        };
476        // The region comes from the last render; the lock is released before
477        // rendering, since the leaf may be this layout itself.
478        let region = self
479            .render_map
480            .lock()
481            .unwrap_or_else(|poisoned| poisoned.into_inner())
482            .iter()
483            .find(|entry| entry.path == path)
484            .map(|entry| entry.region);
485        let Some(Region {
486            x,
487            y,
488            width,
489            height,
490        }) = region
491        else {
492            return Ok(false);
493        };
494        let lines = console.render_lines(
495            layout,
496            &console.options().update_dimensions(width, height),
497            true,
498        );
499        if let Some(entry) = self
500            .render_map
501            .lock()
502            .unwrap_or_else(|poisoned| poisoned.into_inner())
503            .iter_mut()
504            .find(|entry| entry.path == path)
505        {
506            entry.render = lines.clone();
507        }
508        console.update_screen_lines(&lines, x, y)?;
509        Ok(true)
510    }
511
512    /// The leaves of the last render. Port of `Layout.map`.
513    pub fn map(&self) -> Vec<LayoutRender> {
514        self.render_map
515            .lock()
516            .unwrap_or_else(|poisoned| poisoned.into_inner())
517            .clone()
518    }
519}
520
521impl std::ops::Index<&str> for Layout {
522    type Output = Layout;
523
524    /// `layout[name]`; panics as upstream raises `KeyError` when there is no
525    /// such layout.
526    fn index(&self, name: &str) -> &Layout {
527        self.get(name)
528            .unwrap_or_else(|| panic!("No layout with name {name:?}"))
529    }
530}
531
532impl std::ops::IndexMut<&str> for Layout {
533    fn index_mut(&mut self, name: &str) -> &mut Layout {
534        self.get_mut(name)
535            .unwrap_or_else(|| panic!("No layout with name {name:?}"))
536    }
537}
538
539/// Python's `repr()` of a string.
540fn py_repr_str(value: &str) -> String {
541    let quote = if value.contains('\'') && !value.contains('"') {
542        '"'
543    } else {
544        '\''
545    };
546    let mut out = String::with_capacity(value.len() + 2);
547    out.push(quote);
548    for ch in value.chars() {
549        match ch {
550            '\\' => out.push_str("\\\\"),
551            '\n' => out.push_str("\\n"),
552            '\r' => out.push_str("\\r"),
553            '\t' => out.push_str("\\t"),
554            ch if ch == quote => {
555                out.push('\\');
556                out.push(ch);
557            }
558            ch if (ch as u32) < 0x20 || ch as u32 == 0x7f => {
559                out.push_str(&format!("\\x{:02x}", ch as u32));
560            }
561            ch => out.push(ch),
562        }
563    }
564    out.push(quote);
565    out
566}
567
568/// `Pretty(layout)`: the layout's repr, on one line when it fits the width,
569/// else one field per line; highlighted with the `ReprHighlighter`, and dim
570/// for a hidden layout (`Styled(Pretty(layout), "dim")`).
571struct LayoutRepr {
572    fields: Vec<String>,
573    dim: bool,
574}
575
576impl LayoutRepr {
577    /// `pretty_repr(layout, max_width=…)`.
578    fn repr(&self, max_width: usize) -> String {
579        let one_line = format!("Layout({})", self.fields.join(", "));
580        if self.fields.is_empty() || cell_len(&one_line) <= max_width {
581            return one_line;
582        }
583        let last = self.fields.len() - 1;
584        let mut out = String::from("Layout(\n");
585        for (index, field) in self.fields.iter().enumerate() {
586            out.push_str("    ");
587            out.push_str(field);
588            if index != last {
589                out.push(',');
590            }
591            out.push('\n');
592        }
593        out.push(')');
594        out
595    }
596}
597
598impl Renderable for LayoutRepr {
599    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
600        let mut text = Text::new(self.repr(options.max_width));
601        ReprHighlighter::new().highlight(&mut text);
602        let segments = text.rich_render(console, options);
603        if self.dim {
604            Segment::apply_style(&segments, &Style::parse("dim").unwrap_or_default())
605        } else {
606            segments
607        }
608    }
609
610    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
611        let width = self
612            .repr(options.max_width)
613            .lines()
614            .map(cell_len)
615            .max()
616            .unwrap_or(0);
617        Measurement::new(width, width)
618    }
619}
620
621/// Upstream's `_Placeholder`: a blue panel titled with the layout's name and
622/// size, around its centred repr.
623struct Placeholder<'a> {
624    layout: &'a Layout,
625}
626
627impl Renderable for Placeholder<'_> {
628    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
629        let width = options.max_width;
630        let height = options
631            .height
632            .filter(|&height| height > 0)
633            .unwrap_or(options.size.height);
634        let title = match &self.layout.name {
635            Some(name) => format!("{} ({width} x {height})", py_repr_str(name)),
636            None => format!("({width} x {height})"),
637        };
638        let mut title = Text::new(title);
639        ReprHighlighter::new().highlight(&mut title);
640        let repr = LayoutRepr {
641            fields: self.layout.repr_fields(),
642            dim: false,
643        };
644        Panel::new(Box::new(
645            Align::center(Box::new(repr)).vertical(VerticalAlign::Middle),
646        ))
647        .title_as_text(title)
648        .border_style("blue")
649        .height(height)
650        .rich_render(console, options)
651    }
652}
653
654impl Renderable for Layout {
655    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
656        let width = if options.max_width > 0 {
657            options.max_width
658        } else {
659            console.width()
660        };
661        let height = options
662            .height
663            .filter(|&height| height > 0)
664            .unwrap_or_else(|| console.height());
665        let render_map = self.render_regions(console, &options.update_dimensions(width, height));
666        let mut lines: Vec<Vec<Segment>> = vec![Vec::new(); height];
667        for leaf in &render_map {
668            let Region { y, height, .. } = leaf.region;
669            for (row, line) in lines.iter_mut().skip(y).take(height).zip(&leaf.render) {
670                row.extend(line.iter().cloned());
671            }
672        }
673        *self
674            .render_map
675            .lock()
676            .unwrap_or_else(|poisoned| poisoned.into_inner()) = render_map;
677
678        let mut segments = Vec::new();
679        let last = lines.len().saturating_sub(1);
680        for (index, line) in lines.into_iter().enumerate() {
681            segments.extend(line);
682            if index != last {
683                segments.push(Segment::line());
684            }
685        }
686        segments
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use crate::color::ColorSystem;
694    use crate::text::Text;
695
696    fn console(width: usize, height: usize) -> Console {
697        Console::builder()
698            .force_terminal(true)
699            .color_system(Some(ColorSystem::Truecolor))
700            .width(width)
701            .height(height)
702            .build()
703    }
704
705    fn leaf(s: &str) -> Layout {
706        Layout::with_renderable(Box::new(Text::new(s)))
707    }
708
709    /// A line of `text` left-justified into `width` cells.
710    fn cell(text: &str, width: usize) -> String {
711        format!("{text}{}", " ".repeat(width - text.chars().count()))
712    }
713
714    #[test]
715    fn column_split_stacks() {
716        let c = console(24, 4);
717        let mut lay = Layout::new();
718        lay.split_column(vec![leaf("top"), leaf("bottom")]);
719        // Captured from real rich 15.0.0: two ratio-1 rows over height 4.
720        let blank = " ".repeat(24);
721        let expected = format!(
722            "{}\n{blank}\n{}\n{blank}\n",
723            cell("top", 24),
724            cell("bottom", 24)
725        );
726        assert_eq!(c.capture(|con| con.print(&lay)), expected);
727    }
728
729    #[test]
730    fn row_split_side_by_side() {
731        let c = console(24, 4);
732        let mut lay = Layout::new();
733        lay.split_row(vec![leaf("L"), leaf("R")]);
734        // Two ratio-1 columns of width 12; only row 0 has content.
735        let blank = " ".repeat(24);
736        let row0 = format!("{}{}", cell("L", 12), cell("R", 12));
737        let expected = format!("{row0}\n{blank}\n{blank}\n{blank}\n");
738        assert_eq!(c.capture(|con| con.print(&lay)), expected);
739    }
740}