1use 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#[derive(Debug, Clone, Default)]
43pub struct SegmentGridState {
44 pub segment_tree: Option<(TaffyTree<()>, NodeId, HashMap<NodeId, NodeContents>)>,
48
49 pub horizontal_scroll_state: ScrollbarState,
51
52 pub vertical_scroll_state: ScrollbarState,
54
55 pub vertical_scroll: usize,
57
58 pub horizontal_scroll: usize,
60
61 pub max_horizontal_scroll: usize,
63
64 pub max_vertical_scroll: usize,
66}
67
68impl SegmentGridState {
69 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 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 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 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 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 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}