Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/table/mod.rs
1use std::cell::RefCell;
2use std::ops::AddAssign;
3use std::rc::Rc;
4use std::sync::Arc;
5 
6use derive_more::AddAssign as DeriveAddAssign;
7use ordered_float::OrderedFloat;
8use pathfinder_color::ColorU;
9use pathfinder_geometry::rect::RectF;
10use pathfinder_geometry::vector::{vec2f, Vector2F};
11 
12use crate::event::DispatchedEvent;
13use crate::linear_index::{self, SumTree};
14use crate::scene::ClipBounds;
15use crate::text::word_boundaries::WordBoundariesPolicy;
16use crate::text::{IsRect, SelectionDirection, SelectionType};
17use crate::units::{IntoPixels, Pixels};
18use crate::{
19 AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
20 SizeConstraint,
21};
22 
23use super::{
24 Point, ScrollData, ScrollableElement, SelectableElement, Selection, SelectionFragment,
25 SmartSelectFn,
26};
27 
28// ============================================================================
29// Constants
30// ============================================================================
31 
32/// Default estimated row height used when no rows have been measured yet.
33const DEFAULT_ROW_HEIGHT_ESTIMATE: f32 = 32.0;
34 
35/// Callback type for rendering a single row on demand.
36/// Takes the row index and returns a vector of cell elements for that row.
37pub type TableRowRenderFn = dyn Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static;
38 
39// ============================================================================
40// SumTree Types for Row Virtualization
41// ============================================================================
42 
43/// A single row item in the table's sum tree.
44/// Stores the measured height of a row, or None if not yet measured.
45#[derive(Clone, Debug)]
46struct TableRowItem {
47 height: Option<Pixels>,
48}
49 
50/// Summary for the sum tree, tracking total height and measurement state.
51#[derive(Debug, Clone, Default)]
52struct RowLayoutSummary {
53 /// Total height of all items in the sum tree (only measured items contribute).
54 height: Pixels,
55 /// Total number of items (rows) in the sum tree.
56 count: usize,
57 /// Number of items that have been measured (have Some(height)).
58 measured_count: usize,
59}
60 
61impl linear_index::Item for TableRowItem {
62 type Summary = RowLayoutSummary;
63 
64 fn summary(&self) -> Self::Summary {
65 RowLayoutSummary {
66 height: self.height.unwrap_or(Pixels::zero()),
67 count: 1,
68 measured_count: if self.height.is_some() { 1 } else { 0 },
69 }
70 }
71}
72 
73impl AddAssign<&RowLayoutSummary> for RowLayoutSummary {
74 fn add_assign(&mut self, rhs: &RowLayoutSummary) {
75 self.height += rhs.height;
76 self.count += rhs.count;
77 self.measured_count += rhs.measured_count;
78 }
79}
80 
81/// Height dimension for sum tree seeking.
82#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
83struct Height(OrderedFloat<Pixels>);
84 
85impl From<Pixels> for Height {
86 fn from(value: Pixels) -> Self {
87 Self(OrderedFloat(value))
88 }
89}
90 
91impl<'a> linear_index::Dimension<'a, RowLayoutSummary> for Height {
92 fn add_summary(&mut self, summary: &'a RowLayoutSummary) {
93 self.0 .0 += summary.height;
94 }
95}
96 
97/// Count dimension for sum tree seeking by row index.
98#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, DeriveAddAssign)]
99struct RowCount(usize);
100 
101impl<'a> linear_index::Dimension<'a, RowLayoutSummary> for RowCount {
102 fn add_summary(&mut self, summary: &'a RowLayoutSummary) {
103 self.0 += summary.count;
104 }
105}
106 
107/// Scroll position in the table, similar to ViewportedList's ScrollOffset.
108#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
109struct RowScrollOffset {
110 /// The row that is at the top of the viewport.
111 row_index: RowCount,
112 /// Number of pixels offset from the start of that row.
113 offset_from_start: Pixels,
114}
115 
116// ============================================================================
117// Public Table Types
118// ============================================================================
119 
120/// Column width specification for table columns.
121#[derive(Debug, Clone, Copy)]
122pub enum TableColumnWidth {
123 /// Fixed pixel width.
124 Fixed(f32),
125 /// Proportional share of remaining space after fixed columns are allocated.
126 /// Default is Flex(1.0).
127 Flex(f32),
128 /// Fraction of total table width (0.0 to 1.0).
129 Fraction(f32),
130 /// Width determined by measuring the intrinsic width of cell content.
131 /// The column will be sized to fit the widest cell in that column.
132 Intrinsic,
133}
134 
135impl Default for TableColumnWidth {
136 fn default() -> Self {
137 Self::Flex(1.0)
138 }
139}
140 
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
142pub enum TableVerticalSizing {
143 #[default]
144 Viewported,
145 ExpandToContent,
146}
147 
148/// Header cell definition that includes content and column width.
149pub struct TableHeader {
150 pub content: Box<dyn Element>,
151 pub width: TableColumnWidth,
152}
153 
154impl TableHeader {
155 pub fn new(content: Box<dyn Element>) -> Self {
156 Self {
157 content,
158 width: TableColumnWidth::default(),
159 }
160 }
161 
162 pub fn with_width(mut self, width: TableColumnWidth) -> Self {
163 self.width = width;
164 self
165 }
166}
167 
168/// Background color configuration for table rows.
169#[derive(Debug, Clone)]
170pub struct RowBackground {
171 /// Background color for even-indexed rows (0, 2, 4, ...).
172 pub primary: ColorU,
173 /// If set, odd-indexed rows use this background color instead of `primary`.
174 pub alternating: Option<ColorU>,
175}
176 
177impl Default for RowBackground {
178 fn default() -> Self {
179 Self {
180 primary: ColorU::white(),
181 alternating: None,
182 }
183 }
184}
185 
186impl RowBackground {
187 pub fn uniform(color: ColorU) -> Self {
188 Self {
189 primary: color,
190 alternating: None,
191 }
192 }
193 
194 pub fn striped(even: ColorU, odd: ColorU) -> Self {
195 Self {
196 primary: even,
197 alternating: Some(odd),
198 }
199 }
200 
201 pub fn color_for_row(&self, row_index: usize) -> ColorU {
202 if self.alternating.is_some() && row_index % 2 == 1 {
203 self.alternating.unwrap_or(self.primary)
204 } else {
205 self.primary
206 }
207 }
208}
209 
210/// Configuration for table styling.
211#[derive(Debug, Clone)]
212pub struct TableConfig {
213 /// Width of the outer table border and column dividers in pixels.
214 pub border_width: f32,
215 /// Color for the outer table border and column dividers.
216 pub border_color: ColorU,
217 /// Whether to draw the outer table border.
218 pub outer_border: bool,
219 /// Whether to draw vertical dividers between columns.
220 pub column_dividers: bool,
221 /// Whether to draw horizontal dividers between rows.
222 pub row_dividers: bool,
223 /// Padding applied uniformly to all sides of each cell (top, right, bottom, left).
224 pub cell_padding: f32,
225 /// Background color for the header row.
226 pub header_background: ColorU,
227 /// Background colors for data rows.
228 pub row_background: RowBackground,
229 /// When true, the header row stays fixed at the top of the viewport while the body
230 /// scrolls. The header is painted at a fixed position relative to the viewport.
231 pub fixed_header: bool,
232 /// Controls whether the table behaves as an internally viewported widget or expands to its
233 /// full content height and relies on the parent to handle vertical scrolling.
234 pub vertical_sizing: TableVerticalSizing,
235 /// When true, intrinsic-width columns are measured against body cells in addition to headers.
236 pub measure_body_cells_for_intrinsic_widths: bool,
237}
238 
239/// Note: In production code, prefer creating configs using theme colors rather than Default.
240/// This default implementation uses hardcoded colors and is primarily intended for tests and examples.
241impl Default for TableConfig {
242 fn default() -> Self {
243 let border_light_gray = ColorU::new(208, 215, 222, 255);
244 let header_background_light_gray = ColorU::new(246, 248, 250, 255);
245 
246 Self {
247 border_width: 1.0,
248 border_color: border_light_gray,
249 outer_border: true,
250 column_dividers: true,
251 row_dividers: false,
252 cell_padding: 8.0,
253 header_background: header_background_light_gray,
254 row_background: RowBackground::default(),
255 fixed_header: false,
256 vertical_sizing: TableVerticalSizing::Viewported,
257 measure_body_cells_for_intrinsic_widths: false,
258 }
259 }
260}
261 
262/// Internal state for table layout computations using sum tree for row virtualization.
263///
264/// This state persists across renders to maintain measured heights and scroll position.
265pub struct TableState {
266 // Row dimension - uses sum tree for efficient virtualization
267 /// Sum tree storing row heights. Only measured rows contribute to height calculations.
268 rows: SumTree<TableRowItem>,
269 /// The last known measured row where every row up to this index has been measured.
270 /// Can differ from measured_count in tree if a row in the middle was invalidated.
271 last_measured_row_index: usize,
272 /// Current scroll position in the table.
273 scroll_top: RowScrollOffset,
274 /// Height of the viewport, set during layout.
275 viewport_height: Pixels,
276 /// The row index of the first visible row (for painting and selection).
277 /// Updated during layout to track which rows are in self.children.
278 visible_start_row_idx: usize,
279 
280 // Column dimension - stays as flat vectors since columns are typically small and always visible
281 column_widths: Vec<f32>,
282 
283 // Intrinsic column width cache
284 /// Cached intrinsic widths for columns. Some(width) = measured, None = needs measurement.
285 intrinsic_column_widths: Vec<Option<f32>>,
286 
287 // Header height (separate from row sum tree)
288 header_height: Pixels,
289 
290 /// The actual rendered height of the table content (header + visible rows).
291 /// Used in paint() to draw borders at the correct height when content < viewport.
292 rendered_height: Pixels,
293 
294 /// Callback for rendering rows on demand. Rows are generated lazily during layout.
295 row_render_fn: Arc<TableRowRenderFn>,
296 /// Total number of rows.
297 row_count: usize,
298}
299 
300impl TableState {
301 fn new<F>(row_count: usize, row_render_fn: F) -> Self
302 where
303 F: Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static,
304 {
305 Self {
306 rows: SumTree::new(),
307 last_measured_row_index: 0,
308 scroll_top: RowScrollOffset::default(),
309 viewport_height: Pixels::zero(),
310 visible_start_row_idx: 0,
311 column_widths: Vec::new(),
312 intrinsic_column_widths: Vec::new(),
313 header_height: Pixels::zero(),
314 rendered_height: Pixels::zero(),
315 row_render_fn: Arc::new(row_render_fn),
316 row_count,
317 }
318 }
319}
320 
321impl TableState {
322 /// Get the absolute pixel position of the current scroll top (including header).
323 fn scroll_top_pixels(&self) -> Pixels {
324 let mut cursor = self.rows.cursor::<RowCount, Height>();
325 cursor.seek(&self.scroll_top.row_index, linear_index::SeekBias::Right);
326 let row_start_height = cursor.start().0 .0;
327 self.header_height + row_start_height + self.scroll_top.offset_from_start
328 }
329 
330 /// Returns the approximate total height of the table (header + all rows).
331 /// If all rows are measured, returns exact height. Otherwise estimates unmeasured rows.
332 fn approximate_height(&self) -> Pixels {
333 let summary = self.rows.summary();
334 
335 // If all rows are measured, return exact height
336 if summary.count == summary.measured_count {
337 return self.header_height + summary.height;
338 }
339 
340 // Otherwise, estimate total height based on average
341 if summary.measured_count == 0 {
342 return self.header_height;
343 }
344 
345 let avg_height = summary.height.as_f32() / summary.measured_count as f32;
346 let estimated_total = avg_height * summary.count as f32;
347 self.header_height + estimated_total.into_pixels()
348 }
349 
350 /// Returns the average height of measured rows.
351 fn average_height_per_measured_row(&self) -> Pixels {
352 let summary = self.rows.summary();
353 if summary.measured_count == 0 {
354 return Pixels::new(DEFAULT_ROW_HEIGHT_ESTIMATE);
355 }
356 (summary.height.as_f32() / summary.measured_count as f32).into_pixels()
357 }
358 
359 /// Convert an absolute pixel position (from top of table, including header) to a scroll offset.
360 fn absolute_pixels_to_scroll_offset(&self, absolute_pixels: Pixels) -> RowScrollOffset {
361 // Subtract header height to get position in row content
362 let pixels_in_rows = (absolute_pixels - self.header_height).max(Pixels::zero());
363 
364 let summary = self.rows.summary();
365 if summary.count == 0 {
366 return RowScrollOffset::default();
367 }
368 
369 // If scrolling beyond measured region, use average to estimate
370 let avg_height = self.average_height_per_measured_row();
371 let approximate_row = (pixels_in_rows.as_f32() / avg_height.as_f32()).floor() as usize;
372 let approximate_row = approximate_row.min(summary.count);
373 
374 // If the target is within the measured region, seek to exact position
375 if approximate_row <= self.last_measured_row_index {
376 let mut cursor = self.rows.cursor::<Height, RowCount>();
377 cursor.seek(&Height::from(pixels_in_rows), linear_index::SeekBias::Right);
378 let row_index = *cursor.start();
379 
380 let mut height_cursor = self.rows.cursor::<RowCount, Height>();
381 height_cursor.seek(&row_index, linear_index::SeekBias::Right);
382 let row_start_height = height_cursor.start().0 .0;
383 
384 let offset = pixels_in_rows - row_start_height;
385 
386 return RowScrollOffset {
387 row_index,
388 offset_from_start: offset,
389 };
390 }
391 
392 // Beyond measured region, use estimate
393 let approximate_offset =
394 (pixels_in_rows.as_f32() - approximate_row as f32 * avg_height.as_f32()).into_pixels();
395 RowScrollOffset {
396 row_index: RowCount(approximate_row),
397 offset_from_start: approximate_offset,
398 }
399 }
400 
401 /// Invalidate the height of a specific row.
402 fn invalidate_row_height(&mut self, row_idx: usize) {
403 let summary = self.rows.summary();
404 if row_idx >= summary.count {
405 return;
406 }
407 
408 let (new_tree, last_measured) = {
409 let mut cursor = self.rows.cursor::<RowCount, ()>();
410 let mut new_items = cursor.slice(&RowCount(row_idx), linear_index::SeekBias::Right);
411 let last_measured = new_items.summary().measured_count.saturating_sub(1);
412 
413 new_items.push(TableRowItem { height: None });
414 cursor.next();
415 new_items.push_tree(cursor.suffix());
416 (new_items, last_measured)
417 };
418 
419 self.rows = new_tree;
420 self.last_measured_row_index = self.last_measured_row_index.min(last_measured);
421 }
422 
423 /// Get the maximum scroll offset (to prevent scrolling beyond content).
424 fn max_scroll_offset(&self) -> RowScrollOffset {
425 let max_scroll_pixels =
426 (self.approximate_height() - self.viewport_height).max(Pixels::zero());
427 self.absolute_pixels_to_scroll_offset(max_scroll_pixels)
428 }
429}
430 
431/// Handle for shared table state across renders.
432/// This follows the same pattern as MouseStateHandle - the handle should be
433/// created once by the containing view and passed into the table on each render.
434#[derive(Clone)]
435pub struct TableStateHandle {
436 inner: Rc<RefCell<TableState>>,
437}
438 
439impl TableStateHandle {
440 /// Create a new table state handle with the given row count and render function.
441 /// The render function is called lazily during layout to generate row elements.
442 pub fn new<F>(row_count: usize, row_render_fn: F) -> Self
443 where
444 F: Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static,
445 {
446 Self {
447 inner: Rc::new(RefCell::new(TableState::new(row_count, row_render_fn))),
448 }
449 }
450 
451 pub fn column_widths(&self) -> Vec<f32> {
452 self.inner.borrow().column_widths.clone()
453 }
454 
455 /// Invalidate the height of a specific row, forcing it to be re-measured on next layout.
456 pub fn invalidate_row_height(&self, row_idx: usize) {
457 let mut state = self.inner.borrow_mut();
458 state.invalidate_row_height(row_idx);
459 }
460 
461 /// Invalidate the cached intrinsic width for a specific column.
462 ///
463 /// Call this when the content of cells in an `Intrinsic` width column changes
464 /// and you want the column to resize to fit the new content. Without calling
465 /// this, intrinsic column widths are cached and won't update automatically.
466 pub fn invalidate_intrinsic_width(&self, col_idx: usize) {
467 let mut state = self.inner.borrow_mut();
468 if col_idx < state.intrinsic_column_widths.len() {
469 state.intrinsic_column_widths[col_idx] = None;
470 }
471 }
472 
473 /// Invalidate all intrinsic column widths.
474 ///
475 /// Call this when content changes across multiple `Intrinsic` width columns
476 /// and you want all columns to resize to fit their new content.
477 pub fn invalidate_all_intrinsic_widths(&self) {
478 let mut state = self.inner.borrow_mut();
479 for width in &mut state.intrinsic_column_widths {
480 *width = None;
481 }
482 }
483 
484 /// Scroll to a specific row with an optional offset.
485 pub fn scroll_to_row(&self, row_idx: usize, offset_from_start: Option<Pixels>) {
486 let mut state = self.inner.borrow_mut();
487 state.scroll_top = RowScrollOffset {
488 row_index: RowCount(row_idx),
489 offset_from_start: offset_from_start.unwrap_or(Pixels::zero()),
490 };
491 }
492 
493 /// Update the total number of rows.
494 /// Call this when the data source changes size.
495 pub fn set_row_count(&self, count: usize) {
496 let mut state = self.inner.borrow_mut();
497 state.row_count = count;
498 }
499 
500 /// Update the row render function.
501 /// Call this when the render logic needs to change (e.g., data source changes).
502 pub fn set_row_render_fn<F>(&self, f: F)
503 where
504 F: Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static,
505 {
506 let mut state = self.inner.borrow_mut();
507 state.row_render_fn = Arc::new(f);
508 }
509 
510 /// Get the render function.
511 pub(crate) fn row_render_fn(&self) -> Arc<TableRowRenderFn> {
512 self.inner.borrow().row_render_fn.clone()
513 }
514 
515 /// Get the row count for on-demand rendering.
516 pub(crate) fn row_count(&self) -> usize {
517 self.inner.borrow().row_count
518 }
519}
520 
521impl Default for TableStateHandle {
522 fn default() -> Self {
523 Self::new(0, |_, _| vec![])
524 }
525}
526 
527/// A table element that renders a header row followed by data rows.
528///
529/// ## Layout Algorithm
530/// 1. **Intrinsic width measurement**: For columns with `TableColumnWidth::Intrinsic`,
531/// measure each cell with unconstrained width to find the widest content.
532/// 2. **Column width allocation**: Allocate widths based on `TableColumnWidth` specs:
533/// - `Fixed(px)`: exact pixel width
534/// - `Fraction(f)`: fraction of total available width
535/// - `Intrinsic`: measured width (scaled down if total exceeds available)
536/// - `Flex(f)`: proportional share of remaining space after fixed/fraction/intrinsic
537/// 3. **Row layout**: Layout each cell with its column's computed width, track max height per row.
538/// 4. **Position computation**: Compute cumulative column left positions and row top positions.
539///
540/// ## Virtualization
541/// Uses a sumtree to track row heights and efficiently viewport large tables.
542/// Only visible rows are laid out and painted.
543pub struct Table {
544 state: TableStateHandle,
545 headers: Vec<TableHeader>,
546 config: TableConfig,
547 /// Width to use when the table has unconstrained width and no intrinsic content to measure.
548 unconstrained_width: f32,
549 /// Viewport height to use when height constraint is not finite.
550 /// Used for virtualization and clipping calculations.
551 unconstrained_height: f32,
552 /// Computed during `layout()`. None before layout is called.
553 size: Option<Vector2F>,
554 /// Computed during `paint()`. None before paint is called.
555 origin: Option<Point>,
556 /// Stores only the visible row elements for the current frame after layout.
557 /// These are the rows that will be painted.
558 children: Vec<Vec<Box<dyn Element>>>,
559 /// Heights of the children rows (parallel array to children).
560 /// Stored to avoid sumtree queries during paint.
561 children_heights: Vec<Pixels>,
562}
563 
564impl Table {
565 pub fn new(
566 state: TableStateHandle,
567 unconstrained_width: f32,
568 unconstrained_height: f32,
569 ) -> Self {
570 Self {
571 state,
572 headers: Vec::new(),
573 config: TableConfig::default(),
574 unconstrained_width,
575 unconstrained_height,
576 size: None,
577 origin: None,
578 children: Vec::new(),
579 children_heights: Vec::new(),
580 }
581 }
582 
583 pub fn with_headers(mut self, headers: Vec<TableHeader>) -> Self {
584 self.headers = headers;
585 self
586 }
587 
588 /// Update the total number of rows.
589 /// Useful when the row count differs from what was set at TableStateHandle creation.
590 pub fn with_row_count(self, count: usize) -> Self {
591 self.state.set_row_count(count);
592 self
593 }
594 
595 /// Update the row render function.
596 /// Useful when the render function needs to capture different data than at creation.
597 pub fn with_row_render_fn<F>(self, f: F) -> Self
598 where
599 F: Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static,
600 {
601 self.state.set_row_render_fn(f);
602 self
603 }
604 
605 pub fn with_config(mut self, config: TableConfig) -> Self {
606 self.config = config;
607 self
608 }
609 
610 /// Get total row count (for scroll calculations).
611 pub fn total_row_count(&self) -> usize {
612 self.state.row_count()
613 }
614 
615 fn column_count(&self) -> usize {
616 self.headers.len()
617 }
618 
619 /// Compute column widths based on TableColumnWidth specifications.
620 /// `intrinsic_widths` should contain pre-measured widths for Intrinsic columns.
621 /// If total intrinsic width exceeds available width, intrinsic columns are scaled down.
622 fn compute_column_widths(&self, available_width: f32, intrinsic_widths: &[f32]) -> Vec<f32> {
623 let column_count = self.column_count();
624 if column_count == 0 {
625 return Vec::new();
626 }
627 
628 let mut widths = vec![0.0f32; column_count];
629 let mut fixed_width = 0.0f32;
630 let mut total_intrinsic = 0.0f32;
631 let mut total_flex = 0.0f32;
632 
633 for (i, header) in self.headers.iter().enumerate() {
634 match header.width {
635 TableColumnWidth::Fixed(w) => {
636 widths[i] = w;
637 fixed_width += w;
638 }
639 TableColumnWidth::Fraction(f) => {
640 let w = available_width * f;
641 widths[i] = w;
642 fixed_width += w;
643 }
644 TableColumnWidth::Flex(flex) => {
645 total_flex += flex;
646 }
647 TableColumnWidth::Intrinsic => {
648 let w = intrinsic_widths.get(i).copied().unwrap_or(0.0);
649 total_intrinsic += w;
650 }
651 }
652 }
653 
654 let available_for_intrinsic = (available_width - fixed_width).max(0.0);
655 let intrinsic_scale = if total_intrinsic > available_for_intrinsic && total_intrinsic > 0.0
656 {
657 available_for_intrinsic / total_intrinsic
658 } else {
659 1.0
660 };
661 
662 let mut remaining_width = available_width - fixed_width;
663 for (i, header) in self.headers.iter().enumerate() {
664 if let TableColumnWidth::Intrinsic = header.width {
665 let w = intrinsic_widths.get(i).copied().unwrap_or(0.0) * intrinsic_scale;
666 widths[i] = w;
667 remaining_width -= w;
668 }
669 }
670 
671 remaining_width = remaining_width.max(0.0);
672 if total_flex > 0.0 {
673 for (i, header) in self.headers.iter().enumerate() {
674 if let TableColumnWidth::Flex(flex) = header.width {
675 widths[i] = (remaining_width * flex) / total_flex;
676 }
677 }
678 }
679 
680 widths
681 }
682 
683 /// Measure the intrinsic width contribution from a header cell.
684 fn measure_header_intrinsic_width(
685 &mut self,
686 col_idx: usize,
687 ctx: &mut LayoutContext,
688 app: &AppContext,
689 ) -> f32 {
690 let padding = self.config.cell_padding * 2.0;
691 let mut max_width = 0.0f32;
692 
693 if col_idx < self.headers.len() {
694 let header_content = std::mem::replace(
695 &mut self.headers[col_idx].content,
696 Box::new(super::Empty::new()),
697 );
698 let mut header_box = header_content;
699 let unconstrained =
700 SizeConstraint::new(vec2f(0.0, 0.0), vec2f(f32::INFINITY, f32::INFINITY));
701 let size = header_box.layout(unconstrained, ctx, app);
702 max_width = max_width.max(size.x() + padding);
703 self.headers[col_idx].content = header_box;
704 }
705 
706 max_width
707 }
708 
709 /// Measure intrinsic width for a single column by laying out the header cell and, when
710 /// configured, every body cell in the column.
711 fn measure_column_intrinsic_width(
712 &mut self,
713 col_idx: usize,
714 ctx: &mut LayoutContext,
715 app: &AppContext,
716 ) -> f32 {
717 let padding = self.config.cell_padding * 2.0;
718 let mut max_width = self.measure_header_intrinsic_width(col_idx, ctx, app);
719 
720 if self.config.measure_body_cells_for_intrinsic_widths {
721 let render_fn = self.state.row_render_fn();
722 let row_count = self.state.row_count();
723 for row_idx in 0..row_count {
724 let mut row_elements = render_fn(row_idx, app);
725 let Some(cell) = row_elements.get_mut(col_idx) else {
726 continue;
727 };
728 let unconstrained =
729 SizeConstraint::new(vec2f(0.0, 0.0), vec2f(f32::INFINITY, f32::INFINITY));
730 let size = cell.layout(unconstrained, ctx, app);
731 max_width = max_width.max(size.x() + padding);
732 }
733 }
734 
735 max_width
736 }
737 
738 /// Prepare intrinsic widths for all columns that have `Intrinsic` sizing.
739 /// Returns the current widths plus the intrinsic columns whose widths should be cached after
740 /// the caller finishes any additional measurement work.
741 fn prepare_intrinsic_widths(
742 &mut self,
743 header_only_for_uncached_columns: bool,
744 ctx: &mut LayoutContext,
745 app: &AppContext,
746 ) -> (Vec<f32>, Vec<usize>) {
747 let column_count = self.column_count();
748 let mut intrinsic_widths = vec![0.0f32; column_count];
749 
750 let intrinsic_indices: Vec<usize> = self
751 .headers
752 .iter()
753 .enumerate()
754 .filter_map(|(i, header)| {
755 if matches!(header.width, TableColumnWidth::Intrinsic) {
756 Some(i)
757 } else {
758 None
759 }
760 })
761 .collect();
762 
763 if intrinsic_indices.is_empty() {
764 return (intrinsic_widths, Vec::new());
765 }
766 
767 let mut state = self.state.inner.borrow_mut();
768 if state.intrinsic_column_widths.len() != column_count {
769 state.intrinsic_column_widths = vec![None; column_count];
770 }
771 
772 let mut uncached_intrinsic_indices = Vec::new();
773 for &idx in &intrinsic_indices {
774 if let Some(cached_width) = state.intrinsic_column_widths[idx] {
775 intrinsic_widths[idx] = cached_width;
776 } else {
777 uncached_intrinsic_indices.push(idx);
778 }
779 }
780 drop(state);
781 
782 for &idx in &uncached_intrinsic_indices {
783 intrinsic_widths[idx] = if header_only_for_uncached_columns {
784 self.measure_header_intrinsic_width(idx, ctx, app)
785 } else {
786 self.measure_column_intrinsic_width(idx, ctx, app)
787 };
788 }
789 
790 (intrinsic_widths, uncached_intrinsic_indices)
791 }
792 
793 fn cache_intrinsic_widths(&self, intrinsic_indices: &[usize], intrinsic_widths: &[f32]) {
794 if intrinsic_indices.is_empty() {
795 return;
796 }
797 
798 let mut state = self.state.inner.borrow_mut();
799 for &idx in intrinsic_indices {
800 if idx < intrinsic_widths.len() {
801 state.intrinsic_column_widths[idx] = Some(intrinsic_widths[idx]);
802 }
803 }
804 }
805 
806 fn measure_body_intrinsic_widths_from_rows(
807 rows: &mut [Vec<Box<dyn Element>>],
808 intrinsic_indices: &[usize],
809 intrinsic_widths: &mut [f32],
810 cell_padding: f32,
811 ctx: &mut LayoutContext,
812 app: &AppContext,
813 ) {
814 if intrinsic_indices.is_empty() {
815 return;
816 }
817 
818 let unconstrained =
819 SizeConstraint::new(vec2f(0.0, 0.0), vec2f(f32::INFINITY, f32::INFINITY));
820 let padding = cell_padding * 2.0;
821 
822 for row in rows {
823 for &col_idx in intrinsic_indices {
824 let Some(cell) = row.get_mut(col_idx) else {
825 continue;
826 };
827 let size = cell.layout(unconstrained, ctx, app);
828 intrinsic_widths[col_idx] = intrinsic_widths[col_idx].max(size.x() + padding);
829 }
830 }
831 }
832 
833 /// Compute cumulative column left positions from widths.
834 fn compute_column_lefts(widths: &[f32]) -> Vec<f32> {
835 let mut lefts = Vec::with_capacity(widths.len());
836 let mut x = 0.0;
837 for &w in widths {
838 lefts.push(x);
839 x += w;
840 }
841 lefts
842 }
843 
844 /// Layout a single row and return its height.
845 fn layout_row(
846 cells: &mut [Box<dyn Element>],
847 column_widths: &[f32],
848 cell_padding: f32,
849 ctx: &mut LayoutContext,
850 app: &AppContext,
851 ) -> f32 {
852 let mut max_height = 0.0f32;
853 let cell_count = cells.len().min(column_widths.len());
854 
855 for (i, cell) in cells.iter_mut().take(cell_count).enumerate() {
856 let content_width = (column_widths[i] - cell_padding * 2.0).max(0.0);
857 let cell_constraint = SizeConstraint::new(
858 vec2f(content_width, 0.0),
859 vec2f(content_width, f32::INFINITY),
860 );
861 let cell_size = cell.layout(cell_constraint, ctx, app);
862 max_height = max_height.max(cell_size.y());
863 }
864 
865 max_height
866 }
867}
868 
869impl Element for Table {
870 fn layout(
871 &mut self,
872 constraint: SizeConstraint,
873 ctx: &mut LayoutContext,
874 app: &AppContext,
875 ) -> Vector2F {
876 self.children.clear();
877 self.children_heights.clear();
878 
879 let column_count = self.column_count();
880 if column_count == 0 {
881 self.size = Some(Vector2F::zero());
882 return Vector2F::zero();
883 }
884 
885 let cell_padding = self.config.cell_padding;
886 
887 // Update viewport height
888 let viewport_height = if constraint.max.y().is_finite() {
889 constraint.max.y().into_pixels()
890 } else {
891 self.unconstrained_height.into_pixels()
892 };
893 
894 // Get render function and row count
895 let render_fn = self.state.row_render_fn();
896 let effective_row_count = self.state.row_count();
897 
898 // Initialize or update sumtree if row count changed
899 let mut state = self.state.inner.borrow_mut();
900 let current_row_count = state.rows.summary().count;
901 if current_row_count != effective_row_count {
902 let mut new_tree = SumTree::new();
903 for _ in 0..effective_row_count {
904 new_tree.push(TableRowItem { height: None });
905 }
906 state.rows = new_tree;
907 state.last_measured_row_index = 0;
908 }
909 state.viewport_height = viewport_height;
910 drop(state);
911 
912 // Measure intrinsic columns (with caching). In full-content mode with body-aware intrinsic
913 // sizing enabled, we only measure headers here and fold body-cell measurement into the
914 // single full-content row render below.
915 let uses_expand_to_content = matches!(
916 self.config.vertical_sizing,
917 TableVerticalSizing::ExpandToContent
918 );
919 let header_only_intrinsic_measurement =
920 uses_expand_to_content && self.config.measure_body_cells_for_intrinsic_widths;
921 let (mut intrinsic_widths, uncached_intrinsic_indices) =
922 self.prepare_intrinsic_widths(header_only_intrinsic_measurement, ctx, app);
923 if !header_only_intrinsic_measurement {
924 self.cache_intrinsic_widths(&uncached_intrinsic_indices, &intrinsic_widths);
925 }
926 let total_intrinsic: f32 = intrinsic_widths.iter().sum();
927 
928 let mut state = self.state.inner.borrow_mut();
929 if uses_expand_to_content {
930 state.scroll_top = RowScrollOffset::default();
931 drop(state);
932 
933 let mut all_row_elements = (0..effective_row_count)
934 .map(|row_idx| render_fn(row_idx, app))
935 .collect::<Vec<_>>();
936 
937 if header_only_intrinsic_measurement {
938 Self::measure_body_intrinsic_widths_from_rows(
939 &mut all_row_elements,
940 &uncached_intrinsic_indices,
941 &mut intrinsic_widths,
942 cell_padding,
943 ctx,
944 app,
945 );
946 self.cache_intrinsic_widths(&uncached_intrinsic_indices, &intrinsic_widths);
947 }
948 
949 let total_intrinsic: f32 = intrinsic_widths.iter().sum();
950 let available_width = if constraint.max.x().is_finite() {
951 constraint.max.x()
952 } else {
953 total_intrinsic.max(self.unconstrained_width)
954 };
955 let column_widths = self.compute_column_widths(available_width, &intrinsic_widths);
956 
957 let mut header_content_height = 0.0f32;
958 for (i, header) in self.headers.iter_mut().enumerate() {
959 let col_width = column_widths.get(i).copied().unwrap_or(0.0);
960 let content_width = (col_width - cell_padding * 2.0).max(0.0);
961 let cell_constraint = SizeConstraint::new(
962 vec2f(content_width, 0.0),
963 vec2f(content_width, f32::INFINITY),
964 );
965 let cell_size = header.content.layout(cell_constraint, ctx, app);
966 header_content_height = header_content_height.max(cell_size.y());
967 }
968 let header_height = (header_content_height + cell_padding * 2.0).into_pixels();
969 
970 let mut rows_tree = SumTree::new();
971 let mut rows_height = Pixels::zero();
972 for mut row_elements in all_row_elements {
973 let row_content_height =
974 Self::layout_row(&mut row_elements, &column_widths, cell_padding, ctx, app);
975 let row_height = (row_content_height + cell_padding * 2.0).into_pixels();
976 rows_tree.push(TableRowItem {
977 height: Some(row_height),
978 });
979 rows_height += row_height;
980 self.children.push(row_elements);
981 self.children_heights.push(row_height);
982 }
983 
984 let total_width: f32 = column_widths.iter().sum();
985 let content_height = header_height + rows_height;
986 let mut state = self.state.inner.borrow_mut();
987 state.header_height = header_height;
988 state.rows = rows_tree;
989 state.last_measured_row_index = effective_row_count.saturating_sub(1);
990 state.column_widths = column_widths;
991 state.visible_start_row_idx = 0;
992 state.viewport_height = content_height;
993 state.rendered_height = content_height;
994 drop(state);
995 
996 let size = vec2f(total_width, content_height.as_f32());
997 self.size = Some(size);
998 return size;
999 }
1000 
1001 // Compute column widths
1002 let available_width = if constraint.max.x().is_finite() {
1003 constraint.max.x()
1004 } else {
1005 total_intrinsic.max(self.unconstrained_width)
1006 };
1007 let column_widths = self.compute_column_widths(available_width, &intrinsic_widths);
1008 
1009 // Layout header cells directly
1010 let mut header_content_height = 0.0f32;
1011 for (i, header) in self.headers.iter_mut().enumerate() {
1012 let col_width = column_widths.get(i).copied().unwrap_or(0.0);
1013 let content_width = (col_width - cell_padding * 2.0).max(0.0);
1014 let cell_constraint = SizeConstraint::new(
1015 vec2f(content_width, 0.0),
1016 vec2f(content_width, f32::INFINITY),
1017 );
1018 let cell_size = header.content.layout(cell_constraint, ctx, app);
1019 header_content_height = header_content_height.max(cell_size.y());
1020 }
1021 let header_height = (header_content_height + cell_padding * 2.0).into_pixels();
1022 
1023 // Store header height in state
1024 let mut state = self.state.inner.borrow_mut();
1025 state.header_height = header_height;
1026 
1027 // Get current scroll position and ensure it's valid
1028 let scroll_top = state.scroll_top;
1029 let max_scroll = state.max_scroll_offset();
1030 if scroll_top > max_scroll {
1031 state.scroll_top = max_scroll;
1032 }
1033 let scroll_top = state.scroll_top;
1034 let last_measured = state.last_measured_row_index;
1035 drop(state);
1036 
1037 // Pre-scroll measurement: ensure all rows up to scroll position are measured
1038 if scroll_top.row_index.0 > last_measured {
1039 for row_idx in last_measured..scroll_top.row_index.0.min(effective_row_count) {
1040 let mut row_elements = render_fn(row_idx, app);
1041 
1042 let row_content_height =
1043 Self::layout_row(&mut row_elements, &column_widths, cell_padding, ctx, app);
1044 let row_height = (row_content_height + cell_padding * 2.0).into_pixels();
1045 
1046 let mut state = self.state.inner.borrow_mut();
1047 let (new_tree, new_last_measured) = {
1048 let mut cursor = state.rows.cursor::<RowCount, ()>();
1049 let mut new_items =
1050 cursor.slice(&RowCount(row_idx), linear_index::SeekBias::Right);
1051 new_items.push(TableRowItem {
1052 height: Some(row_height),
1053 });
1054 cursor.next();
1055 new_items.push_tree(cursor.suffix());
1056 let new_last_measured = new_items.summary().measured_count.saturating_sub(1);
1057 (new_items, new_last_measured)
1058 };
1059 state.rows = new_tree;
1060 state.last_measured_row_index = new_last_measured;
1061 }
1062 }
1063 
1064 // Layout visible rows using sumtree seeking
1065 let mut rendered_height = Pixels::zero();
1066 
1067 let state = self.state.inner.borrow();
1068 let scroll_top = state.scroll_top;
1069 // Determine starting row index from cursor
1070 let start_row_idx = scroll_top.row_index.0;
1071 drop(state);
1072 
1073 let mut measured_items = Vec::new();
1074 
1075 // Calculate maximum rows we might need to render to fill the viewport.
1076 // We break early once rendered_height >= viewport_height.
1077 let max_possible_rows =
1078 (viewport_height.as_f32() / DEFAULT_ROW_HEIGHT_ESTIMATE).ceil() as usize + 1;
1079 let end_row_idx = (start_row_idx + max_possible_rows).min(effective_row_count);
1080 
1081 for row_idx in start_row_idx..end_row_idx {
1082 if rendered_height >= viewport_height {
1083 break;
1084 }
1085 
1086 let mut row_elements = render_fn(row_idx, app);
1087 
1088 let row_content_height =
1089 Self::layout_row(&mut row_elements, &column_widths, cell_padding, ctx, app);
1090 let row_height = (row_content_height + cell_padding * 2.0).into_pixels();
1091 
1092 self.children.push(row_elements);
1093 self.children_heights.push(row_height);
1094 measured_items.push(TableRowItem {
1095 height: Some(row_height),
1096 });
1097 
1098 if row_idx == start_row_idx {
1099 rendered_height += row_height - scroll_top.offset_from_start;
1100 } else {
1101 rendered_height += row_height;
1102 }
1103 }
1104 
1105 // Update sumtree with newly measured rows
1106 let measured_range = start_row_idx..(start_row_idx + measured_items.len());
1107 let mut state = self.state.inner.borrow_mut();
1108 let new_tree = {
1109 let mut cursor = state.rows.cursor::<RowCount, ()>();
1110 let mut new_items = cursor.slice(
1111 &RowCount(measured_range.start),
1112 linear_index::SeekBias::Right,
1113 );
1114 new_items.extend(measured_items);
1115 cursor.seek(&RowCount(measured_range.end), linear_index::SeekBias::Right);
1116 new_items.push_tree(cursor.suffix());
1117 new_items
1118 };
1119 state.rows = new_tree;
1120 state.last_measured_row_index = state.rows.summary().measured_count.saturating_sub(1);
1121 
1122 // Store column info and visible range
1123 state.column_widths = column_widths.clone();
1124 state.visible_start_row_idx = start_row_idx;
1125 
1126 // Calculate actual content height (header + rendered rows)
1127 let mut rows_height = Pixels::zero();
1128 for h in &self.children_heights {
1129 rows_height += *h;
1130 }
1131 let content_height = header_height + rows_height;
1132 
1133 // Store rendered height for paint() to use
1134 // Use min(content, viewport) so table shrinks when content is smaller
1135 let rendered_height = if content_height < viewport_height {
1136 content_height
1137 } else {
1138 viewport_height
1139 };
1140 state.rendered_height = rendered_height;
1141 drop(state);
1142 
1143 let total_width: f32 = column_widths.iter().sum();
1144 // Return the rendered height (shrinks to content when content < viewport)
1145 let size = vec2f(total_width, rendered_height.as_f32());
1146 self.size = Some(size);
1147 
1148 size
1149 }
1150 
1151 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
1152 for header in &mut self.headers {
1153 header.content.after_layout(ctx, app);
1154 }
1155 for row_elements in &mut self.children {
1156 for cell in row_elements {
1157 cell.after_layout(ctx, app);
1158 }
1159 }
1160 }
1161 
1162 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
1163 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
1164 
1165 let state = self.state.inner.borrow();
1166 let column_widths = state.column_widths.clone();
1167 let header_height = state.header_height;
1168 let scroll_top = state.scroll_top;
1169 let rendered_height = state.rendered_height;
1170 let visible_start_row_idx = state.visible_start_row_idx;
1171 drop(state);
1172 
1173 // Compute column left positions from widths
1174 let column_lefts = Self::compute_column_lefts(&column_widths);
1175 
1176 if column_widths.is_empty() {
1177 return;
1178 }
1179 
1180 let total_width: f32 = column_widths.iter().sum();
1181 let scroll_offset = scroll_top.offset_from_start;
1182 let uses_fixed_header = self.config.fixed_header;
1183 let padding = self.config.cell_padding;
1184 let total_row_count = self.total_row_count();
1185 
1186 // Determine header and content origins based on scroll mode
1187 // Use rendered_height for clip bounds (shrinks to content when content < viewport)
1188 let (header_origin, content_origin, header_visible, body_clip_rect) = if uses_fixed_header {
1189 // Fixed header: header at origin, body below (clipped to exclude header area)
1190 let body_origin = origin + vec2f(0.0, header_height.as_f32());
1191 let body_height = rendered_height.as_f32() - header_height.as_f32();
1192 let body_clip = RectF::new(body_origin, vec2f(total_width, body_height));
1193 (origin, body_origin, true, body_clip)
1194 } else {
1195 // Scrolling header: clip excludes header area so borders paint on top
1196 let header_visible = scroll_top.row_index.0 == 0;
1197 let content_start_y = -scroll_offset.as_f32();
1198 let header_origin = origin + vec2f(0.0, content_start_y);
1199 let content_origin = if header_visible {
1200 origin + vec2f(0.0, content_start_y + header_height.as_f32())
1201 } else {
1202 origin + vec2f(0.0, content_start_y)
1203 };
1204 // Clip starts below header (when visible) so header border isn't covered by layer
1205 let clip_start_y = if uses_fixed_header && header_visible {
1206 header_height.as_f32()
1207 } else {
1208 0.0
1209 };
1210 let clip_origin = origin + vec2f(0.0, clip_start_y);
1211 let clip_height = rendered_height.as_f32() - clip_start_y;
1212 let clip = RectF::new(clip_origin, vec2f(total_width, clip_height));
1213 (header_origin, content_origin, header_visible, clip)
1214 };
1215 
1216 // Start clipping layer for body content
1217 ctx.scene
1218 .start_layer(ClipBounds::BoundedByActiveLayerAnd(body_clip_rect));
1219 
1220 // In scrolling mode, paint header INSIDE the clip so it gets clipped at table bounds
1221 if !uses_fixed_header && header_visible {
1222 let header_rect = RectF::new(header_origin, vec2f(total_width, header_height.as_f32()));
1223 ctx.scene
1224 .draw_rect_with_hit_recording(header_rect)
1225 .with_background(self.config.header_background);
1226 
1227 for (col_idx, header) in self.headers.iter_mut().enumerate() {
1228 let col_left = column_lefts.get(col_idx).copied().unwrap_or(0.0);
1229 let cell_origin = header_origin + vec2f(col_left + padding, padding);
1230 header.content.paint(cell_origin, ctx, app);
1231 }
1232 
1233 if self.config.border_width > 0.0 && self.config.row_dividers {
1234 let divider_y = Self::snap_to_pixel(
1235 header_origin.y() + header_height.as_f32() - self.config.border_width,
1236 );
1237 ctx.scene
1238 .draw_rect_with_hit_recording(RectF::new(
1239 vec2f(origin.x(), divider_y),
1240 vec2f(total_width, self.config.border_width),
1241 ))
1242 .with_background(self.config.border_color);
1243 }
1244 }
1245 
1246 // Paint rows
1247 let row_scroll_offset = if uses_fixed_header {
1248 scroll_offset
1249 } else {
1250 Pixels::zero()
1251 };
1252 let mut current_y = -row_scroll_offset.as_f32();
1253 
1254 for (child_idx, (row_elements, row_height)) in self
1255 .children
1256 .iter_mut()
1257 .zip(&self.children_heights)
1258 .enumerate()
1259 {
1260 let absolute_row_idx = visible_start_row_idx + child_idx;
1261 let row_height_f32 = row_height.as_f32();
1262 
1263 let bg_color = self.config.row_background.color_for_row(absolute_row_idx);
1264 let row_rect = RectF::new(
1265 content_origin + vec2f(0.0, current_y),
1266 vec2f(total_width, row_height_f32),
1267 );
1268 ctx.scene
1269 .draw_rect_with_hit_recording(row_rect)
1270 .with_background(bg_color);
1271 
1272 for (col_idx, cell) in row_elements.iter_mut().enumerate() {
1273 let col_left = column_lefts.get(col_idx).copied().unwrap_or(0.0);
1274 let cell_origin = content_origin + vec2f(col_left + padding, current_y + padding);
1275 cell.paint(cell_origin, ctx, app);
1276 }
1277 
1278 if self.config.border_width > 0.0
1279 && self.config.row_dividers
1280 && absolute_row_idx + 1 < total_row_count
1281 {
1282 let divider_y = Self::snap_to_pixel(
1283 content_origin.y() + current_y + row_height_f32 - self.config.border_width,
1284 );
1285 ctx.scene
1286 .draw_rect_with_hit_recording(RectF::new(
1287 vec2f(origin.x(), divider_y),
1288 vec2f(total_width, self.config.border_width),
1289 ))
1290 .with_background(self.config.border_color);
1291 }
1292 
1293 current_y += row_height_f32;
1294 }
1295 
1296 // Stop body clip layer
1297 ctx.scene.stop_layer();
1298 
1299 // In fixed mode, paint header OUTSIDE the clip so it stays on top while body scrolls
1300 if uses_fixed_header && header_visible {
1301 let header_rect = RectF::new(header_origin, vec2f(total_width, header_height.as_f32()));
1302 ctx.scene
1303 .draw_rect_with_hit_recording(header_rect)
1304 .with_background(self.config.header_background);
1305 
1306 for (col_idx, header) in self.headers.iter_mut().enumerate() {
1307 let col_left = column_lefts.get(col_idx).copied().unwrap_or(0.0);
1308 let cell_origin = header_origin + vec2f(col_left + padding, padding);
1309 header.content.paint(cell_origin, ctx, app);
1310 }
1311 
1312 if self.config.border_width > 0.0 && self.config.row_dividers {
1313 let bw = self.config.border_width;
1314 let header_bottom_y =
1315 Self::snap_to_pixel(header_origin.y() + header_height.as_f32() - bw);
1316 ctx.scene
1317 .draw_rect_with_hit_recording(RectF::new(
1318 vec2f(origin.x(), header_bottom_y),
1319 vec2f(total_width, bw),
1320 ))
1321 .with_background(self.config.border_color);
1322 }
1323 }
1324 
1325 // Draw borders using rendered_height (shrinks to content when content < viewport)
1326 if self.config.border_width > 0.0 {
1327 let bw = self.config.border_width;
1328 let border_height = rendered_height.as_f32();
1329 let border_color = self.config.border_color;
1330 let top_y = Self::snap_to_pixel(origin.y());
1331 let bottom_y = Self::snap_to_pixel(origin.y() + border_height - bw);
1332 let left_x = Self::snap_to_pixel(origin.x());
1333 let right_x = Self::snap_to_pixel(origin.x() + total_width - bw);
1334 let inner_top_y = top_y + bw;
1335 let inner_height = (bottom_y - inner_top_y).max(0.0);
1336 
1337 if self.config.column_dividers {
1338 for &col_left in column_lefts.iter().skip(1) {
1339 let divider_x = Self::snap_to_pixel(origin.x() + col_left);
1340 let (divider_top_y, divider_height) = if self.config.outer_border {
1341 (inner_top_y, inner_height)
1342 } else {
1343 (top_y, border_height)
1344 };
1345 let line_rect =
1346 RectF::new(vec2f(divider_x, divider_top_y), vec2f(bw, divider_height));
1347 ctx.scene
1348 .draw_rect_with_hit_recording(line_rect)
1349 .with_background(border_color);
1350 }
1351 }
1352 
1353 if self.config.outer_border {
1354 ctx.scene
1355 .draw_rect_with_hit_recording(RectF::new(
1356 vec2f(left_x, top_y),
1357 vec2f(total_width, bw),
1358 ))
1359 .with_background(border_color);
1360 ctx.scene
1361 .draw_rect_with_hit_recording(RectF::new(
1362 vec2f(left_x, bottom_y),
1363 vec2f(total_width, bw),
1364 ))
1365 .with_background(border_color);
1366 ctx.scene
1367 .draw_rect_with_hit_recording(RectF::new(
1368 vec2f(left_x, top_y),
1369 vec2f(bw, border_height),
1370 ))
1371 .with_background(border_color);
1372 ctx.scene
1373 .draw_rect_with_hit_recording(RectF::new(
1374 vec2f(right_x, top_y),
1375 vec2f(bw, border_height),
1376 ))
1377 .with_background(border_color);
1378 }
1379 }
1380 }
1381 
1382 fn dispatch_event(
1383 &mut self,
1384 event: &DispatchedEvent,
1385 ctx: &mut EventContext,
1386 app: &AppContext,
1387 ) -> bool {
1388 let mut handled = false;
1389 for header in &mut self.headers {
1390 handled |= header.content.dispatch_event(event, ctx, app);
1391 }
1392 for row_elements in &mut self.children {
1393 for cell in row_elements {
1394 handled |= cell.dispatch_event(event, ctx, app);
1395 }
1396 }
1397 handled
1398 }
1399 
1400 fn size(&self) -> Option<Vector2F> {
1401 self.size
1402 }
1403 
1404 fn origin(&self) -> Option<Point> {
1405 self.origin
1406 }
1407 
1408 fn as_selectable_element(&self) -> Option<&dyn SelectableElement> {
1409 Some(self as &dyn SelectableElement)
1410 }
1411}
1412 
1413impl SelectableElement for Table {
1414 fn get_selection(
1415 &self,
1416 selection_start: Vector2F,
1417 selection_end: Vector2F,
1418 is_rect: IsRect,
1419 ) -> Option<Vec<SelectionFragment>> {
1420 let mut selection_fragments: Vec<SelectionFragment> = Vec::new();
1421 let mut had_prior_row = false;
1422 
1423 let mut header_fragments: Vec<SelectionFragment> = Vec::new();
1424 for header in &self.headers {
1425 if let Some(selectable_child) = header.content.as_selectable_element() {
1426 if let Some(child_fragments) =
1427 selectable_child.get_selection(selection_start, selection_end, is_rect)
1428 {
1429 if !header_fragments.is_empty() {
1430 if let Some(last_fragment) = header_fragments.last() {
1431 header_fragments.push(SelectionFragment {
1432 text: "\t".to_string(),
1433 origin: last_fragment.origin,
1434 });
1435 }
1436 }
1437 header_fragments.extend(child_fragments);
1438 }
1439 }
1440 }
1441 
1442 if !header_fragments.is_empty() {
1443 selection_fragments.extend(header_fragments);
1444 had_prior_row = true;
1445 }
1446 
1447 // VIRTUALIZATION SELECTION LIMITATION:
1448 // When a table is virtualized, only the rendered rows can be selected.
1449 // Non-visible rows are not laid out or painted, so their content cannot be
1450 // included in the selection. This is an expected limitation.
1451 //
1452 // To maintain logical consistency, we insert placeholder newlines for non-visible
1453 // rows between the header and first visible row. This preserves row structure in
1454 // the selection text, but the actual cell content of non-visible rows is unavailable.
1455 let state = self.state.inner.borrow();
1456 let visible_start_row_idx = state.visible_start_row_idx;
1457 drop(state);
1458 
1459 if had_prior_row && !self.children.is_empty() && visible_start_row_idx > 0 {
1460 for _ in 0..visible_start_row_idx {
1461 if let Some(last_fragment) = selection_fragments.last() {
1462 selection_fragments.push(SelectionFragment {
1463 text: "\n".to_string(),
1464 origin: last_fragment.origin,
1465 });
1466 }
1467 }
1468 }
1469 
1470 for row_elements in &self.children {
1471 let mut row_fragments: Vec<SelectionFragment> = Vec::new();
1472 for cell in row_elements {
1473 if let Some(selectable_child) = cell.as_selectable_element() {
1474 if let Some(child_fragments) =
1475 selectable_child.get_selection(selection_start, selection_end, is_rect)
1476 {
1477 if !row_fragments.is_empty() {
1478 if let Some(last_fragment) = row_fragments.last() {
1479 row_fragments.push(SelectionFragment {
1480 text: "\t".to_string(),
1481 origin: last_fragment.origin,
1482 });
1483 }
1484 }
1485 row_fragments.extend(child_fragments);
1486 }
1487 }
1488 }
1489 if !row_fragments.is_empty() {
1490 if had_prior_row {
1491 if let Some(last_fragment) = selection_fragments.last() {
1492 selection_fragments.push(SelectionFragment {
1493 text: "\n".to_string(),
1494 origin: last_fragment.origin,
1495 });
1496 }
1497 }
1498 selection_fragments.extend(row_fragments);
1499 had_prior_row = true;
1500 }
1501 }
1502 
1503 if !selection_fragments.is_empty() {
1504 Some(selection_fragments)
1505 } else {
1506 None
1507 }
1508 }
1509 
1510 fn expand_selection(
1511 &self,
1512 point: Vector2F,
1513 direction: SelectionDirection,
1514 unit: SelectionType,
1515 word_boundaries_policy: &WordBoundariesPolicy,
1516 ) -> Option<Vector2F> {
1517 let mut expanded_selection = None;
1518 
1519 for header in &self.headers {
1520 if let Some(selectable_child) = header.content.as_selectable_element() {
1521 if let Some(selection) = selectable_child.expand_selection(
1522 point,
1523 direction,
1524 unit,
1525 word_boundaries_policy,
1526 ) {
1527 match direction {
1528 SelectionDirection::Backward => return Some(selection),
1529 SelectionDirection::Forward => {
1530 expanded_selection = Some(selection);
1531 }
1532 }
1533 }
1534 }
1535 }
1536 
1537 for row_elements in &self.children {
1538 for cell in row_elements {
1539 if let Some(selectable_child) = cell.as_selectable_element() {
1540 if let Some(selection) = selectable_child.expand_selection(
1541 point,
1542 direction,
1543 unit,
1544 word_boundaries_policy,
1545 ) {
1546 match direction {
1547 SelectionDirection::Backward => return Some(selection),
1548 SelectionDirection::Forward => {
1549 expanded_selection = Some(selection);
1550 }
1551 }
1552 }
1553 }
1554 }
1555 }
1556 
1557 expanded_selection
1558 }
1559 
1560 fn is_point_semantically_before(
1561 &self,
1562 absolute_point: Vector2F,
1563 absolute_point_other: Vector2F,
1564 ) -> Option<bool> {
1565 for header in &self.headers {
1566 if let Some(selectable_child) = header.content.as_selectable_element() {
1567 if let Some(result) = selectable_child
1568 .is_point_semantically_before(absolute_point, absolute_point_other)
1569 {
1570 return Some(result);
1571 }
1572 }
1573 }
1574 
1575 for row_elements in &self.children {
1576 for cell in row_elements {
1577 if let Some(selectable_child) = cell.as_selectable_element() {
1578 if let Some(result) = selectable_child
1579 .is_point_semantically_before(absolute_point, absolute_point_other)
1580 {
1581 return Some(result);
1582 }
1583 }
1584 }
1585 }
1586 
1587 None
1588 }
1589 
1590 fn smart_select(
1591 &self,
1592 absolute_point: Vector2F,
1593 smart_select_fn: SmartSelectFn,
1594 ) -> Option<(Vector2F, Vector2F)> {
1595 for header in &self.headers {
1596 if let Some(selectable_child) = header.content.as_selectable_element() {
1597 if let Some(selection) =
1598 selectable_child.smart_select(absolute_point, smart_select_fn)
1599 {
1600 return Some(selection);
1601 }
1602 }
1603 }
1604 
1605 for row_elements in &self.children {
1606 for cell in row_elements {
1607 if let Some(selectable_child) = cell.as_selectable_element() {
1608 if let Some(selection) =
1609 selectable_child.smart_select(absolute_point, smart_select_fn)
1610 {
1611 return Some(selection);
1612 }
1613 }
1614 }
1615 }
1616 
1617 None
1618 }
1619 
1620 fn calculate_clickable_bounds(&self, current_selection: Option<Selection>) -> Vec<RectF> {
1621 let mut clickable_bounds = Vec::new();
1622 
1623 for header in &self.headers {
1624 if let Some(selectable_child) = header.content.as_selectable_element() {
1625 clickable_bounds
1626 .append(&mut selectable_child.calculate_clickable_bounds(current_selection));
1627 }
1628 }
1629 
1630 for row_elements in &self.children {
1631 for cell in row_elements {
1632 if let Some(selectable_child) = cell.as_selectable_element() {
1633 clickable_bounds.append(
1634 &mut selectable_child.calculate_clickable_bounds(current_selection),
1635 );
1636 }
1637 }
1638 }
1639 
1640 clickable_bounds
1641 }
1642}
1643 
1644impl ScrollableElement for Table {
1645 fn scroll_data(&self, _app: &AppContext) -> Option<ScrollData> {
1646 if matches!(
1647 self.config.vertical_sizing,
1648 TableVerticalSizing::ExpandToContent
1649 ) {
1650 return None;
1651 }
1652 self.vertical_scroll_data()
1653 }
1654 
1655 fn scroll(&mut self, delta: Pixels, ctx: &mut EventContext) {
1656 if matches!(
1657 self.config.vertical_sizing,
1658 TableVerticalSizing::ExpandToContent
1659 ) {
1660 return;
1661 }
1662 self.scroll_vertically(delta, ctx);
1663 }
1664 
1665 fn should_handle_scroll_wheel(&self) -> bool {
1666 !matches!(
1667 self.config.vertical_sizing,
1668 TableVerticalSizing::ExpandToContent
1669 )
1670 }
1671}
1672 
1673impl Table {
1674 fn snap_to_pixel(value: f32) -> f32 {
1675 value.round()
1676 }
1677 fn scroll_vertically(&mut self, delta: Pixels, ctx: &mut EventContext) {
1678 let mut state = self.state.inner.borrow_mut();
1679 
1680 let current_scroll_top = state.scroll_top_pixels();
1681 let viewport_height = state.viewport_height;
1682 let approximate_height = state.approximate_height();
1683 let scroll_max = (approximate_height - viewport_height).max(Pixels::zero());
1684 let new_scroll_top = (current_scroll_top - delta)
1685 .max(Pixels::zero())
1686 .min(scroll_max);
1687 
1688 state.scroll_top = state.absolute_pixels_to_scroll_offset(new_scroll_top);
1689 drop(state);
1690 
1691 ctx.notify();
1692 }
1693 
1694 fn vertical_scroll_data(&self) -> Option<ScrollData> {
1695 let state = self.state.inner.borrow();
1696 let viewport_height = state.viewport_height;
1697 let scroll_start = state.scroll_top_pixels();
1698 let total_size = state.approximate_height();
1699 drop(state);
1700 
1701 Some(ScrollData {
1702 scroll_start,
1703 visible_px: viewport_height,
1704 total_size,
1705 })
1706 }
1707}
1708 
1709impl Table {
1710 #[cfg(test)]
1711 pub(crate) fn visible_row_count(&self) -> usize {
1712 self.children.len()
1713 }
1714}
1715 
1716#[cfg(test)]
1717#[path = "mod_tests.rs"]
1718mod tests;
1719