vortex_tui/browse/ui/
segments.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::sync::Arc;
5
6use humansize::DECIMAL;
7use ratatui::buffer::Buffer;
8use ratatui::layout::Rect;
9use ratatui::prelude::Alignment;
10use ratatui::prelude::Line;
11use ratatui::prelude::Margin;
12use ratatui::prelude::StatefulWidget;
13use ratatui::prelude::Widget;
14use ratatui::widgets::Block;
15use ratatui::widgets::Borders;
16use ratatui::widgets::Paragraph;
17use ratatui::widgets::Scrollbar;
18use ratatui::widgets::ScrollbarOrientation;
19use ratatui::widgets::ScrollbarState;
20use ratatui::widgets::Wrap;
21use taffy::AvailableSpace;
22use taffy::Dimension;
23use taffy::FlexDirection;
24use taffy::LengthPercentage;
25use taffy::NodeId;
26use taffy::PrintTree;
27use taffy::Size;
28use taffy::Style;
29use taffy::TaffyTree;
30use taffy::TraversePartialTree;
31use vortex::dtype::FieldName;
32use vortex::error::VortexExpect;
33use vortex::error::VortexResult;
34use vortex::error::vortex_err;
35use vortex::file::SegmentSpec;
36use vortex::layout::Layout;
37use vortex::layout::LayoutChildType;
38use vortex::utils::aliases::hash_map::HashMap;
39
40use crate::browse::app::AppState;
41
42/// State for the segment grid visualization.
43///
44/// This struct manages the layout tree and scroll state for displaying segments in a grid view.
45/// The segment tree is lazily computed on first render and cached for subsequent frames.
46#[derive(Debug, Clone, Default)]
47pub struct SegmentGridState<'a> {
48    /// The computed layout tree for the segment grid, or `None` if not yet computed.
49    ///
50    /// Contains the taffy layout tree, root node ID, and a map of node contents.
51    pub segment_tree: Option<(TaffyTree<()>, NodeId, HashMap<NodeId, NodeContents<'a>>)>,
52
53    /// State for the horizontal scrollbar widget.
54    pub horizontal_scroll_state: ScrollbarState,
55
56    /// State for the vertical scrollbar widget.
57    pub vertical_scroll_state: ScrollbarState,
58
59    /// Current vertical scroll position in pixels.
60    pub vertical_scroll: usize,
61
62    /// Current horizontal scroll position in pixels.
63    pub horizontal_scroll: usize,
64
65    /// Maximum horizontal scroll position.
66    pub max_horizontal_scroll: usize,
67
68    /// Maximum vertical scroll position.
69    pub max_vertical_scroll: usize,
70}
71
72impl SegmentGridState<'_> {
73    /// Scroll the viewport up by the given amount.
74    pub fn scroll_up(&mut self, amount: usize) {
75        self.vertical_scroll = self.vertical_scroll.saturating_sub(amount);
76        self.vertical_scroll_state = self.vertical_scroll_state.position(self.vertical_scroll);
77    }
78
79    /// Scroll the viewport down by the given amount.
80    pub fn scroll_down(&mut self, amount: usize) {
81        self.vertical_scroll = self
82            .vertical_scroll
83            .saturating_add(amount)
84            .min(self.max_vertical_scroll);
85        self.vertical_scroll_state = self.vertical_scroll_state.position(self.vertical_scroll);
86    }
87
88    /// Scroll the viewport left by the given amount.
89    pub fn scroll_left(&mut self, amount: usize) {
90        self.horizontal_scroll = self.horizontal_scroll.saturating_sub(amount);
91        self.horizontal_scroll_state = self
92            .horizontal_scroll_state
93            .position(self.horizontal_scroll);
94    }
95
96    /// Scroll the viewport right by the given amount.
97    pub fn scroll_right(&mut self, amount: usize) {
98        self.horizontal_scroll = self
99            .horizontal_scroll
100            .saturating_add(amount)
101            .min(self.max_horizontal_scroll);
102        self.horizontal_scroll_state = self
103            .horizontal_scroll_state
104            .position(self.horizontal_scroll);
105    }
106}
107
108#[derive(Debug, Clone)]
109pub struct NodeContents<'a> {
110    title: FieldName,
111    contents: Vec<Line<'a>>,
112}
113
114pub struct SegmentDisplay {
115    name: FieldName,
116    spec: SegmentSpec,
117    row_offset: u64,
118    row_count: u64,
119}
120
121#[expect(
122    clippy::cast_possible_truncation,
123    reason = "UI coordinates are small enough"
124)]
125pub fn segments_ui(app_state: &mut AppState, area: Rect, buf: &mut Buffer) {
126    if app_state.segment_grid_state.segment_tree.is_none() {
127        let segment_tree = collect_segment_tree(
128            app_state.vxf.footer().layout().as_ref(),
129            app_state.vxf.footer().segment_map(),
130        );
131        app_state.segment_grid_state.segment_tree = Some(
132            to_display_segment_tree(segment_tree)
133                .map_err(|e| vortex_err!("Fail to compute segment tree {e}"))
134                .vortex_expect("operation should succeed in TUI"),
135        );
136    }
137
138    let Some((tree, root_node, contents)) = &mut app_state.segment_grid_state.segment_tree else {
139        unreachable!("uninitialized state")
140    };
141
142    if app_state.frame_size != area.as_size() {
143        let viewport_size = Size {
144            width: AvailableSpace::Definite(area.width as f32),
145            height: AvailableSpace::Definite(area.height as f32),
146        };
147        tree.compute_layout(*root_node, viewport_size)
148            .map_err(|e| vortex_err!("Fail to compute layout {e}"))
149            .vortex_expect("operation should succeed in TUI");
150        app_state.frame_size = area.as_size();
151
152        let root_layout = tree.get_final_layout(*root_node);
153
154        app_state.segment_grid_state.max_horizontal_scroll = root_layout.scroll_width() as usize;
155        app_state.segment_grid_state.max_vertical_scroll = root_layout.scroll_height() as usize;
156
157        app_state.segment_grid_state.horizontal_scroll_state = app_state
158            .segment_grid_state
159            .horizontal_scroll_state
160            .content_length(root_layout.scroll_width() as usize)
161            .viewport_content_length(app_state.frame_size.width as usize)
162            .position(app_state.segment_grid_state.horizontal_scroll);
163        app_state.segment_grid_state.vertical_scroll_state = app_state
164            .segment_grid_state
165            .vertical_scroll_state
166            .content_length(root_layout.scroll_height() as usize)
167            .viewport_content_length(app_state.frame_size.height as usize)
168            .position(app_state.segment_grid_state.vertical_scroll);
169    }
170
171    render_tree(
172        tree,
173        *root_node,
174        contents,
175        (
176            app_state.segment_grid_state.horizontal_scroll,
177            app_state.segment_grid_state.vertical_scroll,
178        ),
179        area,
180        buf,
181    );
182
183    let horizontal_scroll = Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
184        .begin_symbol(Some("◄"))
185        .end_symbol(Some("►"));
186    horizontal_scroll.render(
187        area,
188        buf,
189        &mut app_state.segment_grid_state.horizontal_scroll_state,
190    );
191
192    let vertical_scroll = Scrollbar::new(ScrollbarOrientation::VerticalRight)
193        .begin_symbol(Some("▲"))
194        .end_symbol(Some("▼"));
195    vertical_scroll.render(
196        area.inner(Margin {
197            horizontal: 0,
198            vertical: 1,
199        }),
200        buf,
201        &mut app_state.segment_grid_state.vertical_scroll_state,
202    );
203}
204
205#[expect(
206    clippy::cast_possible_truncation,
207    reason = "UI coordinates are small enough"
208)]
209fn render_tree(
210    tree: &TaffyTree<()>,
211    node: NodeId,
212    contents: &HashMap<NodeId, NodeContents>,
213    viewport_top_left: (usize, usize),
214    bounding_box: Rect,
215    buf: &mut Buffer,
216) -> Option<Rect> {
217    let layout = tree.get_final_layout(node);
218
219    let object_x = layout.location.x as usize;
220    let object_y = layout.location.y as usize;
221
222    let x_viewport = object_x.saturating_sub(viewport_top_left.0);
223    let y_viewport = object_y.saturating_sub(viewport_top_left.1);
224
225    let block_contents = contents.get(&node);
226    if (viewport_top_left.0
227        > layout.size.width as usize + layout.scroll_width() as usize + object_x
228        || viewport_top_left.1
229            > layout.size.height as usize + layout.scroll_height() as usize + object_y)
230        && block_contents.is_some_and(|c| !c.contents.is_empty())
231    {
232        return None;
233    }
234
235    let r = bounding_box.intersection(Rect::new(
236        x_viewport as u16 + bounding_box.x,
237        y_viewport as u16 + bounding_box.y,
238        layout.size.width as u16,
239        layout.size.height as u16,
240    ));
241
242    if r.is_empty() {
243        return None;
244    }
245
246    let mut block_to_render = None;
247    if let Some(blk_content) = block_contents {
248        for p in r.positions() {
249            buf[p].reset()
250        }
251
252        let block = Block::new()
253            .title(blk_content.title.as_ref())
254            .borders(Borders::ALL);
255
256        if !blk_content.contents.is_empty() {
257            Paragraph::new(blk_content.contents.clone())
258                .block(block)
259                .alignment(Alignment::Left)
260                .wrap(Wrap { trim: true })
261                .render(r, buf);
262        } else {
263            block_to_render = Some(block);
264        }
265    }
266
267    let _child_area = tree
268        .child_ids(node)
269        .flat_map(|child_node_id| {
270            render_tree(
271                tree,
272                child_node_id,
273                contents,
274                (
275                    viewport_top_left.0.saturating_sub(object_x),
276                    viewport_top_left.1.saturating_sub(object_y),
277                ),
278                r,
279                buf,
280            )
281        })
282        .reduce(|a, b| a.union(b));
283
284    if let Some(block) = block_to_render {
285        block.render(r, buf);
286    }
287
288    Some(r)
289}
290
291fn to_display_segment_tree<'a>(
292    mut segment_tree: SegmentTree,
293) -> anyhow::Result<(TaffyTree<()>, NodeId, HashMap<NodeId, NodeContents<'a>>)> {
294    // Extra node for the parent node of the segment specs, and one parent node as the root
295    let mut tree = TaffyTree::with_capacity(
296        segment_tree
297            .segments
298            .iter()
299            .map(|(_, v)| v.len() + 1)
300            .sum::<usize>()
301            + 1,
302    );
303
304    let mut node_contents: HashMap<NodeId, NodeContents> = HashMap::new();
305
306    let children = segment_tree
307        .segment_ordering
308        .into_iter()
309        .map(|name| {
310            let chunks = segment_tree
311                .segments
312                .get_mut(&name)
313                .vortex_expect("Must have segment for name");
314            chunks.sort_by(|a, b| a.spec.offset.cmp(&b.spec.offset));
315
316            // Build leaf nodes for each segment chunk.
317            let mut leaves = Vec::with_capacity(chunks.len());
318            let mut current_offset = 0u64;
319            for segment in chunks.iter() {
320                let node_id = tree.new_leaf(Style {
321                    min_size: Size {
322                        width: Dimension::percent(1.0),
323                        height: Dimension::length(7.0),
324                    },
325                    size: Size {
326                        width: Dimension::percent(1.0),
327                        height: Dimension::length(15.0),
328                    },
329                    ..Default::default()
330                })?;
331
332                node_contents.insert(
333                    node_id,
334                    NodeContents {
335                        title: segment.name.clone(),
336                        contents: vec![
337                            Line::raw(format!(
338                                "Rows: {}..{} ({})",
339                                segment.row_offset,
340                                segment.row_offset + segment.row_count,
341                                segment.row_count
342                            )),
343                            Line::raw(format!(
344                                "Bytes: {}..{} ({})",
345                                segment.spec.offset,
346                                segment.spec.offset + segment.spec.length as u64,
347                                humansize::format_size(segment.spec.length, DECIMAL),
348                            )),
349                            Line::raw(format!("Align: {}", segment.spec.alignment)),
350                            Line::raw(format!(
351                                "Byte gap: {}",
352                                if current_offset == 0 {
353                                    0
354                                } else {
355                                    segment.spec.offset - current_offset
356                                }
357                            )),
358                        ],
359                    },
360                );
361
362                current_offset = segment.spec.length as u64 + segment.spec.offset;
363                leaves.push(node_id);
364            }
365
366            let node_id = tree.new_with_children(
367                Style {
368                    min_size: Size {
369                        width: Dimension::length(40.0),
370                        height: Dimension::percent(1.0),
371                    },
372                    padding: taffy::Rect {
373                        left: LengthPercentage::length(1.0),
374                        right: LengthPercentage::length(1.0),
375                        top: LengthPercentage::length(1.0),
376                        bottom: LengthPercentage::length(1.0),
377                    },
378                    flex_direction: FlexDirection::Column,
379                    ..Default::default()
380                },
381                &leaves,
382            )?;
383            node_contents.insert(
384                node_id,
385                NodeContents {
386                    title: name,
387                    contents: Vec::new(),
388                },
389            );
390            Ok(node_id)
391        })
392        .collect::<anyhow::Result<Vec<_>>>()?;
393
394    let root = tree.new_with_children(
395        Style {
396            size: Size {
397                width: Dimension::percent(1.0),
398                height: Dimension::percent(1.0),
399            },
400            flex_direction: FlexDirection::Row,
401            ..Default::default()
402        },
403        &children,
404    )?;
405    Ok((tree, root, node_contents))
406}
407
408fn collect_segment_tree(root_layout: &dyn Layout, segments: &Arc<[SegmentSpec]>) -> SegmentTree {
409    let mut tree = SegmentTree {
410        segments: HashMap::new(),
411        segment_ordering: Vec::new(),
412    };
413    segments_by_name_impl(root_layout, None, None, Some(0), segments, &mut tree)
414        .vortex_expect("operation should succeed in TUI");
415
416    tree
417}
418
419struct SegmentTree {
420    segments: HashMap<FieldName, Vec<SegmentDisplay>>,
421    segment_ordering: Vec<FieldName>,
422}
423
424fn segments_by_name_impl(
425    root: &dyn Layout,
426    group_name: Option<FieldName>,
427    name: Option<FieldName>,
428    row_offset: Option<u64>,
429    segments: &Arc<[SegmentSpec]>,
430    segment_tree: &mut SegmentTree,
431) -> VortexResult<()> {
432    // Recurse into children
433    for (child, child_type) in root.children()?.into_iter().zip(root.child_types()) {
434        match child_type {
435            LayoutChildType::Transparent(sub_name) => segments_by_name_impl(
436                child.as_ref(),
437                group_name.clone(),
438                Some(
439                    name.as_ref()
440                        .map(|n| format!("{n}.{sub_name}").into())
441                        .unwrap_or_else(|| sub_name.into()),
442                ),
443                row_offset,
444                segments,
445                segment_tree,
446            )?,
447            LayoutChildType::Auxiliary(aux_name) => segments_by_name_impl(
448                child.as_ref(),
449                group_name.clone(),
450                Some(
451                    name.as_ref()
452                        .map(|n| format!("{n}.{aux_name}").into())
453                        .unwrap_or_else(|| aux_name.into()),
454                ),
455                Some(0),
456                segments,
457                segment_tree,
458            )?,
459            LayoutChildType::Chunk((idx, chunk_row_offset)) => {
460                segments_by_name_impl(
461                    child.as_ref(),
462                    group_name.clone(),
463                    Some(
464                        name.as_ref()
465                            .map(|n| format!("{n}.[{idx}]"))
466                            .unwrap_or_else(|| format!("[{idx}]"))
467                            .into(),
468                    ),
469                    // Compute absolute row offset.
470                    Some(chunk_row_offset + row_offset.unwrap_or(0)),
471                    segments,
472                    segment_tree,
473                )?
474            }
475            LayoutChildType::Field(field_name) => {
476                // Step into a new group name
477                let group_name = group_name
478                    .as_ref()
479                    .map(|n| format!("{n}.{field_name}").into())
480                    .unwrap_or_else(|| field_name);
481                segment_tree.segment_ordering.push(group_name.clone());
482
483                segments_by_name_impl(
484                    child.as_ref(),
485                    Some(group_name),
486                    None,
487                    row_offset,
488                    segments,
489                    segment_tree,
490                )?
491            }
492        }
493    }
494
495    let current_segments = segment_tree
496        .segments
497        .entry(group_name.unwrap_or_else(|| FieldName::from("root")))
498        .or_default();
499
500    for segment_id in root.segment_ids() {
501        let segment_spec = segments[*segment_id as usize].clone();
502        current_segments.push(SegmentDisplay {
503            name: name.clone().unwrap_or_else(|| "<unnamed>".into()),
504            spec: segment_spec,
505            row_count: root.row_count(),
506            row_offset: row_offset.unwrap_or(0),
507        })
508    }
509
510    Ok(())
511}