1use 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#[derive(Debug, Clone, Default)]
47pub struct SegmentGridState<'a> {
48 pub segment_tree: Option<(TaffyTree<()>, NodeId, HashMap<NodeId, NodeContents<'a>>)>,
52
53 pub horizontal_scroll_state: ScrollbarState,
55
56 pub vertical_scroll_state: ScrollbarState,
58
59 pub vertical_scroll: usize,
61
62 pub horizontal_scroll: usize,
64
65 pub max_horizontal_scroll: usize,
67
68 pub max_vertical_scroll: usize,
70}
71
72impl SegmentGridState<'_> {
73 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 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 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 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 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 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 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 Some(chunk_row_offset + row_offset.unwrap_or(0)),
471 segments,
472 segment_tree,
473 )?
474 }
475 LayoutChildType::Field(field_name) => {
476 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}