Skip to main content

vortex_tui/browse/ui/
segments.rs

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