Skip to main content

rich_ext/frame/
regions.rs

1//! Semantic regions in frames (#226, phase 2).
2//!
3//! Core renderables report regions through
4//! [`rich::protocol::RegionSink`] when one is installed, and tag the segments
5//! they drew with the region's id in their style metadata. A
6//! [`RegionRecorder`] is such a sink: it keeps each region's role, label and
7//! parent. [`Frame::from_segments`] reads the tags off the styles (so styles
8//! that differ only by a tag intern as one), and [`Frame::with_regions`] turns
9//! them into [`Region`]s: cell spans per row, with nesting. Links need no
10//! sink: [`Frame::regions`] derives them from the runs' styles.
11
12use std::collections::HashMap;
13use std::ops::Range;
14use std::sync::{Arc, Mutex, PoisonError};
15
16use rich::protocol::{
17    ConsoleRegions, REGION_META_KEY, RegionId, RegionInfo, RegionRole, RegionSink, region_of,
18};
19use rich::style::Meta;
20use rich::{Console, ConsoleOptions, Renderable, Style};
21
22use super::Frame;
23
24/// A [`RegionSink`] that records every region it is told about: its info and
25/// the region that was open when it started (its parent). Regions are
26/// numbered from 0 in the order they start.
27///
28/// Rendering is expected to happen on one thread at a time, as `Console`
29/// rendering does: the open-region stack is shared.
30#[derive(Debug, Default)]
31pub struct RegionRecorder {
32    state: Mutex<RecorderState>,
33}
34
35#[derive(Debug, Default)]
36struct RecorderState {
37    regions: Vec<(RegionInfo, Option<RegionId>)>,
38    open: Vec<RegionId>,
39}
40
41impl RegionRecorder {
42    pub fn new() -> Self {
43        RegionRecorder::default()
44    }
45
46    /// Every region recorded, in the order they started: id, info, parent.
47    pub fn regions(&self) -> Vec<(RegionId, RegionInfo, Option<RegionId>)> {
48        let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
49        state
50            .regions
51            .iter()
52            .enumerate()
53            .map(|(index, (info, parent))| (RegionId(index as u64), info.clone(), *parent))
54            .collect()
55    }
56}
57
58impl RegionSink for RegionRecorder {
59    fn enter(&self, region: RegionInfo) -> RegionId {
60        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
61        let id = RegionId(state.regions.len() as u64);
62        let parent = state.open.last().copied();
63        state.regions.push((region, parent));
64        state.open.push(id);
65        id
66    }
67
68    fn exit(&self, id: RegionId) {
69        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
70        if let Some(at) = state.open.iter().rposition(|open| *open == id) {
71            state.open.truncate(at);
72        }
73    }
74}
75
76/// The cells `columns` of row `row`.
77#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct Span {
79    pub row: usize,
80    pub columns: Range<usize>,
81}
82
83/// The smallest rectangle holding a region's spans.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct Rect {
86    pub row: usize,
87    pub column: usize,
88    pub width: usize,
89    pub height: usize,
90}
91
92/// A semantic region of a frame: the cells a renderable drew, with its role,
93/// an optional label and link target, and its place in the nesting.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct Region {
96    pub role: RegionRole,
97    pub label: Option<String>,
98    pub link: Option<String>,
99    /// The enclosing region, as an index into the same list; `None` at the
100    /// top. A parent always comes before its children.
101    pub parent: Option<usize>,
102    /// How many regions enclose this one.
103    pub depth: usize,
104    /// The cells covered, in row order, one span per contiguous stretch of a
105    /// row. A region covers its children's cells too.
106    pub spans: Vec<Span>,
107}
108
109impl Region {
110    /// The smallest rectangle holding every span; `None` when there are none.
111    pub fn bounds(&self) -> Option<Rect> {
112        let first = self.spans.first()?;
113        let (mut top, mut bottom) = (first.row, first.row);
114        let (mut left, mut right) = (first.columns.start, first.columns.end);
115        for span in &self.spans {
116            top = top.min(span.row);
117            bottom = bottom.max(span.row);
118            left = left.min(span.columns.start);
119            right = right.max(span.columns.end);
120        }
121        Some(Rect {
122            row: top,
123            column: left,
124            width: right - left,
125            height: bottom - top + 1,
126        })
127    }
128
129    /// Whether the spans fill [`Region::bounds`] exactly: one span per row,
130    /// all of one width, on consecutive rows.
131    pub fn is_rectangle(&self) -> bool {
132        let Some(bounds) = self.bounds() else {
133            return false;
134        };
135        self.spans.len() == bounds.height
136            && self.spans.iter().enumerate().all(|(index, span)| {
137                span.row == bounds.row + index
138                    && span.columns == (bounds.column..bounds.column + bounds.width)
139            })
140    }
141}
142
143/// A stable, lower-case name for a role, as HTML export and snapshots write
144/// it: `panel`, `table`, `table-header`, `table-cell`, `table-footer`,
145/// `rule`, `heading`, `code`, `link`, or an [`RegionRole::Other`] name.
146pub fn role_name(role: &RegionRole) -> String {
147    match role {
148        RegionRole::Panel => "panel".into(),
149        RegionRole::Table => "table".into(),
150        RegionRole::TableHeader { .. } => "table-header".into(),
151        RegionRole::TableCell { .. } => "table-cell".into(),
152        RegionRole::TableFooter { .. } => "table-footer".into(),
153        RegionRole::Rule => "rule".into(),
154        RegionRole::Heading { .. } => "heading".into(),
155        RegionRole::Code => "code".into(),
156        RegionRole::Link => "link".into(),
157        RegionRole::Other(name) => name.clone(),
158        _ => "region".into(),
159    }
160}
161
162/// `style` without its region tag, and the tag; `None` when it has none.
163pub(super) fn split_region(style: &Style) -> Option<(RegionId, Style)> {
164    let id = region_of(style)?;
165    let rest: Meta = style
166        .meta_ref()?
167        .iter()
168        .filter(|(key, _)| *key != REGION_META_KEY)
169        .map(|(key, value)| (key.to_string(), value.clone()))
170        .collect();
171    let stripped = if rest.is_empty() {
172        let bare = style.clear_meta_and_links();
173        match style.link() {
174            Some(link) => bare.with_link(link),
175            None => bare,
176        }
177    } else {
178        style.clone().with_meta(rest)
179    };
180    Some((id, stripped))
181}
182
183/// Render `renderable` with a [`RegionRecorder`] installed on a copy of
184/// `console`, and build its frame with regions. Without regions to report,
185/// the frame is what [`Frame::from_segments`] builds from a plain render.
186pub fn render_frame(
187    console: &Console,
188    options: &ConsoleOptions,
189    renderable: &dyn Renderable,
190) -> Frame {
191    let recorder = Arc::new(RegionRecorder::new());
192    let mut observed = console.clone();
193    observed.set_region_sink(Some(recorder.clone()));
194    let segments = renderable.rich_render(&observed, options);
195    Frame::from_segments(&segments).with_regions(&recorder)
196}
197
198impl Frame {
199    /// This frame with the regions `recorder` recorded, as spans of the cells
200    /// their tags cover. A region that drew nothing that is still in the
201    /// frame is left out, and its children move up to the nearest ancestor
202    /// that remains.
203    pub fn with_regions(mut self, recorder: &RegionRecorder) -> Frame {
204        let recorded = recorder.regions();
205        let index: HashMap<RegionId, usize> = recorded
206            .iter()
207            .enumerate()
208            .map(|(at, (id, _, _))| (*id, at))
209            .collect();
210        let parents: Vec<Option<usize>> = recorded
211            .iter()
212            .map(|(_, _, parent)| parent.and_then(|id| index.get(&id).copied()))
213            .collect();
214        let mut spans: Vec<Vec<Span>> = vec![Vec::new(); recorded.len()];
215        for row in 0..self.height() {
216            let mut column = 0;
217            for run_index in self.row_range(row) {
218                let width = self.runs[run_index].cells();
219                let tag = self.run_tag(run_index);
220                if width > 0 {
221                    let mut at = tag.and_then(|id| index.get(&id).copied());
222                    while let Some(region) = at {
223                        push_span(&mut spans[region], row, column..column + width);
224                        at = parents[region];
225                    }
226                }
227                column += width;
228            }
229        }
230        // Keep regions that cover something; renumber, re-parent.
231        let mut kept: Vec<Option<usize>> = vec![None; recorded.len()];
232        let mut regions: Vec<Region> = Vec::new();
233        for (at, ((_, info, _), spans)) in recorded.into_iter().zip(spans).enumerate() {
234            if spans.is_empty() {
235                continue;
236            }
237            let mut parent = parents[at];
238            while let Some(p) = parent {
239                if kept[p].is_some() {
240                    break;
241                }
242                parent = parents[p];
243            }
244            let parent = parent.and_then(|p| kept[p]);
245            let depth = parent.map_or(0, |p| regions[p].depth + 1);
246            kept[at] = Some(regions.len());
247            regions.push(Region {
248                role: info.role,
249                label: info.label,
250                link: info.link,
251                parent,
252                depth,
253                spans,
254            });
255        }
256        self.region_ids = kept
257            .iter()
258            .enumerate()
259            .filter_map(|(at, kept)| kept.map(|k| (RegionId(at as u64), k)))
260            .collect();
261        self.regions = regions;
262        self
263    }
264
265    /// The regions: those recorded with [`Frame::with_regions`], then one
266    /// [`RegionRole::Link`] region per stretch of runs sharing a link
267    /// target. A link region's parent is the innermost recorded region
268    /// holding its first cell.
269    pub fn regions(&self) -> Vec<Region> {
270        let mut regions = self.regions.clone();
271        let mut open: Option<usize> = None;
272        for row in 0..self.height() {
273            let mut column = 0;
274            for run_index in self.row_range(row) {
275                let run = self.runs[run_index];
276                let width = run.cells();
277                if width == 0 {
278                    continue;
279                }
280                let link = self.styles.get(run.style).and_then(Style::link);
281                match (link, open) {
282                    (Some(link), Some(at)) if regions[at].link.as_deref() == Some(link) => {
283                        push_span(&mut regions[at].spans, row, column..column + width);
284                    }
285                    (Some(link), _) => {
286                        let parent = self.innermost(run_index);
287                        let depth = parent.map_or(0, |p| regions[p].depth + 1);
288                        regions.push(Region {
289                            role: RegionRole::Link,
290                            label: None,
291                            link: Some(link.to_string()),
292                            parent,
293                            depth,
294                            spans: vec![Span {
295                                row,
296                                columns: column..column + width,
297                            }],
298                        });
299                        open = Some(regions.len() - 1);
300                    }
301                    (None, _) => open = None,
302                }
303                column += width;
304            }
305        }
306        regions
307    }
308
309    /// Whether the frame has recorded regions (links aside).
310    pub fn has_regions(&self) -> bool {
311        !self.regions.is_empty()
312    }
313
314    /// The recorded region that drew run `run_index`, as an index into
315    /// [`Frame::regions`].
316    pub(super) fn innermost(&self, run_index: usize) -> Option<usize> {
317        self.run_tag(run_index)
318            .and_then(|id| self.region_ids.get(&id).copied())
319    }
320}
321
322/// Add `columns` of `row` to `spans`, joining it to the last span when they
323/// touch.
324fn push_span(spans: &mut Vec<Span>, row: usize, columns: Range<usize>) {
325    match spans.last_mut() {
326        Some(last) if last.row == row && last.columns.end == columns.start => {
327            last.columns.end = columns.end;
328        }
329        _ => spans.push(Span { row, columns }),
330    }
331}