1use 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#[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 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct Span {
79 pub row: usize,
80 pub columns: Range<usize>,
81}
82
83#[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#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct Region {
96 pub role: RegionRole,
97 pub label: Option<String>,
98 pub link: Option<String>,
99 pub parent: Option<usize>,
102 pub depth: usize,
104 pub spans: Vec<Span>,
107}
108
109impl Region {
110 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 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
143pub 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
162pub(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
183pub 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 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 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 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 pub fn has_regions(&self) -> bool {
311 !self.regions.is_empty()
312 }
313
314 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
322fn 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}