1use super::*;
9impl<T: 'static> Widget for TreeTableView<T> {
10 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
11 let self_id = ctx.self_id();
12 ctx.enabled_when(self_id, self.enabled.clone());
13
14 let row_h = self.effective_row_height();
15 let header_h = self.effective_header_height();
16 let indent_per_level = self.effective_indent();
17
18 let version = ctx.signal(0_u64);
19 version.bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
20
21 self.scroll_y.bind_to(
22 ctx.self_id(),
23 ctx.binding_registry(),
24 BindingLevel::Relayout,
25 );
26 ctx.register_animated_signal(&self.scroll_y);
27
28 self.scroll_x.bind_to(
29 ctx.self_id(),
30 ctx.binding_registry(),
31 BindingLevel::Relayout,
32 );
33 ctx.register_animated_signal(&self.scroll_x);
34
35 self.pane_total_refresh.bind_to(
40 ctx.self_id(),
41 ctx.binding_registry(),
42 BindingLevel::Relayout,
43 );
44
45 self.column_widths_signal.bind_to(
46 ctx.self_id(),
47 ctx.binding_registry(),
48 BindingLevel::Relayout,
49 );
50 self.resize_preview_x.bind_to(
53 ctx.self_id(),
54 ctx.binding_registry(),
55 BindingLevel::RepaintOnly,
56 );
57
58 {
62 let resize_state = self.resize_state.clone();
63 let resize_target = self.resize_target.clone();
64 let resize_preview_x = self.resize_preview_x.clone();
65 ctx.effect(&ctx.window_active_signal(), move |active| {
66 if !*active && resize_state.borrow().is_some() {
67 *resize_state.borrow_mut() = None;
68 resize_target.set(None);
69 resize_preview_x.set(None);
70 }
71 });
72 }
73 self.focused_cell.bind_to(
74 ctx.self_id(),
75 ctx.binding_registry(),
76 BindingLevel::RepaintOnly,
77 );
78 self.focused_cell.bind_to(
83 ctx.self_id(),
84 ctx.binding_registry(),
85 BindingLevel::AccessibilityOnly,
86 );
87
88 self.view_focused = ctx.begin_view_focus();
98 ctx.end_view_focus();
99 self.focus_visible = ctx.focus_visible();
100 self.reveal_current_row_on_focus(ctx);
101 self.view_focused.bind_to(
102 ctx.self_id(),
103 ctx.binding_registry(),
104 BindingLevel::RepaintOnly,
105 );
106 self.focus_visible.bind_to(
107 ctx.self_id(),
108 ctx.binding_registry(),
109 BindingLevel::RepaintOnly,
110 );
111 self.drop_feedback.bind_to(
114 ctx.self_id(),
115 ctx.binding_registry(),
116 BindingLevel::RepaintOnly,
117 );
118
119 let v_for_proj = version.clone();
125 let proj_ver = Rc::new(Cell::new(0_u64));
126 let prev_visible_count = Rc::new(Cell::new(self.source.visible_count()));
127 ctx.effect(&self.source.version_signal(), {
128 let metrics = self.row_metrics.clone();
129 let src = self.source.clone();
130 let row_sel = self.row_selection.clone();
131 let cell_sel = self.cell_selection.clone();
132 let prev_visible_count = prev_visible_count.clone();
133 move |_| {
134 metrics
135 .borrow_mut()
136 .apply_divergence(src.first_changed_index(), src.visible_count());
137 if let Some(ref rs) = row_sel {
140 rs.prune();
141 }
142 let new_visible_count = src.visible_count();
156 if let Some(ref cs) = cell_sel
157 && new_visible_count != prev_visible_count.get()
158 {
159 cs.clear();
160 }
161 prev_visible_count.set(new_visible_count);
162 let next = proj_ver.get() + 1;
163 proj_ver.set(next);
164 v_for_proj.set(next);
165 }
166 });
167
168 let v_for_sort = version.clone();
179 let sv = Rc::new(Cell::new(0_u64));
180 ctx.effect(&self.sort_signal, move |_| {
181 let next = sv.get() + 1;
182 sv.set(next);
183 v_for_sort.set(next);
184 });
185 let v_for_order = version.clone();
186 let ov = Rc::new(Cell::new(0_u64));
187 ctx.effect(&self.column_order_signal, move |_| {
188 let next = ov.get() + 1;
189 ov.set(next);
190 v_for_order.set(next);
191 });
192 let v_for_pin = version.clone();
193 let pv = Rc::new(Cell::new(0_u64));
194 ctx.effect(&self.column_pinning_signal, move |_| {
195 let next = pv.get() + 1;
196 pv.set(next);
197 v_for_pin.set(next);
198 });
199 let display_indices = self.display_order();
206
207 {
214 let old_display = self.display_indices.borrow();
215 if !old_display.is_empty() {
216 let old_to_new: Vec<Option<usize>> = old_display
217 .iter()
218 .map(|&decl_idx| {
219 let id = &self.columns[decl_idx].id;
220 display_indices
221 .iter()
222 .position(|&new_decl_idx| self.columns[new_decl_idx].id == *id)
223 })
224 .collect();
225 drop(old_display);
226 imperative::remap_cell_state(
227 &self.focused_cell,
228 &self.editing_cell,
229 self.cell_selection.as_ref(),
230 &old_to_new,
231 );
232 }
233 }
234 *self.display_indices.borrow_mut() = display_indices.clone();
235 let tree_decl = self.tree_column_decl_index();
236 let tree_display_pos = display_indices
237 .iter()
238 .position(|&i| i == tree_decl)
239 .unwrap_or(0);
240
241 let line_height = row_h;
243
244 let column_ids_in_display_order: Vec<String> = display_indices
245 .iter()
246 .map(|&i| self.columns[i].id.clone())
247 .collect();
248 let display_col_to_id: Rc<dyn Fn(usize) -> Option<String>> = {
249 let ids = column_ids_in_display_order;
250 Rc::new(move |pos| ids.get(pos).cloned())
251 };
252 let display_col_triggers: Rc<dyn Fn(usize) -> EditTriggers> = {
256 let view_triggers = self.edit_triggers;
257 let per_display_column: Vec<EditTriggers> = display_indices
258 .iter()
259 .map(|&i| self.columns[i].effective_edit_triggers(view_triggers))
260 .collect();
261 Rc::new(move |pos| {
262 per_display_column
263 .get(pos)
264 .copied()
265 .unwrap_or(EditTriggers::NONE)
266 })
267 };
268
269 let navigator: Rc<dyn RowNavigator> = Rc::new(TreeNavigator::new(self.source.clone()));
270 let type_ahead_label: Option<Rc<dyn Fn(usize) -> Option<String>>> =
273 self.type_ahead_label.clone().map(|user| {
274 let src = self.source.clone();
275 Rc::new(move |i: usize| src.with_row_str(i, &|item| user(item)))
276 as Rc<dyn Fn(usize) -> Option<String>>
277 });
278
279 let key_cfg = keyboard::KeyHandlerConfig {
280 navigator,
281 col_count: display_indices.len().max(1),
282 tree_column_display_pos: tree_display_pos,
287 focused_cell: self.focused_cell.clone(),
288 cell_map: self.cell_map.clone(),
289 selection_mode: self.selection_mode,
290 selection: self.row_selection.clone(),
291 cell_selection: self.cell_selection.clone(),
292 scroll_y: self.scroll_y.clone(),
293 max_scroll_y: self.max_scroll_y.clone(),
294 viewport_height: self.viewport_height.clone(),
295 body_bounds: self.body_bounds.clone(),
296 row_metrics: self.row_metrics.clone(),
297 tab_traversal: self.tab_traversal,
298 editing_cell: self.editing_cell.clone(),
299 display_col_to_id,
300 display_col_triggers,
301 on_cell_edit_request: self.on_cell_edit_request.clone(),
302 on_row_activate: self.on_row_activate.clone(),
303 type_ahead: self.type_ahead.clone(),
304 type_ahead_label,
305 type_ahead_timeout: self.type_ahead_timeout,
306 column_widths: self.column_widths.clone(),
307 pane_boundaries: *self.pane_boundaries.borrow(),
308 scroll_x: self.scroll_x.clone(),
309 max_scroll_x: self.max_scroll_x.clone(),
310 middle_viewport_width: self.middle_viewport_width.clone(),
311 };
312
313 let (reorder_perform, reparent_perform) = if self.reorderable {
323 let follow: Rc<dyn Fn(usize)> = {
324 let focused = self.focused_cell.clone();
325 let sel = self.row_selection.clone();
326 Rc::new(move |new_flat: usize| {
327 let col = focused.get().map(|(_, c)| c).unwrap_or(0);
328 focused.set(Some((new_flat, col)));
329 if let Some(ref s) = sel {
330 s.select(new_flat);
331 }
332 })
333 };
334 let name_of: Rc<dyn Fn(usize) -> Option<String>> = {
335 let source = self.source.clone();
336 let label = self.type_ahead_label.clone();
337 Rc::new(move |index: usize| {
338 let label = label.as_ref()?;
339 source.with_row_str(index, &|item| label(item))
340 })
341 };
342 let sibling = {
343 let source = self.source.clone();
344 let follow = follow.clone();
345 let name_of = name_of.clone();
346 let sort = self.sort_signal.clone();
347 Rc::new(
348 move |mv: crate::common::ordered_move::OrderedMove,
349 flat: usize,
350 ctx: &mut EventContext| {
351 if sort.get().is_some() {
352 return;
353 }
354 let name = (name_of)(flat);
355 let Some(new_flat) = source.sibling_move(flat, mv) else {
356 return;
357 };
358 follow(new_flat);
359 let (pos, size) = source.sibling_position(new_flat);
360 ctx.announce(crate::common::ordered_move::move_announcement(
361 name.as_deref(),
362 pos.saturating_sub(1),
363 size,
364 ));
365 },
366 ) as crate::common::ordered_move::MoveRow
367 };
368 let reparent = {
369 let source = self.source.clone();
370 let follow = follow.clone();
371 let sort = self.sort_signal.clone();
372 Rc::new(
373 move |mv: crate::common::ordered_move::TreeMove,
374 flat: usize,
375 ctx: &mut EventContext| {
376 if sort.get().is_some() {
377 return;
378 }
379 let name = (name_of)(flat);
380 let Some(new_flat) = source.reparent(flat, mv) else {
381 return;
382 };
383 follow(new_flat);
384 let level = source.meta(new_flat).map_or(1, |m| m.depth + 1);
385 ctx.announce(crate::common::ordered_move::reparent_announcement(
386 name.as_deref(),
387 level,
388 ));
389 },
390 ) as crate::common::ordered_move::TreeReparentRow
391 };
392 (Some(sibling), Some(reparent))
393 } else {
394 (None, None)
395 };
396
397 let mut shared_key = keyboard::build_key_handler(key_cfg);
402 let reorder_key = reorder_perform.clone();
403 let reparent_key = reparent_perform.clone();
404 let focused_kbd = self.focused_cell.clone();
405 let sel_kbd = self.row_selection.clone();
406 let key_handler = move |event: &teksilo_core::event::WidgetEvent,
407 ctx: &mut EventContext|
408 -> EventResponse {
409 use teksilo_core::event::WidgetEvent;
410 if let Some(ref sibling) = reorder_key
411 && let Some(ref reparent) = reparent_key
412 && let WidgetEvent::KeyDown { key, modifiers, .. } = event
413 && (modifiers.alt() || modifiers.command())
416 {
417 let row = focused_kbd.get().map(|(r, _)| r).or_else(|| {
418 sel_kbd
419 .as_ref()
420 .and_then(|s| s.selected_indices().first().copied())
421 });
422 if let Some(flat_idx) = row {
423 if let Some(mv) = crate::common::ordered_move::OrderedMove::from_key(
424 *key,
425 *modifiers,
426 crate::common::ordered_move::MoveAxis::Vertical,
427 ctx.is_rtl(),
428 ) {
429 sibling(mv, flat_idx, ctx);
430 return EventResponse::Handled;
431 }
432 if let Some(mv) = crate::common::ordered_move::TreeMove::from_key(
433 *key,
434 *modifiers,
435 ctx.is_rtl(),
436 ) {
437 reparent(mv, flat_idx, ctx);
438 return EventResponse::Handled;
439 }
440 }
441 }
442 shared_key(event, ctx)
443 };
444
445 let mut handlers = HandlerSet::new()
450 .on_key(key_handler)
451 .clips_children(true)
452 .focusable(true);
453 {
454 use crate::common::scrollable::{
455 ScrollableAxes, ScrollableBehavior, handle_scroll_event, shift_wheel_remap,
456 };
457 let axes = ScrollableAxes::new(
458 self.scroll_x.clone(),
459 self.scroll_y.clone(),
460 self.max_scroll_x.clone(),
461 self.max_scroll_y.clone(),
462 );
463 let behavior = ScrollableBehavior::new(axes.clone())
464 .with_scroller(self.scroller.clone())
465 .axes(PanAxes::BOTH)
466 .overscroll(self.overscroll_behavior)
467 .smooth(self.smooth_scrolling)
468 .smooth_duration(self.smooth_scroll_duration)
469 .line_height(line_height)
470 .reduced_motion(ctx.prefers_reduced_motion())
471 .physics(ctx.theme().input.scroll_physics);
472 let scroller = behavior.scroller();
477 let options = behavior.options();
478 let behavior = behavior.before(move |event, ctx| {
486 shift_wheel_remap(event, ctx)
487 .map(|remapped| handle_scroll_event(&remapped, &axes, &scroller, &options, ctx))
488 });
489 handlers = behavior.install(handlers);
490 }
491
492 if self.export.is_drop_target(self.reorderable) || self.on_foreign_drop.is_some() {
512 let my_model_id = self.model_id;
513 let source_for_hover = self.source.clone();
514 let metrics_for_hover = self.row_metrics.clone();
515 let scroll_for_hover = self.scroll_y.clone();
516 let header_h_for_hover = header_h;
517 let feedback_for_hover = self.drop_feedback.clone();
518 let sort_for_hover = self.sort_signal.clone();
519 let reorderable_hover = self.reorderable;
520 let export_for_hover = self.export.clone();
521 let has_foreign_hook_hover = self.on_foreign_drop.is_some();
522 let bounds_for_hover = self.body_bounds.clone();
523 handlers = handlers.on_drag_hover(move |payload, position, ctx| {
524 if payload.has_typed::<ColumnReorderDragData>() {
532 feedback_for_hover.set(None);
533 return teksilo_core::DropFeedback::NoFeedback;
534 }
535 let viz_width = bounds_for_hover.get().width.max(1.0);
538 let count = source_for_hover.visible_count();
539 if count == 0 {
540 feedback_for_hover.set(None);
541 return teksilo_core::DropFeedback::NoFeedback;
542 }
543 let rd = payload.get_typed::<RowDragData<T>>();
544 let is_same_view = rd.is_some_and(|r| r.source == my_model_id);
545 let reorder_ok =
546 is_same_view && reorderable_hover && sort_for_hover.get().is_none();
547 let foreign_ok = !is_same_view
551 && (has_foreign_hook_hover
552 || export_for_hover.accepts_foreign_export(payload, my_model_id));
553 if !reorder_ok && !foreign_ok {
554 feedback_for_hover.set(None);
555 return teksilo_core::DropFeedback::NoFeedback;
556 }
557 let scroll = scroll_for_hover.get().max(0.0);
558 let content_y = position.y - header_h_for_hover + scroll;
559 let (insertion_top, row_idx, row_top, row_h) = {
560 let mut m = metrics_for_hover.borrow_mut();
561 m.resize(count);
562 let ins = m.insertion_index(content_y);
563 let r = m.row_at(content_y);
564 (m.row_top(ins), r, m.row_top(r), m.row_height(r))
565 };
566 let y_in_row = content_y - row_top;
572 let drop_pos = crate::common::drop_bands::drop_position_in_row(
573 y_in_row,
574 row_h,
575 ctx.pointer_kind(),
576 );
577 let (effective, depth) = if reorder_ok {
585 match (source_for_hover.dnd.can_accept_fn)(
586 payload,
587 row_idx,
588 drop_pos,
589 my_model_id,
590 ) {
591 DropResponse::Reject => {
592 if !foreign_ok {
593 feedback_for_hover.set(None);
594 return teksilo_core::DropFeedback::NoFeedback;
595 }
596 (DropPosition::Before, 0)
597 }
598 DropResponse::Accept => (drop_pos, source_for_hover.depth(row_idx)),
599 DropResponse::Redirect(p) => (p, source_for_hover.depth(row_idx)),
600 }
601 } else {
602 (DropPosition::Before, 0)
604 };
605 if effective == DropPosition::Into {
606 let top = row_top - scroll;
607 feedback_for_hover.set(Some(DropViz::Rect {
608 top,
609 height: row_h,
610 width: viz_width,
611 depth,
612 }));
613 teksilo_core::DropFeedback::HighlightRect {
614 rect: Rect::new(0.0, top, viz_width, row_h),
615 color: drop_into_tint(),
616 }
617 } else {
618 let insertion_y = insertion_top - scroll;
619 feedback_for_hover.set(Some(DropViz::Line {
620 y: insertion_y,
621 width: viz_width,
622 depth,
623 }));
624 teksilo_core::DropFeedback::InsertionLine {
625 y: insertion_y,
626 width: viz_width,
627 }
628 }
629 });
630
631 let drop_model_id = self.model_id;
632 let source_for_drop = self.source.clone();
633 let metrics_for_drop = self.row_metrics.clone();
634 let scroll_for_drop = self.scroll_y.clone();
635 let header_h_for_drop = header_h;
636 let feedback_for_drop = self.drop_feedback.clone();
637 let sort_for_drop = self.sort_signal.clone();
638 let reorderable_drop = self.reorderable;
639 let on_foreign_for_drop = self.on_foreign_drop.clone();
640 let proxy_for_foreign_hook = self.proxy.clone();
641 let export_for_drop = self.export.clone();
642 handlers = handlers.on_drop(move |mut payload, position, ctx| {
643 feedback_for_drop.set(None);
644 if payload.has_typed::<ColumnReorderDragData>() {
648 return false;
649 }
650 let count = source_for_drop.visible_count();
651 if count == 0 {
652 return false;
653 }
654 let scroll = scroll_for_drop.get().max(0.0);
655 let content_y = position.y - header_h_for_drop + scroll;
656 let (flat_idx, row_top, row_h, ins) = {
657 let mut m = metrics_for_drop.borrow_mut();
658 m.resize(count);
659 let idx = m.row_at(content_y);
660 let ins = m.insertion_index(content_y);
661 (idx, m.row_top(idx), m.row_height(idx), ins)
662 };
663 let y_in_row = content_y - row_top;
669 let drop_pos = crate::common::drop_bands::drop_position_in_row(
670 y_in_row,
671 row_h,
672 ctx.pointer_kind(),
673 );
674 let is_same_view = payload
675 .get_typed::<RowDragData<T>>()
676 .is_some_and(|rd| rd.source == drop_model_id);
677 if is_same_view && (!reorderable_drop || sort_for_drop.get().is_some()) {
678 return false;
679 }
680 if (reorderable_drop || !is_same_view)
685 && (source_for_drop.dnd.accept_drop_fn)(
686 &payload,
687 flat_idx,
688 drop_pos,
689 drop_model_id,
690 )
691 {
692 if is_same_view {
693 export_for_drop.note_self_reorder();
694 }
695 return true;
696 }
697 if export_for_drop.foreign_receive(&mut payload, drop_model_id, ins, ctx) {
700 return true;
701 }
702 if let Some(ref hook) = on_foreign_for_drop
706 && let Some(ref p) = proxy_for_foreign_hook
707 && let Some(node) = p.visible_node_id(flat_idx)
708 {
709 return hook(&payload, node, drop_pos, ctx);
710 }
711 false
712 });
713
714 let feedback_for_leave = self.drop_feedback.clone();
715 handlers = handlers.on_drag_leave(move |_ctx| {
716 feedback_for_leave.set(None);
717 });
718
719 let scroll_for_tick = self.scroll_y.clone();
720 let max_scroll_for_tick = self.max_scroll_y.clone();
721 let viewport_for_tick = self.viewport_height.clone();
722 let header_h_for_tick = header_h;
723 handlers = handlers.on_drag_tick(move |pos, ctx| {
724 let body_h = (viewport_for_tick.get() - header_h_for_tick).max(0.0);
727 let y = pos.y - header_h_for_tick;
728 let band = crate::common::drag_autoscroll::band_for(ctx.pointer_kind());
729 let delta = crate::common::drag_autoscroll::step(y, body_h, band);
730 if delta.abs() > 0.01 {
731 let max = max_scroll_for_tick.get();
732 let new_y = (scroll_for_tick.get() + delta).clamp(0.0, max);
733 scroll_for_tick.set(new_y);
734 }
735 });
736 }
737
738 handlers = self.export.install_completion(handlers);
748
749 ctx.apply_self_handlers(handlers);
750
751 self.header_row_id = None;
754 self.body_pane_id = None;
755 self.scrollbar_id = None;
756 self.h_scrollbar_id = None;
757 self.empty_id = None;
758
759 if self.show_header {
761 *self.resize_state.borrow_mut() = None;
765 self.resize_target.set(None);
766 self.resize_preview_x.set(None);
767
768 let boundaries = *self.pane_boundaries.borrow();
769 let stretched_slot = self
772 .stretch_last_column
773 .then(|| display_indices.len().saturating_sub(1));
774 let resize_columns: ColumnResizeTable = Rc::new(
775 display_indices
776 .iter()
777 .enumerate()
778 .map(|(slot, &i)| {
779 let c = &self.columns[i];
780 ColumnResizeInfo {
781 id: c.id.clone(),
782 min_width: c.min_width.unwrap_or(cp::MIN_COLUMN_WIDTH_DEFAULT),
783 max_width: c.max_width,
784 resizable: c.resizable && stretched_slot != Some(slot),
785 flex: matches!(c.width, crate::ColumnWidth::Flex(_)),
786 }
787 })
788 .collect(),
789 );
790 let mut cell_ids: Vec<WidgetId> = Vec::with_capacity(display_indices.len());
791 let active_sort = self.sort_signal.get();
792 let cell_padding_horizontal =
793 crate::styles::recipe_table_style::resolve_table_style(ctx)
794 .cell_padding_horizontal(&ctx.theme().input);
795 for (display_pos, &col_idx) in display_indices.iter().enumerate() {
796 let col = &self.columns[col_idx];
797 let current_sort = active_sort
798 .as_ref()
799 .and_then(|(id, dir)| if id == &col.id { Some(*dir) } else { None });
800 let filter_zone_width = cp::FILTER_INDICATOR_SIZE + cell_padding_horizontal;
804 let cell = HeaderCell::new(HeaderCellSpec {
805 col_id: col.id.clone(),
806 label: col.header_label.resolve_now(),
807 col_index_1based: display_pos + 1,
808 sortable: col.sortable,
809 reorderable: col.reorderable,
810 filterable: col.filterable,
811 resize_grip: cp::RESIZE_HANDLE_WIDTH,
812 filter_zone_width,
813 current_sort,
814 width_index: display_pos,
815 pane_boundaries: boundaries,
816 resize_columns: resize_columns.clone(),
817 resize_policy: self.column_resize_policy,
818 resize_state: self.resize_state.clone(),
819 resize_target: self.resize_target.clone(),
820 resize_preview_x: self.resize_preview_x.clone(),
821 table_id: self.table_id,
822 sort_signal: self.sort_signal.clone(),
823 column_widths_signal: self.column_widths_signal.clone(),
824 column_widths: self.column_widths.clone(),
825 filters_signal: self.filters_signal.clone(),
826 });
827 cell_ids.push(ctx.add(cell));
828 }
829 let header_row = HeaderRow::new(
830 cell_ids,
831 self.column_widths.clone(),
832 cp::GRID_LINE_THICKNESS,
833 *self.pane_boundaries.borrow(),
834 self.scroll_x.clone(),
835 self.column_widths_signal.clone(),
836 );
837 let header_row_id = ctx.add(header_row);
847 attach_header_reorder_handlers(
848 ctx,
849 header_row_id,
850 self.table_id,
851 self.column_widths.clone(),
852 self.display_indices.clone(),
853 self.pane_boundaries.clone(),
854 self.column_order_signal.clone(),
855 self.column_pinning_signal.clone(),
856 self.columns.iter().map(|c| c.id.clone()).collect(),
857 self.header_strip_width.clone(),
858 self.scroll_x.clone(),
859 );
860 self.header_row_id = Some(header_row_id);
861 }
862
863 let row_count = self.source.visible_count();
868
869 let (vis_start, vis_end) = self.visible_range();
877 (self.source.dnd.request_window_fn)(vis_start..vis_end);
878 if (self.source.dnd.can_fetch_more_fn)() && vis_end + BUFFER_ROWS >= row_count {
879 (self.source.dnd.fetch_more_fn)();
880 }
881
882 if row_count > 0 {
883 let pane = body_pane::TreeBodyPane::<T> {
884 source: self.source.clone(),
885 editing_anchor: self.editing_anchor.clone(),
886 columns: self.columns.clone(),
887 display_indices: self.display_indices.clone(),
888 column_widths: self.column_widths.clone(),
889 pane_boundaries: *self.pane_boundaries.borrow(),
890 scroll_x: self.scroll_x.clone(),
891 tree_display_pos,
892 indent_per_level,
893 row_metrics: self.row_metrics.clone(),
894 selection_mode: self.selection_mode,
895 selection: self.row_selection.clone(),
896 cell_selection: self.cell_selection.clone(),
897 scroll_y: self.scroll_y.clone(),
898 viewport_height: self.viewport_height.clone(),
899 editing_cell: self.editing_cell.clone(),
900 focused_cell: self.focused_cell.clone(),
901 reorderable: self.reorderable,
902 reorder_perform: reorder_perform.clone(),
903 reparent_perform: reparent_perform.clone(),
904 model_id: self.model_id,
905 export: self.export.clone(),
906 drag_anchor: ctx.self_id(),
907 on_row_activate: self.on_row_activate.clone(),
908 activate_on: self.activate_on,
909 edit_triggers: self.edit_triggers,
910 on_cell_edit_request: self.on_cell_edit_request.clone(),
911 on_cell_edit_dismissed: self.on_cell_edit_dismissed.clone(),
912 version: self.pane_version.clone(),
913 prev_built_start: self.pane_built_start.clone(),
914 prev_built_end: self.pane_built_end.clone(),
915 total_refresh: self.pane_total_refresh.clone(),
916 row_entries: Vec::new(),
917 row_roots: Vec::new(),
918 row_map: self.row_map.clone(),
919 cell_map: self.cell_map.clone(),
920 };
921 self.body_pane_id = Some(ctx.add(pane));
922 if let Some(handlers) = crate::table_view::body_pane::root_edit_dismiss_handler(
926 &self.on_cell_edit_dismissed,
927 &self.editing_cell,
928 &Rc::new(
929 display_indices
930 .iter()
931 .map(|&i| self.columns[i].id.clone())
932 .collect::<Vec<_>>(),
933 ),
934 ) {
935 ctx.apply_self_handlers(handlers);
936 }
937 } else if let Some(ref f) = self.empty_view {
938 self.empty_id = Some(ctx.add_boxed(f()));
940 }
941
942 if self.show_internal_scrollbars {
944 let sb = ScrollBar::new(
945 ScrollBarOrientation::Vertical,
946 self.scroll_y.clone(),
947 self.max_scroll_y.clone(),
948 self.viewport_ratio_y.clone(),
949 )
950 .visual(match self.scroll_bar_style {
951 ScrollBarMode::Permanent => ScrollBarVisual::Permanent,
952 ScrollBarMode::Overlay => ScrollBarVisual::Overlay,
953 ScrollBarMode::Thin => ScrollBarVisual::Thin,
954 });
955 self.scrollbar_id = Some(ctx.add(sb));
956
957 let hsb = ScrollBar::new(
959 ScrollBarOrientation::Horizontal,
960 self.scroll_x.clone(),
961 self.max_scroll_x.clone(),
962 self.viewport_ratio_x.clone(),
963 )
964 .visual(match self.scroll_bar_style {
965 ScrollBarMode::Permanent => ScrollBarVisual::Permanent,
966 ScrollBarMode::Overlay => ScrollBarVisual::Overlay,
967 ScrollBarMode::Thin => ScrollBarVisual::Thin,
968 });
969 self.h_scrollbar_id = Some(ctx.add(hsb));
970 }
971
972 let mut children: Vec<WidgetId> = Vec::new();
976 if let Some(id) = self.body_pane_id {
977 children.push(id);
978 }
979 if let Some(id) = self.empty_id {
980 children.push(id);
981 }
982 if let Some(id) = self.scrollbar_id {
983 children.push(id);
984 }
985 if let Some(id) = self.h_scrollbar_id {
986 children.push(id);
987 }
988 if let Some(id) = self.header_row_id {
989 children.push(id);
990 }
991 let _ = (header_h, row_h);
992 children
993 }
994
995 fn layout_response(
996 &self,
997 proposal: SizeProposal,
998 _ctx: &LayoutContext,
999 ) -> teksilo_core::widget::LayoutResponse {
1000 let size = crate::common::viewport::viewport_size(
1004 proposal,
1005 &self.viewport_height,
1006 Size::new(400.0, 300.0),
1007 );
1008 if proposal.height.is_some() {
1009 self.laid_out.set(true);
1013 }
1014 size.into()
1015 }
1016
1017 fn place_children(
1018 &self,
1019 bounds: Rect,
1020 _proposal: SizeProposal,
1021 children: &mut [WidgetPlacement],
1022 ctx: &LayoutContext,
1023 ) {
1024 self.scroller
1028 .borrow_mut()
1029 .set_viewport(teksilo_canvas::Vec2::new(bounds.width, bounds.height));
1030 if children.is_empty() {
1031 return;
1032 }
1033 let rtl = ctx.is_rtl();
1034 let header_h = self.effective_header_height();
1035 let body_height_provisional = (bounds.height - header_h).max(0.0);
1036
1037 let total_height = self
1041 .row_metrics
1042 .borrow_mut()
1043 .total_height(self.source.visible_count());
1044 let needs_v_scrollbar =
1045 self.show_internal_scrollbars && total_height > body_height_provisional + 0.5;
1046 let reserves_v_bar = needs_v_scrollbar && self.scroll_bar_style == ScrollBarMode::Permanent;
1049 let body_width = if reserves_v_bar {
1050 (bounds.width - SCROLLBAR_THICKNESS).max(0.0)
1051 } else {
1052 bounds.width
1053 };
1054 let band_left = if rtl && reserves_v_bar {
1058 bounds.x + SCROLLBAR_THICKNESS
1059 } else {
1060 bounds.x
1061 };
1062 let scrollbar_x = if rtl {
1063 bounds.x
1064 } else {
1065 bounds.x + bounds.width - SCROLLBAR_THICKNESS
1066 };
1067 self.header_strip_width.set(body_width);
1070
1071 let overrides = self.column_widths_signal.get();
1072 let display = self.display_indices.borrow().clone();
1073 let widths = layout::ColumnSolver::resolve_in_order(
1074 &self.columns,
1075 &display,
1076 body_width,
1077 cp::MIN_COLUMN_WIDTH_DEFAULT,
1078 &overrides,
1079 self.stretch_last_column,
1080 );
1081
1082 let boundaries = *self.pane_boundaries.borrow();
1084 let (leading_w, middle_content_w, trailing_w) = layout::pane_widths(&widths, boundaries);
1085 let middle_viewport_w = (body_width - leading_w - trailing_w).max(0.0);
1086 let max_x = (middle_content_w - middle_viewport_w).max(0.0);
1087 self.max_scroll_x.set(max_x);
1088 self.middle_viewport_width.set(middle_viewport_w);
1089 let x_ratio = if middle_content_w > 0.0 {
1090 (middle_viewport_w / middle_content_w).clamp(0.0, 1.0)
1091 } else {
1092 1.0
1093 };
1094 self.viewport_ratio_x.set(x_ratio);
1095 {
1096 let current = self.scroll_x.get();
1097 let clamped = current.clamp(0.0, max_x);
1098 if (clamped - current).abs() > 0.001 {
1099 self.scroll_x.set(clamped);
1100 }
1101 }
1102
1103 *self.column_widths.borrow_mut() = widths;
1104
1105 let needs_h_scrollbar = self.show_internal_scrollbars && max_x > 0.5;
1106 let reserves_h_bar = needs_h_scrollbar && self.scroll_bar_style == ScrollBarMode::Permanent;
1107 let body_height = if reserves_h_bar {
1108 (body_height_provisional - SCROLLBAR_THICKNESS).max(0.0)
1109 } else {
1110 body_height_provisional
1111 };
1112
1113 let max_y = (total_height - body_height).max(0.0);
1114 self.max_scroll_y.set(max_y);
1115 let y_ratio = if total_height > 0.0 {
1116 (body_height / total_height).clamp(0.0, 1.0)
1117 } else {
1118 1.0
1119 };
1120 self.viewport_ratio_y.set(y_ratio);
1121 self.clamp_scroll();
1122
1123 let body_origin_y = bounds.y + header_h;
1124 self.body_bounds
1126 .set(Rect::new(band_left, body_origin_y, body_width, body_height));
1127
1128 let mut next = 0;
1129
1130 if self.body_pane_id.is_some() {
1133 if let Some(child) = children.get_mut(next) {
1134 child.origin = Point::new(band_left, body_origin_y);
1135 child.size = Size::new(body_width, body_height);
1136 }
1137 next += 1;
1138 }
1139
1140 if self.empty_id.is_some() {
1142 if let Some(child) = children.get_mut(next) {
1143 child.origin = Point::new(band_left, body_origin_y);
1144 child.size = Size::new(body_width, body_height);
1145 }
1146 next += 1;
1147 }
1148
1149 if self.scrollbar_id.is_some() {
1151 if let Some(child) = children.get_mut(next) {
1152 if needs_v_scrollbar {
1153 child.origin = Point::new(scrollbar_x, body_origin_y);
1154 child.size = Size::new(SCROLLBAR_THICKNESS, body_height);
1155 } else {
1156 child.origin = bounds.origin();
1157 child.size = Size::ZERO;
1158 }
1159 }
1160 next += 1;
1161 }
1162
1163 if self.h_scrollbar_id.is_some() {
1165 if let Some(child) = children.get_mut(next) {
1166 if needs_h_scrollbar {
1167 let h_x = if rtl {
1168 band_left + trailing_w
1169 } else {
1170 band_left + leading_w
1171 };
1172 child.origin = Point::new(h_x, body_origin_y + body_height);
1173 child.size = Size::new(middle_viewport_w, SCROLLBAR_THICKNESS);
1174 } else {
1175 child.origin = bounds.origin();
1176 child.size = Size::ZERO;
1177 }
1178 }
1179 next += 1;
1180 }
1181
1182 if self.header_row_id.is_some()
1185 && let Some(child) = children.get_mut(next)
1186 {
1187 child.origin = Point::new(band_left, bounds.y);
1188 child.size = Size::new(body_width, header_h);
1189 }
1190 }
1191
1192 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
1193 let header_h = self.effective_header_height();
1194 let colors = &ctx.theme.colors;
1195 let scroll_y = self.scroll_y.get();
1196 let body_origin_y = bounds.y + header_h;
1197 let body_height = (bounds.height - header_h).max(0.0);
1198 let widths = self.column_widths.borrow();
1199 let body_width = widths.iter().sum::<f32>();
1200 let body_width_for_paint = if body_width > 0.0 {
1201 body_width.min(bounds.width)
1202 } else {
1203 bounds.width
1204 };
1205 let rtl = ctx.layout_direction == teksilo_core::environment::LayoutDirection::RightToLeft;
1207 let content_left = if rtl {
1208 bounds.x + bounds.width - body_width_for_paint
1209 } else {
1210 bounds.x
1211 };
1212
1213 let row_count = self.source.visible_count();
1216 let (first_visible, last_visible) =
1217 self.row_metrics
1218 .borrow_mut()
1219 .visible_range(scroll_y, body_height, row_count, 0);
1220
1221 canvas.set_clip(Rect::new(
1227 content_left,
1228 body_origin_y,
1229 body_width_for_paint,
1230 body_height,
1231 ));
1232
1233 if self.alternating_rows {
1234 let mut m = self.row_metrics.borrow_mut();
1235 for row_idx in first_visible..last_visible {
1236 if row_idx % 2 == 1 {
1237 let y = body_origin_y + m.row_top(row_idx) - scroll_y;
1238 let h = m.row_height(row_idx);
1239 let rect = Rect::new(content_left, y, body_width_for_paint, h);
1240 canvas.fill_rect(rect, SurfaceRole::AltRow.resolve(colors));
1241 }
1242 }
1243 }
1244
1245 if let Some(ref sel) = self.row_selection
1246 && matches!(
1247 self.selection_mode,
1248 TableSelectionMode::SingleRow | TableSelectionMode::MultiRow
1249 )
1250 {
1251 let bg = if self.view_focused.get() && ctx.window_active {
1255 SurfaceRole::Selected.resolve(colors)
1256 } else {
1257 SurfaceRole::SelectedInactive.resolve(colors)
1258 };
1259 let mut m = self.row_metrics.borrow_mut();
1260 for row_idx in sel.selected_indices() {
1261 let y = body_origin_y + m.row_top(row_idx) - scroll_y;
1262 let h = m.row_height(row_idx);
1263 if y + h < body_origin_y || y > body_origin_y + body_height {
1264 continue;
1265 }
1266 let rect = Rect::new(content_left, y, body_width_for_paint, h);
1267 canvas.fill_rect(rect, bg);
1268 }
1269 }
1270
1271 let line_color = BorderRole::Divider.resolve(colors);
1272 let line_w = cp::GRID_LINE_THICKNESS.max(1.0);
1273 if matches!(self.grid_lines, GridLines::Horizontal | GridLines::Both) {
1274 let mut m = self.row_metrics.borrow_mut();
1275 for row_idx in first_visible..last_visible {
1276 let bottom = m.row_top(row_idx) + m.row_height(row_idx);
1277 let y = body_origin_y + bottom - scroll_y - line_w;
1278 let rect = Rect::new(content_left, y, body_width_for_paint, line_w);
1279 canvas.fill_rect(rect, line_color);
1280 }
1281 }
1282
1283 let boundaries = *self.pane_boundaries.borrow();
1286 let scroll_x = self.scroll_x.get();
1287 let content_bounds = Rect::new(
1288 content_left,
1289 body_origin_y,
1290 body_width_for_paint,
1291 body_height,
1292 );
1293 let (leading_rect, middle_rect, trailing_rect) =
1294 layout::band_rects(content_bounds, &widths, boundaries, rtl);
1295
1296 if matches!(self.grid_lines, GridLines::Vertical | GridLines::Both) {
1297 let leading_end = boundaries.leading_count.min(widths.len());
1298 let middle_end = boundaries.middle_end.min(widths.len()).max(leading_end);
1299 crate::table_view::draw_pane_dividers(
1300 canvas,
1301 leading_rect,
1302 &widths[..leading_end],
1303 0.0,
1304 rtl,
1305 line_color,
1306 line_w,
1307 );
1308 crate::table_view::draw_pane_dividers(
1309 canvas,
1310 middle_rect,
1311 &widths[leading_end..middle_end],
1312 scroll_x,
1313 rtl,
1314 line_color,
1315 line_w,
1316 );
1317 crate::table_view::draw_pane_dividers(
1318 canvas,
1319 trailing_rect,
1320 &widths[middle_end..],
1321 0.0,
1322 rtl,
1323 line_color,
1324 line_w,
1325 );
1326 }
1327
1328 if self.view_focused.get()
1331 && self.focus_visible.get()
1332 && let Some((focus_row, focus_col)) = self.focused_cell.get()
1333 && focus_col < widths.len()
1334 && let Some(x_off) = layout::column_logical_x(
1335 &widths,
1336 boundaries,
1337 scroll_x,
1338 body_width_for_paint,
1339 focus_col,
1340 )
1341 {
1342 let cell_w = widths[focus_col];
1343 let (focus_top, focus_h) = {
1344 let mut m = self.row_metrics.borrow_mut();
1345 (m.row_top(focus_row), m.row_height(focus_row))
1346 };
1347 let y = body_origin_y + focus_top - scroll_y;
1348 if y + focus_h >= body_origin_y && y <= body_origin_y + body_height {
1349 let pane_rect = if focus_col < boundaries.leading_count {
1350 leading_rect
1351 } else if focus_col >= boundaries.middle_end {
1352 trailing_rect
1353 } else {
1354 middle_rect
1355 };
1356 canvas.set_clip(pane_rect);
1357 let inset = cp::FOCUS_RING_INSET;
1358 let stroke = cp::GRID_LINE_THICKNESS.max(1.5);
1359 let ring_color = BorderRole::Focused.resolve(colors);
1360 let rx = if rtl {
1361 content_left + body_width_for_paint - x_off - cell_w + inset
1362 } else {
1363 content_left + x_off + inset
1364 };
1365 let ry = y + inset;
1366 let rw = (cell_w - inset * 2.0).max(0.0);
1367 let rh = (focus_h - inset * 2.0).max(0.0);
1368 canvas.fill_rect(Rect::new(rx, ry, rw, stroke), ring_color);
1369 canvas.fill_rect(Rect::new(rx, ry + rh - stroke, rw, stroke), ring_color);
1370 canvas.fill_rect(Rect::new(rx, ry, stroke, rh), ring_color);
1371 canvas.fill_rect(Rect::new(rx + rw - stroke, ry, stroke, rh), ring_color);
1372 canvas.clear_clip();
1373 }
1374 }
1375
1376 let drop_indent_origin = |depth: usize| -> f32 {
1387 let step = self.effective_indent();
1388 let tree_decl = self.tree_column_decl_index();
1389 let tree_slot = self
1390 .display_indices
1391 .borrow()
1392 .iter()
1393 .position(|&i| i == tree_decl)
1394 .unwrap_or(0);
1395 let col_x = layout::column_logical_x(
1396 &widths,
1397 boundaries,
1398 scroll_x,
1399 body_width_for_paint,
1400 tree_slot,
1401 )
1402 .unwrap_or(0.0);
1403 (col_x + depth as f32 * step).clamp(0.0, body_width_for_paint)
1404 };
1405 match self.drop_feedback.get() {
1406 Some(DropViz::Line { y, depth, .. }) => {
1407 let recipe = ctx
1408 .theme
1409 .style_slots
1410 .list_container
1411 .as_ref()
1412 .map(|s| s.insertion())
1413 .unwrap_or_default();
1414 let line_color = recipe.role.resolve(colors);
1415 let thickness = recipe.thickness;
1416 let line_y = body_origin_y + y - thickness * 0.5;
1417 let indent = drop_indent_origin(depth);
1418 let x = if rtl {
1421 content_left
1422 } else {
1423 content_left + indent
1424 };
1425 canvas.fill_rect(
1426 Rect::new(x, line_y, body_width_for_paint - indent, thickness),
1427 line_color,
1428 );
1429 }
1430 Some(DropViz::Rect {
1435 top, height, depth, ..
1436 }) => {
1437 let into = ctx
1438 .theme
1439 .style_slots
1440 .list_container
1441 .as_ref()
1442 .map(|s| s.drop_into())
1443 .unwrap_or_default();
1444 let color = into.role.resolve(colors);
1445 let indent = drop_indent_origin(depth);
1446 let x = if rtl {
1447 content_left
1448 } else {
1449 content_left + indent
1450 };
1451 let rect = Rect::new(
1452 x + into.inset,
1453 body_origin_y + top + into.inset,
1454 (body_width_for_paint - indent - into.inset * 2.0).max(0.0),
1455 (height - into.inset * 2.0).max(0.0),
1456 );
1457 let radius = teksilo_tokens::CornerRadius::uniform(into.corner_radius);
1458 canvas.fill_rounded_rect(rect, radius, color.with_alpha(into.fill_alpha));
1459 canvas.stroke_rounded_rect(rect, radius, color, into.thickness);
1460 }
1461 None => {}
1462 }
1463
1464 canvas.clear_clip();
1465
1466 let nothing_indicated = self.focused_cell.get().is_none()
1470 && self
1471 .row_selection
1472 .as_ref()
1473 .is_none_or(|s| s.selected_indices().is_empty())
1474 && self.cell_selection.as_ref().is_none_or(|s| s.count() == 0);
1475 if self.view_focused.get() && self.focus_visible.get() && nothing_indicated {
1476 let inset = 1.0_f32;
1477 let rect = Rect::new(
1478 bounds.x + inset,
1479 bounds.y + inset,
1480 (bounds.width - inset * 2.0).max(0.0),
1481 (bounds.height - inset * 2.0).max(0.0),
1482 );
1483 canvas.stroke_rect(rect, BorderRole::Focused.resolve(colors), 1.5);
1484 }
1485
1486 if let Some(x) = self.resize_preview_x.get() {
1488 let thickness = cp::GRID_LINE_THICKNESS.max(1.5);
1489 canvas.fill_rect(
1490 Rect::new(x - thickness * 0.5, bounds.y, thickness, bounds.height),
1491 BorderRole::Focused.resolve(colors),
1492 );
1493 }
1494 }
1495
1496 fn context_menu_key_target(&self) -> Option<WidgetId> {
1510 let index = self.focused_cell.get().map(|(row, _col)| row).or_else(|| {
1511 self.row_selection
1512 .as_ref()
1513 .and_then(|s| s.selected_indices().first().copied())
1514 })?;
1515 let map = self.row_map.borrow();
1516 map.iter().find(|(i, _)| *i == index).map(|(_, id)| *id)
1517 }
1518
1519 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
1520 builder.set_role(teksilo_core::accesskit::Role::TreeGrid);
1521 if self
1534 .row_selection
1535 .as_ref()
1536 .is_some_and(|selection| selection.mode() == teksilo_data::SelectionMode::Multi)
1537 {
1538 builder.set_multiselectable(true);
1539 }
1540
1541 if let Some(ref label) = self.a11y_label {
1542 builder.set_name(label.resolve_now());
1543 }
1544 let row_count = self.source.visible_count() + if self.show_header { 1 } else { 0 };
1545 let col_count = self.columns.len();
1546 let n = builder.inner_mut();
1547 n.set_row_count(row_count);
1548 n.set_column_count(col_count);
1549
1550 if let Some((row, col)) = self.focused_cell.get()
1557 && let Some(cell_id) = self.realized_cell(row, col)
1558 {
1559 builder.set_active_descendant(widget_id_to_node_id(cell_id));
1560 }
1561 }
1562
1563 fn as_any(&self) -> Option<&dyn std::any::Any> {
1564 Some(self)
1565 }
1566
1567 fn children(&self) -> Vec<WidgetId> {
1568 let mut out: Vec<WidgetId> = Vec::new();
1571 if let Some(id) = self.body_pane_id {
1572 out.push(id);
1573 }
1574 if let Some(id) = self.empty_id {
1575 out.push(id);
1576 }
1577 if let Some(id) = self.scrollbar_id {
1578 out.push(id);
1579 }
1580 if let Some(id) = self.h_scrollbar_id {
1581 out.push(id);
1582 }
1583 if let Some(id) = self.header_row_id {
1584 out.push(id);
1585 }
1586 out
1587 }
1588
1589 fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
1590 let out: Vec<WidgetId> = [
1594 self.header_row_id,
1595 self.body_pane_id,
1596 self.empty_id,
1597 self.scrollbar_id,
1598 self.h_scrollbar_id,
1599 ]
1600 .into_iter()
1601 .flatten()
1602 .collect();
1603 if out.is_empty() { None } else { Some(out) }
1604 }
1605
1606 fn clips_children(&self) -> bool {
1607 true
1608 }
1609}