1pub(crate) mod a11y;
29pub(crate) mod body_pane;
30pub(crate) mod drag;
31pub(crate) mod keyboard;
32pub mod layout;
33pub mod sections;
34pub(crate) mod selection;
35#[cfg(test)]
36mod tests;
37
38use std::cell::Cell;
39use std::collections::BTreeSet;
40use std::rc::Rc;
41
42use teksilo_canvas::{EdgeInsets, Point, Rect, Size, SizeProposal};
43use teksilo_core::accessibility::{AccessNodeBuilder, widget_id_to_node_id};
44use teksilo_core::binding::BindingLevel;
45use teksilo_core::build_context::BuildContext;
46use teksilo_core::drag_payload::DragPayload;
47use teksilo_core::event::{EventResponse, ScrollDelta, WidgetEvent};
48use teksilo_core::signal::{Prop, Signal};
49use teksilo_core::styles::GridViewStyle;
50use teksilo_core::widget::{LayoutContext, PaintContext, Widget, WidgetPlacement};
51use teksilo_core::widget_builder::HandlerSet;
52use teksilo_core::widget_id::WidgetId;
53use teksilo_data::{
54 DataChange, DropPosition, DropResponse, ListModel, SelectionMode, SelectionModel,
55};
56use teksilo_tokens::{Easing, SurfaceRole};
57
58use std::time::Duration;
59
60use crate::common::scroll::OverscrollBehavior;
61use crate::data_views::{DragTransferMode, RowDragData, ViewId, ViewKind, flat_insertion_target};
62use crate::list_source::ListSource;
63use crate::primitives::TextWidget;
64use crate::scroll_area::ScrollBarMode;
65use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVisual};
66
67use body_pane::{GridBodyPane, TileDelegate};
68use keyboard::{GridKeyConfig, build_grid_key_handler};
69use layout::masonry::VirtualizedMasonry;
70use layout::sectioned::SectionedGrid;
71use layout::strategy::{GridLayoutStrategy, TileRect};
72use layout::uniform::UniformGrid;
73use layout::variable_row::VariableRowGrid;
74use sections::{SectionData, SectionProvider};
75use selection::{MarqueeConfig, MarqueeState, build_marquee_handler};
76
77pub use sections::{GroupingSections, SectionProvider as GridSectionProvider, grouping_sections};
78
79#[derive(Debug, Clone, Copy)]
81enum StrategyKind {
82 Uniform,
84 VariableRow { estimated: f32 },
86 Waterfall { estimated: f32 },
88}
89
90pub use keyboard::GridTabTraversal;
91pub use layout::{GridSizing, ScrollAnchor};
92
93type CanAcceptFn = Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> DropResponse>;
95
96fn drop_allowed<T: 'static>(
101 can_accept: &CanAcceptFn,
102 payload: &DragPayload,
103 idx: usize,
104 len: usize,
105 view_id: ViewId,
106 has_drop_cb: bool,
107 export: &crate::data_views::RowExport<T>,
108) -> bool {
109 match flat_insertion_target(idx, len) {
110 Some((target, position)) => match (can_accept)(payload, target, position, view_id) {
111 DropResponse::Accept | DropResponse::Redirect(_) => true,
112 DropResponse::Reject => {
113 let foreign = is_foreign::<T>(payload, view_id);
114 foreign && (has_drop_cb || export.accepts_foreign_export(payload, view_id))
115 }
116 },
117 None => false,
118 }
119}
120
121fn is_foreign<T: 'static>(payload: &DragPayload, view_id: ViewId) -> bool {
125 payload
126 .get_typed::<RowDragData<T>>()
127 .is_none_or(|rd| rd.source != view_id)
128}
129
130const SCROLLBAR_THICKNESS: f32 = 12.0;
132
133pub struct TileContext<'a, T: 'static> {
141 pub index: usize,
143 pub row: usize,
145 pub col: usize,
147 pub item: &'a T,
149 pub is_selected: bool,
151 pub is_focused: bool,
156}
157
158pub struct GridView<T: 'static> {
160 source: ListSource<T>,
161 delegate: TileDelegate<T>,
162
163 sizing: GridSizing,
168 sizing_signal: Option<Signal<GridSizing>>,
172 col_gap: f32,
173 row_gap: f32,
174 inset: EdgeInsets,
175 strategy_kind: StrategyKind,
176 #[allow(clippy::type_complexity)]
178 exact_item_height: Option<Rc<dyn Fn(usize) -> f32>>,
179 strategy: Option<Rc<dyn GridLayoutStrategy>>,
182
183 selection: Option<SelectionModel>,
185 #[allow(clippy::type_complexity)]
186 on_selection_changed: Option<Rc<dyn Fn(&BTreeSet<usize>)>>,
187 focused_index: Signal<Option<usize>>,
188 marquee_selection: bool,
190 marquee: Signal<Option<MarqueeState>>,
191
192 wrap_navigation: bool,
194 tab_traversal: GridTabTraversal,
195
196 show_scrollbar: bool,
198 overscroll_behavior: OverscrollBehavior,
199 smooth_scrolling: bool,
202 smooth_scroll_duration: Duration,
204 scroll_bar_style: ScrollBarMode,
207 scroll_y: Signal<f32>,
208 max_scroll_y: Signal<f32>,
209 viewport_ratio_y: Signal<f32>,
210 column_count: Signal<usize>,
214
215 reorderable: bool,
217 #[allow(clippy::type_complexity)]
218 on_item_drop: Option<
219 Rc<
220 dyn Fn(
221 teksilo_core::drag_payload::DragPayload,
222 usize,
223 &mut teksilo_core::widget::EventContext,
224 ) -> bool,
225 >,
226 >,
227 insertion: Signal<Option<usize>>,
229 model_id: ViewId,
232
233 export: crate::data_views::RowExport<T>,
238
239 #[allow(clippy::type_complexity)]
241 on_tile_activate: Option<Rc<dyn Fn(usize, &mut teksilo_core::widget::EventContext)>>,
242 activate_on: crate::data_views::ActivateOn,
245 #[allow(clippy::type_complexity)]
246 tile_context_menu: Option<
247 Rc<
248 dyn Fn(
249 usize,
250 Point,
251 &mut teksilo_core::widget::EventContext,
252 ) -> Option<Box<dyn Widget>>,
253 >,
254 >,
255 type_ahead_timeout: std::time::Duration,
256 #[allow(clippy::type_complexity)]
257 type_ahead_label: Option<Rc<dyn Fn(usize) -> String>>,
258 #[allow(clippy::type_complexity)]
262 tile_a11y_label: Option<Rc<dyn Fn(usize) -> String>>,
263
264 #[allow(clippy::type_complexity)]
266 empty_view: Option<Rc<dyn Fn() -> Box<dyn Widget>>>,
267 #[allow(clippy::type_complexity)]
268 loading_view: Option<Rc<dyn Fn() -> Box<dyn Widget>>>,
269 is_loading: Option<Prop<bool>>,
270 loading_id: Option<WidgetId>,
271
272 section_data: Option<SectionData>,
274 #[allow(clippy::type_complexity)]
275 header_delegate: Option<Rc<dyn Fn(usize, &str) -> Box<dyn Widget>>>,
276 header_height: f32,
277 pinned_section_headers: bool,
278 current_section: Signal<usize>,
279 pinned_header_id: Option<WidgetId>,
280
281 a11y_label: Option<String>,
283 tile_map: Rc<std::cell::RefCell<Vec<(usize, WidgetId)>>>,
286
287 style: Option<Rc<dyn GridViewStyle>>,
290
291 viewport_width: Rc<Cell<f32>>,
293 viewport_height: Rc<Cell<f32>>,
294 viewport_origin: Rc<Cell<Option<Point>>>,
299 last_needs_scrollbar: Cell<bool>,
303
304 body_pane_id: Option<WidgetId>,
306 empty_id: Option<WidgetId>,
307 scrollbar_id: Option<WidgetId>,
308 overlay_id: Option<WidgetId>,
309
310 enabled: Prop<bool>,
315}
316
317impl<T: 'static> GridView<T> {
318 pub fn new(
321 model: ListModel<T>,
322 delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static,
323 ) -> Self {
324 Self::create(ListSource::from_model(model), delegate)
325 }
326
327 pub fn from_source<S: teksilo_data::ListDataSource<Item = T>>(
329 source: S,
330 delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static,
331 ) -> Self {
332 Self::create(ListSource::from_data_source(source), delegate)
333 }
334
335 fn create(
336 source: ListSource<T>,
337 delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static,
338 ) -> Self {
339 Self {
340 source,
341 delegate: Rc::new(delegate),
342 sizing: GridSizing::Adaptive {
343 min_width: 120.0,
344 max_width: None,
345 height: 120.0,
346 },
347 sizing_signal: None,
348 col_gap: 8.0,
349 row_gap: 8.0,
350 inset: EdgeInsets::ZERO,
351 strategy_kind: StrategyKind::Uniform,
352 exact_item_height: None,
353 strategy: None,
354 selection: None,
355 on_selection_changed: None,
356 focused_index: Signal::new(None),
357 marquee_selection: true,
358 marquee: Signal::new(None),
359 wrap_navigation: false,
360 tab_traversal: GridTabTraversal::OutOfGrid,
361 show_scrollbar: true,
362 overscroll_behavior: OverscrollBehavior::default(),
363 smooth_scrolling: true,
364 smooth_scroll_duration: Duration::from_millis(150),
365 scroll_bar_style: ScrollBarMode::Permanent,
366 scroll_y: Signal::new_animated(0.0),
367 max_scroll_y: Signal::new(0.0),
368 viewport_ratio_y: Signal::new(1.0),
369 column_count: Signal::new(1),
370 reorderable: false,
371 on_item_drop: None,
372 insertion: Signal::new(None),
373 model_id: ViewId::next(ViewKind::Grid),
374 export: crate::data_views::RowExport::default(),
375 on_tile_activate: None,
376 activate_on: crate::data_views::ActivateOn::default(),
377 tile_context_menu: None,
378 type_ahead_timeout: std::time::Duration::from_millis(500),
379 type_ahead_label: None,
380 tile_a11y_label: None,
381 empty_view: None,
382 loading_view: None,
383 is_loading: None,
384 loading_id: None,
385 section_data: None,
386 header_delegate: None,
387 header_height: 28.0,
388 pinned_section_headers: false,
389 current_section: Signal::new(0),
390 pinned_header_id: None,
391 a11y_label: None,
392 tile_map: Rc::new(std::cell::RefCell::new(Vec::new())),
393 style: None,
394 viewport_width: Rc::new(Cell::new(400.0)),
395 viewport_height: Rc::new(Cell::new(400.0)),
396 viewport_origin: Rc::new(Cell::new(None)),
397 last_needs_scrollbar: Cell::new(false),
398 body_pane_id: None,
399 empty_id: None,
400 scrollbar_id: None,
401 overlay_id: None,
402 enabled: Prop::Static(true),
403 }
404 }
405
406 pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self {
409 self.enabled = enabled.into();
410 self
411 }
412
413 pub fn sizing(mut self, sizing: impl Into<Prop<GridSizing>>) -> Self {
425 let sig = sizing.into().as_signal();
426 self.sizing = sig.get();
427 self.sizing_signal = Some(sig);
428 self
429 }
430
431 pub fn tile_size(mut self, width: f32, height: f32) -> Self {
433 self.sizing = GridSizing::Fixed { width, height };
434 self.sizing_signal = None;
435 self
436 }
437
438 pub fn column_count(mut self, count: usize, tile_height: f32) -> Self {
440 self.sizing = GridSizing::FixedColumnCount {
441 count,
442 height: tile_height,
443 };
444 self.sizing_signal = None;
445 self
446 }
447
448 pub fn variable_row_heights(mut self, estimated: f32) -> Self {
454 self.strategy_kind = StrategyKind::VariableRow {
455 estimated: estimated.max(1.0),
456 };
457 self
458 }
459
460 pub fn item_height(mut self, f: impl Fn(usize) -> f32 + 'static) -> Self {
465 self.exact_item_height = Some(Rc::new(f));
466 if matches!(self.strategy_kind, StrategyKind::Uniform) {
467 self.strategy_kind = StrategyKind::VariableRow {
468 estimated: self.sizing.tile_height().max(1.0),
469 };
470 }
471 self
472 }
473
474 pub fn waterfall(mut self, estimated: f32) -> Self {
480 self.strategy_kind = StrategyKind::Waterfall {
481 estimated: estimated.max(1.0),
482 };
483 self
484 }
485
486 pub fn column_spacing(mut self, spacing: f32) -> Self {
490 self.col_gap = spacing.max(0.0);
491 self
492 }
493
494 pub fn row_spacing(mut self, spacing: f32) -> Self {
496 self.row_gap = spacing.max(0.0);
497 self
498 }
499
500 pub fn spacing(mut self, spacing: f32) -> Self {
502 self.col_gap = spacing.max(0.0);
503 self.row_gap = spacing.max(0.0);
504 self
505 }
506
507 pub fn content_inset(mut self, inset: EdgeInsets) -> Self {
509 self.inset = inset;
510 self
511 }
512
513 pub fn selection(mut self, sel: SelectionModel) -> Self {
517 self.selection = Some(sel);
518 self
519 }
520
521 pub fn on_selection_changed(mut self, f: impl Fn(&BTreeSet<usize>) + 'static) -> Self {
524 self.on_selection_changed = Some(Rc::new(f));
525 self
526 }
527
528 pub fn marquee_selection(mut self, enabled: bool) -> Self {
531 self.marquee_selection = enabled;
532 self
533 }
534
535 pub fn wrap_navigation(mut self, enabled: bool) -> Self {
539 self.wrap_navigation = enabled;
540 self
541 }
542
543 pub fn tab_traversal(mut self, traversal: GridTabTraversal) -> Self {
545 self.tab_traversal = traversal;
546 self
547 }
548
549 pub fn show_scrollbar(mut self, show: bool) -> Self {
554 self.show_scrollbar = show;
555 self
556 }
557
558 pub fn overscroll_behavior(mut self, behavior: OverscrollBehavior) -> Self {
560 self.overscroll_behavior = behavior;
561 self
562 }
563
564 pub fn smooth_scrolling(mut self, enabled: bool) -> Self {
566 self.smooth_scrolling = enabled;
567 self
568 }
569
570 pub fn smooth_scroll_duration(mut self, duration: Duration) -> Self {
572 self.smooth_scroll_duration = duration;
573 self
574 }
575
576 pub fn scroll_bar_style(mut self, style: ScrollBarMode) -> Self {
580 self.scroll_bar_style = style;
581 self
582 }
583
584 pub fn scroll_y_signal(&self) -> &Signal<f32> {
586 &self.scroll_y
587 }
588
589 pub fn max_scroll_y_signal(&self) -> &Signal<f32> {
591 &self.max_scroll_y
592 }
593
594 pub fn viewport_ratio_y_signal(&self) -> &Signal<f32> {
596 &self.viewport_ratio_y
597 }
598
599 pub fn ensure_index_visible(&self, index: usize, anchor: ScrollAnchor) {
601 let Some(ref strategy) = self.strategy else {
602 return;
603 };
604 let delta = strategy.scroll_delta_to_reveal(
605 index,
606 self.scroll_y.get(),
607 self.viewport_height.get(),
608 self.viewport_width.get(),
609 anchor,
610 );
611 if delta.abs() > 0.01 {
612 let max = self.max_scroll_y.get();
613 self.scroll_y
614 .set((self.scroll_y.get() + delta).clamp(0.0, max));
615 }
616 }
617
618 pub fn scroll_to_index(&self, index: usize, anchor: ScrollAnchor) {
621 self.ensure_index_visible(index, anchor);
622 }
623
624 pub fn sections<P: SectionProvider>(mut self, provider: P) -> Self {
631 let provider = Rc::new(provider);
632 let counts_provider = provider.clone();
633 let title_provider = provider.clone();
634 self.section_data = Some(SectionData {
635 counts_fn: Rc::new(move || counts_provider.section_counts()),
636 title_fn: Rc::new(move |s| title_provider.section_title(s)),
637 });
638 self
639 }
640
641 pub fn section_header_delegate(
644 mut self,
645 f: impl Fn(usize, &str) -> Box<dyn Widget> + 'static,
646 ) -> Self {
647 self.header_delegate = Some(Rc::new(f));
648 self
649 }
650
651 pub fn section_header_height(mut self, height: f32) -> Self {
653 self.header_height = height.max(0.0);
654 self
655 }
656
657 pub fn pinned_section_headers(mut self, enabled: bool) -> Self {
660 self.pinned_section_headers = enabled;
661 self
662 }
663
664 pub fn a11y_label(mut self, label: impl Into<String>) -> Self {
666 self.a11y_label = Some(label.into());
667 self
668 }
669
670 pub fn style(mut self, style: impl GridViewStyle) -> Self {
674 self.style = Some(Rc::new(style));
675 self
676 }
677
678 #[allow(clippy::type_complexity)]
681 fn header_factory(&self) -> Option<Rc<dyn Fn(usize) -> Box<dyn Widget>>> {
682 let data = self.section_data.as_ref()?;
683 let title_fn = data.title_fn.clone();
684 let delegate = self.header_delegate.clone();
685 Some(Rc::new(move |section| {
686 let title = title_fn(section);
687 match &delegate {
688 Some(d) => d(section, &title),
689 None => Box::new(TextWidget::new(teksilo_i18n::lit!(title))) as Box<dyn Widget>,
690 }
691 }))
692 }
693
694 pub fn empty_view(mut self, f: impl Fn() -> Box<dyn Widget> + 'static) -> Self {
696 self.empty_view = Some(Rc::new(f));
697 self
698 }
699
700 pub fn loading_view(mut self, f: impl Fn() -> Box<dyn Widget> + 'static) -> Self {
702 self.loading_view = Some(Rc::new(f));
703 self
704 }
705
706 pub fn is_loading(mut self, flag: impl Into<Prop<bool>>) -> Self {
709 self.is_loading = Some(flag.into());
710 self
711 }
712
713 pub fn reorderable(mut self, enabled: bool) -> Self {
719 self.reorderable = enabled;
720 self
721 }
722
723 pub fn exportable(mut self, mode: DragTransferMode) -> Self
739 where
740 T: Clone,
741 {
742 self.export.set_exportable(mode);
743 self
744 }
745
746 pub fn export_external(mut self, f: impl Fn(&[T]) -> Vec<(String, Vec<u8>)> + 'static) -> Self
754 where
755 T: Clone,
756 {
757 self.export.set_export_external(f);
758 self
759 }
760
761 pub fn on_rows_transferred_out(
767 mut self,
768 f: impl Fn(&[usize], &mut teksilo_core::widget::EventContext) + 'static,
769 ) -> Self {
770 self.export.set_on_rows_transferred_out(f);
771 self
772 }
773
774 pub fn accept_foreign_rows(mut self, accept: bool) -> Self {
781 self.export.accept_foreign_rows = accept;
782 self
783 }
784
785 pub fn on_rows_received(
789 mut self,
790 f: impl Fn(Vec<T>, usize, &mut teksilo_core::widget::EventContext) + 'static,
791 ) -> Self {
792 self.export.set_on_rows_received(f);
793 self
794 }
795
796 pub fn on_item_drop(
799 mut self,
800 f: impl Fn(
801 teksilo_core::drag_payload::DragPayload,
802 usize,
803 &mut teksilo_core::widget::EventContext,
804 ) -> bool
805 + 'static,
806 ) -> Self {
807 self.on_item_drop = Some(Rc::new(f));
808 self
809 }
810
811 pub fn on_tile_activate(
817 mut self,
818 f: impl Fn(usize, &mut teksilo_core::widget::EventContext) + 'static,
819 ) -> Self {
820 self.on_tile_activate = Some(Rc::new(f));
821 self
822 }
823
824 pub fn activate_on(mut self, mode: crate::data_views::ActivateOn) -> Self {
828 self.activate_on = mode;
829 self
830 }
831
832 pub fn tile_context_menu(
835 mut self,
836 f: impl Fn(usize, Point, &mut teksilo_core::widget::EventContext) -> Option<Box<dyn Widget>>
837 + 'static,
838 ) -> Self {
839 self.tile_context_menu = Some(Rc::new(f));
840 self
841 }
842
843 pub fn type_ahead_label(mut self, f: impl Fn(usize) -> String + 'static) -> Self {
846 self.type_ahead_label = Some(Rc::new(f));
847 self
848 }
849
850 pub fn tile_a11y_label(mut self, f: impl Fn(usize) -> String + 'static) -> Self {
855 self.tile_a11y_label = Some(Rc::new(f));
856 self
857 }
858
859 pub fn type_ahead_timeout(mut self, timeout: std::time::Duration) -> Self {
861 self.type_ahead_timeout = timeout;
862 self
863 }
864
865 fn ensure_strategy(&mut self) -> Rc<dyn GridLayoutStrategy> {
870 if self.strategy.is_none() {
871 if let Some(ref data) = self.section_data {
873 let s: Rc<dyn GridLayoutStrategy> = Rc::new(SectionedGrid::new(
874 self.sizing,
875 self.col_gap,
876 self.row_gap,
877 self.inset,
878 self.header_height,
879 data.counts_fn.clone(),
880 ));
881 self.strategy = Some(s);
882 return self.strategy.as_ref().unwrap().clone();
883 }
884 let s: Rc<dyn GridLayoutStrategy> = match self.strategy_kind {
885 StrategyKind::Uniform => Rc::new(UniformGrid::new(
886 self.sizing,
887 self.col_gap,
888 self.row_gap,
889 self.inset,
890 )),
891 StrategyKind::VariableRow { estimated } => Rc::new(VariableRowGrid::new(
892 self.sizing,
893 self.col_gap,
894 self.row_gap,
895 self.inset,
896 estimated,
897 self.exact_item_height.clone(),
898 )),
899 StrategyKind::Waterfall { estimated } => Rc::new(VirtualizedMasonry::new(
900 self.sizing,
901 self.col_gap,
902 self.row_gap,
903 self.inset,
904 estimated,
905 self.exact_item_height.clone(),
906 )),
907 };
908 self.strategy = Some(s);
909 }
910 self.strategy.as_ref().unwrap().clone()
911 }
912}
913
914impl<T: 'static> std::fmt::Debug for GridView<T> {
915 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
916 f.debug_struct("GridView")
917 .field("items", &self.source.len())
918 .field("scroll_bar_style", &self.scroll_bar_style)
919 .field("scroll_y", &self.scroll_y.get())
920 .finish()
921 }
922}
923
924impl<T: 'static> Widget for GridView<T> {
925 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
926 let self_id = ctx.self_id();
927 ctx.enabled_when(self_id, self.enabled.clone());
928
929 if let Some(ref sig) = self.sizing_signal {
934 sig.bind_to(self_id, ctx.binding_registry(), BindingLevel::Rebuild);
935 let next = sig.get();
936 if self.sizing != next {
937 self.sizing = next;
938 self.strategy = None;
939 }
940 }
941
942 let strategy = self.ensure_strategy();
943
944 let version = ctx.signal(0_u64);
946 version.bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
947
948 self.scroll_y.bind_to(
950 ctx.self_id(),
951 ctx.binding_registry(),
952 BindingLevel::Relayout,
953 );
954 ctx.register_animated_signal(&self.scroll_y);
955
956 if let Some(ref sel) = self.selection {
958 sel.selection_signal().bind_to(
959 ctx.self_id(),
960 ctx.binding_registry(),
961 BindingLevel::AccessibilityOnly,
962 );
963 }
964 self.focused_index.bind_to(
965 ctx.self_id(),
966 ctx.binding_registry(),
967 BindingLevel::AccessibilityOnly,
968 );
969
970 {
972 let v = version.clone();
973 let counter = Rc::new(Cell::new(0_u64));
974 let strategy_obs = strategy.clone();
975 let selection_obs = self.selection.clone();
976 let len_fn = self.source.len_fn.clone();
977 let scroll_reset = self.scroll_y.clone();
978 let focused_obs = self.focused_index.clone();
979 let handle = (self.source.observe_fn)(Box::new(move |change| {
980 match change {
981 DataChange::ItemsInserted { range } => {
982 strategy_obs.invalidate_rows(range.start..usize::MAX);
983 strategy_obs.resize((len_fn)());
984 if let Some(ref s) = selection_obs {
985 s.adjust_for_insert(range.start, range.end - range.start);
986 }
987 }
988 DataChange::ItemsRemoved { range } => {
989 strategy_obs.invalidate_rows(range.start..usize::MAX);
990 strategy_obs.resize((len_fn)());
991 if let Some(ref s) = selection_obs {
992 s.adjust_for_remove(range.start, range.end - range.start);
993 }
994 }
995 DataChange::ItemsMoved { from, to, count } => {
996 strategy_obs.invalidate_rows(0..usize::MAX);
997 if let Some(ref s) = selection_obs {
998 s.adjust_for_move(*from, *to, *count);
999 }
1000 }
1001 DataChange::ItemUpdated { index } => {
1002 strategy_obs.invalidate_rows(*index..index + 1);
1003 }
1004 DataChange::WindowLoaded { range } => {
1005 strategy_obs.invalidate_rows(range.start..range.end);
1006 }
1007 DataChange::Reset => {
1008 strategy_obs.invalidate_rows(0..usize::MAX);
1009 strategy_obs.resize(0);
1010 if let Some(ref s) = selection_obs {
1011 s.clear();
1012 }
1013 scroll_reset.set(0.0);
1014 }
1015 }
1016 if let Some(current) = focused_obs.get() {
1023 focused_obs.set(teksilo_data::data_change::adjust_single_index_for_change(
1024 current, change,
1025 ));
1026 }
1027 let next = counter.get() + 1;
1028 counter.set(next);
1029 v.set(next);
1030 }));
1031 ctx.own_handle(handle);
1032 }
1033
1034 if let (Some(sel), Some(cb)) = (&self.selection, &self.on_selection_changed) {
1038 let cb = cb.clone();
1039 ctx.effect(&sel.selection_signal(), move |set| cb(set));
1040 }
1041
1042 if let Some(flag) = &self.is_loading {
1044 let v = version.clone();
1045 let c = Rc::new(Cell::new(0_u64));
1046 ctx.effect(&flag.as_signal(), move |_| {
1047 c.set(c.get() + 1);
1048 v.set(c.get());
1049 });
1050 }
1051
1052 let mut handlers = HandlerSet::new().clips_children(true).focusable(true);
1054 {
1055 let scroll_y = self.scroll_y.clone();
1056 let max_scroll = self.max_scroll_y.clone();
1057 let line_height = strategy.estimated_row_height().max(1.0);
1058 let overscroll = self.overscroll_behavior;
1059 let smooth_scrolling = self.smooth_scrolling;
1060 let smooth_scroll_duration = self.smooth_scroll_duration;
1061 handlers = handlers.on_scroll(move |event, _ctx| match event {
1062 WidgetEvent::Scroll { delta, .. } => {
1063 let dy = match delta {
1064 ScrollDelta::Lines { y, .. } => y * line_height,
1065 ScrollDelta::Pixels { y, .. } => *y,
1066 };
1067 let base = scroll_y.animation_target().unwrap_or(scroll_y.get());
1070 let (new_y, moved) =
1071 crate::common::scroll::scroll_clamp_axis(base, dy, max_scroll.get());
1072 if moved {
1073 if smooth_scrolling {
1074 scroll_y.animate_to(new_y, smooth_scroll_duration, Easing::EaseOut);
1075 } else {
1076 scroll_y.set(new_y);
1077 }
1078 }
1079 crate::common::scroll::scroll_response(
1080 moved,
1081 overscroll == OverscrollBehavior::Contain,
1082 )
1083 }
1084 _ => EventResponse::Ignored,
1085 });
1086 }
1087 handlers = handlers.on_key(build_grid_key_handler(GridKeyConfig {
1088 len_fn: self.source.len_fn.clone(),
1089 col_count: self.column_count.clone(),
1090 focused_index: self.focused_index.clone(),
1091 selection: self.selection.clone(),
1092 scroll_y: self.scroll_y.clone(),
1093 max_scroll_y: self.max_scroll_y.clone(),
1094 viewport_height: self.viewport_height.clone(),
1095 viewport_width: self.viewport_width.clone(),
1096 viewport_origin: self.viewport_origin.clone(),
1097 strategy: strategy.clone(),
1098 wrap_navigation: self.wrap_navigation,
1099 tab_traversal: self.tab_traversal,
1100 on_tile_activate: self.on_tile_activate.clone(),
1101 reorderable: self.reorderable,
1102 accept_drop_fn: self.source.dnd.accept_drop_fn.clone(),
1103 view_id: self.model_id,
1104 make_reorder_payload: {
1105 let model_id = self.model_id;
1106 let stash = self.source.dnd.stash_drag_keys_fn.clone();
1107 Rc::new(move |idx| {
1108 (stash)(&[idx]);
1112 DragPayload::typed(RowDragData::<T> {
1113 source: model_id,
1114 rows: vec![idx],
1115 items: None,
1116 })
1117 })
1118 },
1119 type_ahead_timeout: self.type_ahead_timeout,
1120 type_ahead_label: self.type_ahead_label.as_ref().map(|label| {
1127 let label = label.clone();
1128 let with_item_str = self.source.with_item_str_fn.clone();
1129 Rc::new(move |i: usize| (with_item_str)(i, &|_item: &T| label(i)))
1130 as Rc<dyn Fn(usize) -> Option<String>>
1131 }),
1132 }));
1133
1134 let marquee_on = self.marquee_selection
1138 && self
1139 .selection
1140 .as_ref()
1141 .map(|s| s.mode() == SelectionMode::Multi)
1142 .unwrap_or(false);
1143 if marquee_on {
1144 let additive_mods = Rc::new(Cell::new(false));
1145 {
1146 let mods = additive_mods.clone();
1147 handlers = handlers.on_pointer_event(move |event, _ctx| {
1148 if let WidgetEvent::PointerDown { modifiers, .. } = event {
1149 mods.set(modifiers.command() || modifiers.shift());
1150 }
1151 EventResponse::Ignored
1152 });
1153 }
1154 handlers = handlers.on_drag(build_marquee_handler(MarqueeConfig {
1155 marquee: self.marquee.clone(),
1156 selection: self.selection.clone().unwrap(),
1157 strategy: strategy.clone(),
1158 scroll_y: self.scroll_y.clone(),
1159 viewport_width: self.viewport_width.clone(),
1160 len_fn: self.source.len_fn.clone(),
1161 additive_mods,
1162 }));
1163
1164 let frame_request = ctx.frame_request_handle();
1176 let marquee_for_tick = self.marquee.clone();
1177 let scroll_for_tick = self.scroll_y.clone();
1178 let max_scroll_for_tick = self.max_scroll_y.clone();
1179 let viewport_h_for_tick = self.viewport_height.clone();
1180 ctx.effect(&ctx.frame_tick(), move |_delta| {
1181 let Some(st) = marquee_for_tick.get() else {
1182 return;
1183 };
1184 let step =
1185 selection::marquee_auto_scroll_step(st.current.y, viewport_h_for_tick.get());
1186 if step != 0.0 {
1187 let max = max_scroll_for_tick.get();
1188 let new_y = (scroll_for_tick.get() + step).clamp(0.0, max);
1189 scroll_for_tick.set(new_y);
1190 frame_request.set(true);
1194 }
1195 });
1196 }
1197
1198 if self.export.is_drop_target(self.reorderable) || self.on_item_drop.is_some() {
1206 let has_drop_cb = self.on_item_drop.is_some();
1207 let my_id = self.model_id;
1208
1209 let strategy_h = strategy.clone();
1210 let scroll_h = self.scroll_y.clone();
1211 let vp_w_h = self.viewport_width.clone();
1212 let len_h = self.source.len_fn.clone();
1213 let can_accept_h = self.source.dnd.can_accept_fn.clone();
1214 let insertion_h = self.insertion.clone();
1215 let export_for_hover = self.export.clone();
1216 handlers = handlers.on_drag_hover(move |payload, position, _ctx| {
1217 let len = (len_h)();
1218 let idx = drag::insertion_index(
1219 strategy_h.as_ref(),
1220 position,
1221 scroll_h.get(),
1222 vp_w_h.get(),
1223 len,
1224 );
1225 let allowed = drop_allowed::<T>(
1226 &can_accept_h,
1227 payload,
1228 idx,
1229 len,
1230 my_id,
1231 has_drop_cb,
1232 &export_for_hover,
1233 );
1234 if allowed {
1235 insertion_h.set(Some(idx));
1236 teksilo_core::DropFeedback::Accept
1239 } else {
1240 insertion_h.set(None);
1241 teksilo_core::DropFeedback::NoFeedback
1242 }
1243 });
1244
1245 let insertion_leave = self.insertion.clone();
1246 handlers = handlers.on_drag_leave(move |_ctx| {
1247 insertion_leave.set(None);
1248 });
1249
1250 let strategy_d = strategy.clone();
1251 let scroll_d = self.scroll_y.clone();
1252 let vp_w_d = self.viewport_width.clone();
1253 let len_d = self.source.len_fn.clone();
1254 let accept_drop_d = self.source.dnd.accept_drop_fn.clone();
1255 let drop_cb = self.on_item_drop.clone();
1256 let insertion_d = self.insertion.clone();
1257 let export_for_drop = self.export.clone();
1258 let reorderable_for_drop = self.reorderable;
1259 handlers = handlers.on_drop(move |mut payload, position, ctx| {
1260 insertion_d.set(None);
1261 let len = (len_d)();
1262 let to = drag::insertion_index(
1263 strategy_d.as_ref(),
1264 position,
1265 scroll_d.get(),
1266 vp_w_d.get(),
1267 len,
1268 );
1269 let is_same_view = payload
1270 .get_typed::<RowDragData<T>>()
1271 .is_some_and(|rd| rd.source == my_id);
1272 if (reorderable_for_drop || !is_same_view)
1277 && let Some((target, position_kind)) = flat_insertion_target(to, len)
1278 && (accept_drop_d)(&payload, target, position_kind, my_id)
1279 {
1280 if is_same_view {
1283 export_for_drop.note_self_reorder();
1284 }
1285 return true;
1286 }
1287 if export_for_drop.foreign_receive(&mut payload, my_id, to, ctx) {
1293 return true;
1294 }
1295 if let Some(ref cb) = drop_cb {
1298 return cb(payload, to, ctx);
1299 }
1300 false
1301 });
1302 }
1303 ctx.apply_self_handlers(handlers);
1304
1305 self.body_pane_id = None;
1310 self.empty_id = None;
1311 self.scrollbar_id = None;
1312 self.overlay_id = None;
1313 self.pinned_header_id = None;
1314
1315 let len = self.source.len();
1316 if len == 0 {
1317 self.tile_map.borrow_mut().clear();
1318 if let Some(ref ef) = self.empty_view {
1319 self.empty_id = Some(ctx.add_boxed(ef()));
1320 }
1321 } else {
1322 let pane_total_refresh = ctx.signal(0_u64);
1327 pane_total_refresh.bind_to(
1328 ctx.self_id(),
1329 ctx.binding_registry(),
1330 teksilo_core::binding::BindingLevel::Relayout,
1331 );
1332 let pane = GridBodyPane {
1333 len_fn: self.source.len_fn.clone(),
1334 with_item_fn: self.source.with_item_fn.clone(),
1335 delegate: self.delegate.clone(),
1336 strategy: strategy.clone(),
1337 viewport_width: self.viewport_width.clone(),
1338 viewport_height: self.viewport_height.clone(),
1339 viewport_origin: self.viewport_origin.clone(),
1340 column_count: self.column_count.clone(),
1341 scroll_y: self.scroll_y.clone(),
1342 selection: self.selection.clone(),
1343 focused_index: self.focused_index.clone(),
1344 on_tile_activate: self.on_tile_activate.clone(),
1345 activate_on: self.activate_on,
1346 tile_context_menu: self.tile_context_menu.clone(),
1347 tile_a11y_label: self.tile_a11y_label.clone(),
1348 reorderable: self.reorderable,
1349 model_id: self.model_id,
1350 scope_owner: ctx.self_id(),
1351 drag_fn: self.source.dnd.drag_fn.clone(),
1352 row_state_fn: self.source.dnd.row_state_fn.clone(),
1353 request_window_fn: self.source.dnd.request_window_fn.clone(),
1354 can_fetch_more_fn: self.source.dnd.can_fetch_more_fn.clone(),
1355 fetch_more_fn: self.source.dnd.fetch_more_fn.clone(),
1356 export: self.export.clone(),
1357 read_item_fn: self.source.read_item_fn.clone(),
1358 snapshot_out_fn: self.source.dnd.snapshot_out_fn.clone(),
1359 tile_map: self.tile_map.clone(),
1360 header_factory: self.header_factory(),
1361 header_title: self.section_data.as_ref().map(|d| d.title_fn.clone()),
1362 version: Signal::new(0_u64),
1365 prev_built_start: Rc::new(Cell::new(0)),
1366 prev_built_end: Rc::new(Cell::new(0)),
1367 total_refresh: pane_total_refresh,
1368 tile_entries: Vec::new(),
1369 header_entries: Vec::new(),
1370 in_place_children: Cell::new(false),
1371 };
1372 self.body_pane_id = Some(ctx.add(pane));
1373
1374 let overlay = GridOverlay {
1375 focused_index: self.focused_index.clone(),
1376 view_focused: ctx.view_focus_active(),
1380 focus_visible: ctx.focus_visible(),
1381 selection: self.selection.clone(),
1382 scroll_y: self.scroll_y.clone(),
1383 strategy: strategy.clone(),
1384 viewport_width: self.viewport_width.clone(),
1385 marquee: self.marquee.clone(),
1386 insertion: self.insertion.clone(),
1387 style: self.style.clone(),
1388 len_fn: self.source.len_fn.clone(),
1389 };
1390 self.overlay_id = Some(ctx.add(overlay));
1391
1392 self.pinned_header_id = None;
1399 let section_count = self
1400 .section_data
1401 .as_ref()
1402 .map(|d| (d.counts_fn)().len())
1403 .unwrap_or(0);
1404 if self.pinned_section_headers && section_count > 0 {
1405 if let Some(factory) = self.header_factory() {
1406 let ph = PinnedHeader {
1407 current_section: self.current_section.clone(),
1408 factory,
1409 child: None,
1410 style: self.style.clone(),
1411 };
1412 self.pinned_header_id = Some(ctx.add(ph));
1413 }
1414 }
1415 }
1416
1417 if self.show_scrollbar {
1418 let sb = ScrollBar::new(
1419 ScrollBarOrientation::Vertical,
1420 self.scroll_y.clone(),
1421 self.max_scroll_y.clone(),
1422 self.viewport_ratio_y.clone(),
1423 )
1424 .visual(match self.scroll_bar_style {
1425 ScrollBarMode::Permanent => ScrollBarVisual::Permanent,
1426 ScrollBarMode::Overlay => ScrollBarVisual::Overlay,
1427 ScrollBarMode::Thin => ScrollBarVisual::Thin,
1428 });
1429 self.scrollbar_id = Some(ctx.add(sb));
1430 }
1431
1432 self.loading_id = None;
1434 if let Some(flag) = &self.is_loading {
1435 if flag.get() {
1436 if let Some(ref lv) = self.loading_view {
1437 self.loading_id = Some(ctx.add_boxed(lv()));
1438 }
1439 }
1440 }
1441
1442 let mut children = Vec::new();
1444 if let Some(id) = self.body_pane_id {
1445 children.push(id);
1446 }
1447 if let Some(id) = self.empty_id {
1448 children.push(id);
1449 }
1450 if let Some(id) = self.scrollbar_id {
1451 children.push(id);
1452 }
1453 if let Some(id) = self.overlay_id {
1454 children.push(id);
1455 }
1456 if let Some(id) = self.pinned_header_id {
1457 children.push(id);
1458 }
1459 if let Some(id) = self.loading_id {
1460 children.push(id);
1461 }
1462 children
1463 }
1464
1465 fn layout_response(
1466 &self,
1467 proposal: SizeProposal,
1468 _ctx: &LayoutContext,
1469 ) -> teksilo_core::widget::LayoutResponse {
1470 let size = crate::common::viewport::viewport_size(
1474 proposal,
1475 &self.viewport_height,
1476 Size::new(400.0, 400.0),
1477 );
1478 if proposal.width.is_some() {
1479 self.viewport_width.set(size.width);
1480 }
1481 size.into()
1482 }
1483
1484 fn place_children(
1485 &self,
1486 bounds: Rect,
1487 _proposal: SizeProposal,
1488 children: &mut [WidgetPlacement],
1489 _ctx: &LayoutContext,
1490 ) {
1491 let Some(ref strategy) = self.strategy else {
1492 return;
1493 };
1494 let len = self.source.len();
1495 let vp_h = bounds.height;
1496
1497 let reserves_bar = self.scroll_bar_style == ScrollBarMode::Permanent;
1505 let body_w = if self.last_needs_scrollbar.get() && reserves_bar {
1506 (bounds.width - SCROLLBAR_THICKNESS).max(0.0)
1507 } else {
1508 bounds.width
1509 };
1510 self.viewport_width.set(body_w);
1511
1512 let cols = strategy.column_count(body_w).max(1);
1513 if self.column_count.get() != cols {
1514 self.column_count.set(cols);
1515 }
1516
1517 let total = strategy.total_content_height(len, body_w);
1518 let needs_sb = self.show_scrollbar && total > vp_h + 0.5;
1519 if self.last_needs_scrollbar.get() != needs_sb {
1520 self.last_needs_scrollbar.set(needs_sb);
1521 }
1522 let max_y = (total - vp_h).max(0.0);
1523 self.max_scroll_y.set(max_y);
1524 let ratio = if total > 0.0 {
1525 (vp_h / total).clamp(0.0, 1.0)
1526 } else {
1527 1.0
1528 };
1529 self.viewport_ratio_y.set(ratio);
1530 let cur = self.scroll_y.get();
1532 let clamped = cur.clamp(0.0, max_y);
1533 if (clamped - cur).abs() > 0.001 {
1534 self.scroll_y.set(clamped);
1535 }
1536
1537 let pinned_rect = if self.pinned_header_id.is_some() {
1540 let cur = strategy.current_section(self.scroll_y.get(), body_w);
1541 if let Some(cur) = cur {
1542 if self.current_section.get() != cur {
1543 self.current_section.set(cur);
1544 }
1545 strategy.header_rect(cur, body_w).map(|r| {
1547 let screen_y = bounds.y + r.y - self.scroll_y.get();
1548 let visible = screen_y < bounds.y - 0.5;
1549 (visible, r.height)
1550 })
1551 } else {
1552 None
1553 }
1554 } else {
1555 None
1556 };
1557
1558 let body_rect_origin = bounds.origin();
1559 let body_size = Size::new(body_w, vp_h);
1560 for child in children.iter_mut() {
1561 if Some(child.id) == self.scrollbar_id {
1562 if needs_sb {
1563 child.origin =
1566 Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
1567 child.size = Size::new(SCROLLBAR_THICKNESS, vp_h);
1568 } else {
1569 child.origin = bounds.origin();
1570 child.size = Size::ZERO;
1571 }
1572 } else if Some(child.id) == self.pinned_header_id {
1573 match pinned_rect {
1574 Some((true, h)) => {
1575 child.origin = bounds.origin();
1576 child.size = Size::new(body_w, h);
1577 }
1578 _ => {
1579 child.origin = bounds.origin();
1580 child.size = Size::ZERO;
1581 }
1582 }
1583 } else {
1584 child.origin = body_rect_origin;
1586 child.size = body_size;
1587 }
1588 }
1589 }
1590
1591 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
1592 builder.set_role(teksilo_core::accesskit::Role::Grid);
1593 if let Some(ref label) = self.a11y_label {
1594 builder.set_name(label.clone());
1595 }
1596
1597 let total = self.source.len();
1598 let cols = self.column_count.get().max(1);
1599 let rows = total.div_ceil(cols);
1600 builder.set_row_count(rows);
1601 builder.set_column_count(cols);
1602
1603 if let Some(ref sel) = self.selection {
1604 if sel.mode() == SelectionMode::Multi {
1605 builder.set_multiselectable(true);
1606 }
1607 let count = sel.count();
1608 if count > 0 {
1609 builder.set_value(format!(
1610 "{} item{} selected",
1611 count,
1612 if count == 1 { "" } else { "s" }
1613 ));
1614 }
1615 builder.set_live(teksilo_core::accesskit::Live::Polite);
1616 }
1617
1618 if let Some(idx) = self.focused_index.get() {
1620 let map = self.tile_map.borrow();
1621 if let Some((_, tile_id)) = map.iter().find(|(i, _)| *i == idx) {
1622 builder.set_active_descendant(widget_id_to_node_id(*tile_id));
1623 }
1624 }
1625 }
1626
1627 fn as_any(&self) -> Option<&dyn std::any::Any> {
1628 Some(self)
1629 }
1630
1631 fn children(&self) -> Vec<WidgetId> {
1632 let mut ids = Vec::new();
1633 if let Some(id) = self.body_pane_id {
1634 ids.push(id);
1635 }
1636 if let Some(id) = self.empty_id {
1637 ids.push(id);
1638 }
1639 if let Some(id) = self.scrollbar_id {
1640 ids.push(id);
1641 }
1642 if let Some(id) = self.overlay_id {
1643 ids.push(id);
1644 }
1645 if let Some(id) = self.pinned_header_id {
1646 ids.push(id);
1647 }
1648 if let Some(id) = self.loading_id {
1649 ids.push(id);
1650 }
1651 ids
1652 }
1653
1654 fn clips_children(&self) -> bool {
1655 true
1656 }
1657}
1658
1659struct GridOverlay {
1664 focused_index: Signal<Option<usize>>,
1665 view_focused: Signal<bool>,
1669 focus_visible: Signal<bool>,
1672 selection: Option<SelectionModel>,
1676 scroll_y: Signal<f32>,
1677 strategy: Rc<dyn GridLayoutStrategy>,
1678 viewport_width: Rc<Cell<f32>>,
1679 marquee: Signal<Option<MarqueeState>>,
1680 insertion: Signal<Option<usize>>,
1681 style: Option<Rc<dyn GridViewStyle>>,
1682 len_fn: Rc<dyn Fn() -> usize>,
1688}
1689
1690impl std::fmt::Debug for GridOverlay {
1691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1692 f.debug_struct("GridOverlay").finish()
1693 }
1694}
1695
1696impl GridOverlay {
1697 fn focus_recipe(&self, ctx: &PaintContext) -> teksilo_core::styles::GridFocusRingRecipe {
1698 resolve_grid_style(&self.style, ctx, |s| s.focus_ring())
1699 }
1700 fn marquee_recipe(&self, ctx: &PaintContext) -> teksilo_core::styles::GridMarqueeRecipe {
1701 resolve_grid_style(&self.style, ctx, |s| s.marquee())
1702 }
1703 fn insertion_recipe(&self, ctx: &PaintContext) -> teksilo_core::styles::GridInsertionRecipe {
1704 resolve_grid_style(&self.style, ctx, |s| s.insertion())
1705 }
1706}
1707
1708fn insertion_bar_geometry(
1716 strategy: &dyn GridLayoutStrategy,
1717 ins: usize,
1718 len: usize,
1719 viewport_width: f32,
1720) -> Option<(f32, TileRect)> {
1721 if len == 0 {
1722 return None;
1723 }
1724 if ins < len {
1725 let r = strategy.tile_rect(ins, viewport_width);
1726 Some((r.x, r))
1727 } else {
1728 let r = strategy.tile_rect(len - 1, viewport_width);
1729 Some((r.x + r.width, r))
1730 }
1731}
1732
1733fn resolve_grid_style<R: Default>(
1736 override_style: &Option<Rc<dyn GridViewStyle>>,
1737 ctx: &PaintContext,
1738 f: impl Fn(&dyn GridViewStyle) -> R,
1739) -> R {
1740 if let Some(s) = override_style {
1741 f(s.as_ref())
1742 } else if let Some(s) = ctx.theme.style_slots.grid_view.as_ref() {
1743 f(s.as_ref())
1744 } else {
1745 R::default()
1746 }
1747}
1748
1749impl Widget for GridOverlay {
1750 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1751 self.scroll_y.bind_to(
1753 ctx.self_id(),
1754 ctx.binding_registry(),
1755 BindingLevel::RepaintOnly,
1756 );
1757 self.focused_index.bind_to(
1758 ctx.self_id(),
1759 ctx.binding_registry(),
1760 BindingLevel::RepaintOnly,
1761 );
1762 self.view_focused.bind_to(
1763 ctx.self_id(),
1764 ctx.binding_registry(),
1765 BindingLevel::RepaintOnly,
1766 );
1767 self.focus_visible.bind_to(
1768 ctx.self_id(),
1769 ctx.binding_registry(),
1770 BindingLevel::RepaintOnly,
1771 );
1772 if let Some(ref sel) = self.selection {
1773 sel.selection_signal().bind_to(
1774 ctx.self_id(),
1775 ctx.binding_registry(),
1776 BindingLevel::RepaintOnly,
1777 );
1778 }
1779 self.marquee.bind_to(
1780 ctx.self_id(),
1781 ctx.binding_registry(),
1782 BindingLevel::RepaintOnly,
1783 );
1784 self.insertion.bind_to(
1785 ctx.self_id(),
1786 ctx.binding_registry(),
1787 BindingLevel::RepaintOnly,
1788 );
1789 ctx.apply_self_handlers(HandlerSet::new().event_pass_through(true));
1791 Vec::new()
1792 }
1793
1794 fn layout_response(
1795 &self,
1796 proposal: SizeProposal,
1797 _ctx: &LayoutContext,
1798 ) -> teksilo_core::widget::LayoutResponse {
1799 proposal.resolve(0.0, 0.0).into()
1800 }
1801
1802 fn paint(&self, bounds: Rect, canvas: &mut teksilo_canvas::Canvas, ctx: &PaintContext) {
1803 if let Some(m) = self.marquee.get() {
1805 let lr = m.local_rect(self.scroll_y.get());
1806 let rect = Rect::new(bounds.x + lr.x, bounds.y + lr.y, lr.width, lr.height);
1807 let recipe = self.marquee_recipe(ctx);
1808 let c = recipe.role.resolve(&ctx.theme.colors);
1809 let fill = teksilo_tokens::Color::new(c.r(), c.g(), c.b(), recipe.fill_alpha);
1810 canvas.fill_rect(rect, fill);
1811 canvas.stroke_rect(rect, c, recipe.stroke_width);
1812 }
1813
1814 if let Some(ins) = self.insertion.get()
1818 && let Some((bar_x, r)) =
1819 insertion_bar_geometry(self.strategy.as_ref(), ins, (self.len_fn)(), bounds.width)
1820 {
1821 let scroll_y = self.scroll_y.get();
1822 let y = bounds.y + r.y - scroll_y;
1823 let h = r.height;
1824 if y + h >= bounds.y && y <= bounds.bottom() {
1825 let recipe = self.insertion_recipe(ctx);
1826 let color = recipe.role.resolve(&ctx.theme.colors);
1827 let t = recipe.thickness;
1828 canvas.fill_rect(Rect::new(bounds.x + bar_x - t * 0.5, y, t, h), color);
1829 }
1830 }
1831
1832 if !self.view_focused.get() || !self.focus_visible.get() {
1835 return;
1836 }
1837 let idx = self.focused_index.get().filter(|&i| i < (self.len_fn)());
1841 let Some(idx) = idx else {
1842 let empty = self.selection.as_ref().is_none_or(|s| s.count() == 0);
1846 if empty {
1847 let inset = 1.0_f32;
1848 let rect = Rect::new(
1849 bounds.x + inset,
1850 bounds.y + inset,
1851 (bounds.width - inset * 2.0).max(0.0),
1852 (bounds.height - inset * 2.0).max(0.0),
1853 );
1854 let color = teksilo_tokens::BorderRole::Focused.resolve(&ctx.theme.colors);
1855 canvas.stroke_rect(rect, color, 1.5);
1856 }
1857 return;
1858 };
1859 let vp_w = bounds.width;
1860 let r = self.strategy.tile_rect(idx, vp_w);
1861 let scroll_y = self.scroll_y.get();
1862 let recipe = self.focus_recipe(ctx);
1863 let inset = recipe.inset;
1864 let stroke = recipe.thickness;
1865 let rx = bounds.x + r.x + inset;
1866 let ry = bounds.y + r.y - scroll_y + inset;
1867 let rw = (r.width - inset * 2.0).max(0.0);
1868 let rh = (r.height - inset * 2.0).max(0.0);
1869 if ry + rh < bounds.y || ry > bounds.bottom() {
1871 return;
1872 }
1873 let color = recipe.role.resolve(&ctx.theme.colors);
1874 canvas.fill_rect(Rect::new(rx, ry, rw, stroke), color); canvas.fill_rect(Rect::new(rx, ry + rh - stroke, rw, stroke), color); canvas.fill_rect(Rect::new(rx, ry, stroke, rh), color); canvas.fill_rect(Rect::new(rx + rw - stroke, ry, stroke, rh), color); }
1879
1880 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
1881 builder.set_hidden();
1882 }
1883}
1884
1885struct PinnedHeader {
1889 current_section: Signal<usize>,
1890 #[allow(clippy::type_complexity)]
1891 factory: Rc<dyn Fn(usize) -> Box<dyn Widget>>,
1892 child: Option<WidgetId>,
1893 style: Option<Rc<dyn GridViewStyle>>,
1894}
1895
1896impl std::fmt::Debug for PinnedHeader {
1897 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1898 f.debug_struct("PinnedHeader")
1899 .field("section", &self.current_section.get())
1900 .finish()
1901 }
1902}
1903
1904impl Widget for PinnedHeader {
1905 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1906 self.current_section
1907 .bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
1908 let section = self.current_section.get();
1909 let id = ctx.add_boxed((self.factory)(section));
1910 self.child = Some(id);
1911 vec![id]
1912 }
1913
1914 fn layout_response(
1915 &self,
1916 proposal: SizeProposal,
1917 _ctx: &LayoutContext,
1918 ) -> teksilo_core::widget::LayoutResponse {
1919 proposal.resolve(0.0, 0.0).into()
1920 }
1921
1922 fn place_children(
1923 &self,
1924 bounds: Rect,
1925 _proposal: SizeProposal,
1926 children: &mut [WidgetPlacement],
1927 _ctx: &LayoutContext,
1928 ) {
1929 for child in children.iter_mut() {
1930 child.origin = bounds.origin();
1931 child.size = bounds.size();
1932 }
1933 }
1934
1935 fn paint(&self, bounds: Rect, canvas: &mut teksilo_canvas::Canvas, ctx: &PaintContext) {
1936 if bounds.height > 0.5 {
1937 let surface = self
1938 .style
1939 .as_ref()
1940 .or(ctx.theme.style_slots.grid_view.as_ref())
1941 .map(|s| s.pinned_header_surface())
1942 .unwrap_or(SurfaceRole::Raised);
1943 canvas.fill_rect(bounds, surface.resolve(&ctx.theme.colors));
1944 }
1945 }
1946
1947 fn children(&self) -> Vec<WidgetId> {
1948 self.child.into_iter().collect()
1949 }
1950
1951 fn clips_children(&self) -> bool {
1952 true
1953 }
1954}