1use std::cell::Cell;
5use std::collections::HashMap;
6use std::ops::Range;
7use std::rc::Rc;
8use ui_core::widget::{AppModel, ModelPath, StructureChange};
9
10use taffy::style_helpers::{length, percent};
11use taffy::{NodeId, Style, TaffyTree};
12use tilo_resource::{Id, Resource};
13use ui_core::a11y::{AccessNode, Role};
14use ui_core::presence::PresenceCtx;
15use ui_core::text_render::TextRenderer;
16use ui_core::vello::Scene;
17use ui_core::vello::kurbo::{Affine, Point, Rect};
18use ui_core::widget::{ContextMenu, Element, MenuItem, TaffyTreeExt, Widget, draw_child};
19use ui_core::winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
20use ui_core::winit::keyboard::{Key, NamedKey};
21use ui_core::{Theme, taffy};
22
23use super::dispatch::Dispatcher;
24use super::live::Live;
25use super::reactive::{Gate, Updaters};
26
27struct MenuEntry {
29 address: tilo_actions::ActionAddress,
30 prompts: bool,
33}
34
35#[derive(Default)]
37struct RowMenu {
38 menu: ContextMenu,
39 entries: Vec<MenuEntry>,
40 cursor: Point,
41 palette: Option<tilo_actions::ActionAddress>,
43}
44
45struct Row {
48 id: Id,
49 height: Option<f64>,
52 view: Option<Box<RowView>>,
55}
56
57struct RowView {
59 _alive: Rc<()>,
61 gate: Gate,
63 widget: Element,
64 node: Option<NodeId>,
66}
67
68impl Row {
69 fn node(&self) -> Option<NodeId> {
70 self.view.as_ref().and_then(|view| view.node)
71 }
72
73 fn widget(&self) -> Option<&dyn Widget> {
74 self.view.as_ref().map(|view| view.widget.as_ref())
75 }
76}
77
78#[derive(Clone, Copy, PartialEq, Eq)]
80enum Builds {
81 Unknown,
84 OnMount,
86 Every,
89}
90
91#[derive(Default)]
93struct Pushed {
94 conventions: Option<ui_core::platform::PlatformConventions>,
95 environment: Option<ui_core::interaction::InteractionEnvironment>,
96 motion_reduced: Option<bool>,
97}
98
99impl Pushed {
100 fn apply(&self, widget: &mut dyn Widget) {
101 if let Some(conventions) = self.conventions {
102 widget.set_platform_conventions(conventions);
103 }
104 if let Some(environment) = self.environment {
105 widget.set_interaction_environment(environment);
106 }
107 if let Some(reduced) = self.motion_reduced {
108 widget.set_motion_reduced(reduced);
109 }
110 }
111}
112
113type Ids<R> = Box<dyn Fn(&<R as Resource>::State) -> Vec<Id>>;
114type BuildRow = Box<dyn Fn(&Id, &Rc<()>, &Gate) -> Element>;
115
116const ESTIMATED_ROW_HEIGHT: f64 = 32.0;
118const INITIAL_ROWS: usize = 64;
120const MIN_OVERSCAN: f64 = 160.0;
122const MAX_REMOUNTS_PER_FRAME: u8 = 3;
126
127pub struct Each<R: Resource> {
151 live: Live<R>,
152 dispatch: Dispatcher<R>,
153 menu: RowMenu,
154 ids: Ids<R>,
155 build: BuildRow,
156 rows: Vec<Row>,
157 builds: Builds,
158 pushed: Pushed,
159 stale: Rc<Cell<bool>>,
162 updaters: Updaters<R>,
164 mounted: Vec<usize>,
166 tops: Vec<f64>,
168 total: f64,
169 measured: (f64, u32),
172 estimated: f64,
173 gap: f64,
174 width: f64,
176 node: Option<NodeId>,
177 rect: Rect,
178 visible: Option<(f64, f64)>,
181 laid_out: bool,
182 remounts: u8,
183 structure: StructureChange,
188 geometry_dirty: bool,
189}
190
191impl<R: Resource> Each<R> {
192 pub(crate) fn new(
193 live: Live<R>,
194 dispatch: Dispatcher<R>,
195 ids: Ids<R>,
196 stale: Rc<Cell<bool>>,
197 updaters: Updaters<R>,
198 build: BuildRow,
199 ) -> Self {
200 let mut each = Self {
201 stale,
202 updaters,
203 live,
204 dispatch,
205 menu: RowMenu::default(),
206 ids,
207 build,
208 rows: Vec::new(),
209 builds: Builds::Unknown,
210 pushed: Pushed::default(),
211 mounted: Vec::new(),
212 tops: Vec::new(),
213 total: 0.0,
214 measured: (0.0, 0),
215 estimated: ESTIMATED_ROW_HEIGHT,
216 gap: 0.0,
217 width: 0.0,
218 node: None,
219 rect: Rect::ZERO,
220 visible: None,
221 laid_out: false,
222 remounts: 0,
223 structure: StructureChange::None,
224 geometry_dirty: false,
225 };
226 each.reconcile();
227 each
228 }
229
230 fn reconcile(&mut self) {
231 let ids = self.live.with(|state| (self.ids)(state));
232 let unchanged = ids.len() == self.rows.len()
233 && ids.iter().zip(&self.rows).all(|(id, row)| *id == row.id);
234 if unchanged {
235 return;
236 }
237 let was_mounted: Vec<Id> = self
238 .mounted
239 .iter()
240 .filter_map(|&index| self.rows.get(index))
241 .map(|row| row.id.clone())
242 .collect();
243 let mut old: HashMap<Id, Row> = self
244 .rows
245 .drain(..)
246 .map(|row| (row.id.clone(), row))
247 .collect();
248 self.rows = ids
250 .into_iter()
251 .map(|id| {
252 old.remove(&id).unwrap_or(Row {
253 id,
254 height: None,
255 view: None,
256 })
257 })
258 .collect();
259 self.mounted = self
264 .rows
265 .iter()
266 .enumerate()
267 .filter(|(_, row)| row.node().is_some())
268 .map(|(index, _)| index)
269 .collect();
270 let still_mounted = self.mounted().map(|(id, _)| id);
271 if !still_mounted.eq(was_mounted.iter()) {
272 self.structure = StructureChange::Rebuild;
273 }
274 if self.builds == Builds::Every {
275 self.build_all();
276 } else if self.builds == Builds::Unknown && !self.rows.is_empty() {
277 self.ensure_built(0);
280 }
281 self.recount();
282 self.relayout_tops(0);
283 self.geometry_dirty = true;
284 }
285
286 fn ensure_built(&mut self, index: usize) {
289 let Some(row) = self.rows.get_mut(index) else {
290 return;
291 };
292 if row.view.is_some() {
293 return;
294 }
295 let alive = Rc::new(());
296 let gate = Gate::mounted();
297 let mut widget = (self.build)(&row.id, &alive, &gate);
298 self.pushed.apply(widget.as_mut());
299 let reads_unmounted = gate.reads_unmounted();
300 row.view = Some(Box::new(RowView {
301 _alive: alive,
302 gate,
303 widget,
304 node: None,
305 }));
306 if reads_unmounted && self.builds != Builds::Every {
307 self.builds = Builds::Every;
308 self.build_all();
309 } else if self.builds == Builds::Unknown {
310 self.builds = Builds::OnMount;
311 }
312 }
313
314 fn build_all(&mut self) {
315 for index in 0..self.rows.len() {
316 self.ensure_built(index);
317 }
318 }
319
320 fn estimate(&self) -> f64 {
322 let (sum, count) = self.measured;
323 if count == 0 {
324 ESTIMATED_ROW_HEIGHT
325 } else {
326 sum / f64::from(count)
327 }
328 }
329
330 fn recount(&mut self) {
332 self.measured = self
333 .rows
334 .iter()
335 .filter_map(|row| row.height)
336 .fold((0.0, 0_u32), |(sum, count), height| {
337 (sum + height, count.saturating_add(1))
338 });
339 }
340
341 fn relayout_tops(&mut self, from: usize) {
344 let estimate = self.estimate();
345 let from = if (estimate - self.estimated).abs() > f64::EPSILON
346 || self.tops.len() != self.rows.len()
347 {
348 0
349 } else {
350 from.min(self.rows.len())
351 };
352 self.estimated = estimate;
353 self.tops.truncate(from);
354 let mut y = from
355 .checked_sub(1)
356 .and_then(|last| {
357 let top = self.tops.get(last)?;
358 let row = self.rows.get(last)?;
359 Some(top + row.height.unwrap_or(estimate))
360 })
361 .unwrap_or(0.0);
362 for (index, row) in self.rows.iter().enumerate().skip(from) {
363 if index > 0 {
364 y += self.gap;
365 }
366 self.tops.push(y);
367 y += row.height.unwrap_or(estimate);
368 }
369 self.total = y;
370 }
371
372 fn rows_between(&self, top: f64, bottom: f64) -> Range<usize> {
375 let first = self
376 .tops
377 .partition_point(|&row_top| row_top <= top)
378 .saturating_sub(1);
379 let end = self.tops.partition_point(|&row_top| row_top < bottom);
380 first..end.max(first)
381 }
382
383 fn overscan(&self) -> f64 {
385 self.visible.map_or(0.0, |(top, bottom)| {
386 ((bottom - top) * 0.5).max(MIN_OVERSCAN)
387 })
388 }
389
390 fn focused_index(&self) -> Option<usize> {
392 self.mounted.iter().copied().find(|&index| {
393 self.rows
394 .get(index)
395 .and_then(Row::widget)
396 .is_some_and(holds_focus)
397 })
398 }
399
400 fn around_focus(&self) -> Range<usize> {
403 self.focused_index().map_or(0..0, |index| {
404 index.saturating_sub(1)..index.saturating_add(2).min(self.rows.len())
405 })
406 }
407
408 fn window(&self) -> Vec<usize> {
412 let count = self.rows.len();
413 if !self.laid_out {
414 return (0..count.min(INITIAL_ROWS)).collect();
415 }
416 let Some((top, bottom)) = self.visible else {
417 return (0..count).collect();
418 };
419 let overscan = self.overscan();
420 let mut window: Vec<usize> = self
421 .rows_between(top - overscan, bottom + overscan)
422 .collect();
423 window.extend(self.around_focus());
424 window.extend(self.mounted.iter().copied().filter(|&index| {
425 self.rows
426 .get(index)
427 .and_then(Row::widget)
428 .is_some_and(|widget| {
429 widget.wants_pointer_capture() || widget.modal_capture().is_some()
430 })
431 }));
432 window.sort_unstable();
433 window.dedup();
434 window
435 }
436
437 fn window_stale(&self) -> bool {
441 let Some((top, bottom)) = self.visible else {
442 return self.mounted.len() != self.rows.len();
443 };
444 let mounts = |rows: Range<usize>| {
445 rows.is_empty()
446 || self.mounted.binary_search(&rows.start).is_ok_and(|at| {
447 let last = at.saturating_add(rows.len()).saturating_sub(1);
448 self.mounted.get(last) == Some(&rows.end.saturating_sub(1))
449 })
450 };
451 let overscan = self.overscan();
452 let needed = self.rows_between(top - overscan / 4.0, bottom + overscan / 4.0);
453 let wanted = self.rows_between(top - overscan, bottom + overscan).len();
454 !mounts(needed)
455 || !mounts(self.around_focus())
456 || self.mounted.len() > wanted.saturating_mul(2).saturating_add(8)
457 }
458
459 pub fn row_ids(&self) -> impl Iterator<Item = &Id> {
461 self.rows.iter().map(|row| &row.id)
462 }
463
464 #[must_use]
466 pub fn mounted_rows(&self) -> usize {
467 self.mounted.len()
468 }
469
470 fn mounted(&self) -> impl Iterator<Item = (&Id, &RowView)> {
472 self.mounted.iter().filter_map(|&index| {
473 let row = self.rows.get(index)?;
474 Some((&row.id, row.view.as_deref()?))
475 })
476 }
477
478 fn for_each_mounted(&mut self, mut visit: impl FnMut(&mut RowView)) {
479 for &index in &self.mounted {
480 if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) {
481 visit(view);
482 }
483 }
484 }
485
486 fn push_geometry(&mut self, taffy: &mut TaffyTree<()>) -> bool {
489 let mut changed = false;
490 if let Some(node) = self.node
491 && let Ok(style) = taffy.style(node)
492 && style.size.height != length(px(self.total))
493 {
494 let mut style = style.clone();
495 style.size.height = length(px(self.total));
496 changed |= taffy.set_style(node, style).is_ok();
497 }
498 for &index in &self.mounted {
499 let Some(child) = self.rows.get(index).and_then(Row::node) else {
500 continue;
501 };
502 let top = length(px(self.tops.get(index).copied().unwrap_or_default()));
503 if let Ok(style) = taffy.style(child)
504 && style.inset.top != top
505 {
506 let mut style = style.clone();
507 style.inset.top = top;
508 changed |= taffy.set_style(child, style).is_ok();
509 }
510 }
511 self.geometry_dirty = false;
512 changed
513 }
514
515 fn open_menu(&mut self, item: &Id, at: Point) -> bool {
518 let children = self.live.with(|state| R::children(state));
519 let Some(resource) = children.iter().find(|child| {
520 tilo_resource::items::item_of_resource::<R>(child.id.as_str())
521 .is_some_and(|(_, id)| &id == item)
522 }) else {
523 return false;
524 };
525 let read_only = self.dispatch.read_only();
526 let mut menu = ContextMenu::new();
527 let mut entries = Vec::new();
528 for spec in &resource.actions {
529 let entry = if spec.contract.risk == tilo_actions::RiskLevel::Destructive {
530 MenuItem::destructive_action(spec.label.clone(), || {})
531 } else {
532 MenuItem::action(spec.label.clone(), || {})
533 };
534 menu.push(entry.with_enabled(!read_only));
535 entries.push(MenuEntry {
536 address: tilo_actions::ActionAddress::new(resource.id.as_str(), spec.id.as_str()),
537 prompts: !spec.params().is_empty(),
538 });
539 }
540 if entries.is_empty() {
541 return false;
542 }
543 #[allow(
544 clippy::cast_possible_truncation,
545 reason = "a menu anchor is a logical pixel position on screen"
546 )]
547 menu.open_at([at.x as f32, at.y as f32]);
548 self.menu.menu = menu;
549 self.menu.entries = entries;
550 true
551 }
552
553 fn choose(&mut self, index: usize) {
556 let Some(entry) = self.menu.entries.get(index) else {
557 return;
558 };
559 if entry.prompts {
560 self.menu.palette = Some(entry.address.clone());
561 } else {
562 self.dispatch
563 .invoke_at(entry.address.clone(), serde_json::Map::new());
564 }
565 }
566
567 fn focused_row(&self) -> Option<(&Id, &dyn Widget)> {
569 let row = self.rows.get(self.focused_index()?)?;
570 Some((&row.id, row.widget()?))
571 }
572
573 fn menu_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) -> bool {
575 if let WindowEvent::CursorMoved { position, .. } = event {
576 self.menu.cursor = ctx.logical(position);
577 }
578 if self.menu.menu.is_open() {
579 self.menu.menu.handle_event(event, ctx);
580 if let Some(index) = self.menu.menu.take_selection() {
581 self.choose(index);
582 }
583 return *ctx.handled;
584 }
585 let target = match event {
586 WindowEvent::MouseInput {
587 state: ElementState::Pressed,
588 button: MouseButton::Right,
589 ..
590 } => self
591 .mounted()
592 .find(|(_, view)| view.widget.get_rect().contains(self.menu.cursor))
593 .map(|(id, _)| (id.clone(), self.menu.cursor)),
594 WindowEvent::KeyboardInput {
595 event:
596 KeyEvent {
597 logical_key,
598 state: ElementState::Pressed,
599 ..
600 },
601 ..
602 } if matches!(logical_key, Key::Named(NamedKey::ContextMenu))
603 || (matches!(logical_key, Key::Named(NamedKey::F10))
604 && ctx.modifiers.state().shift_key()) =>
605 {
606 self.focused_row().map(|(id, widget)| {
607 let rect = widget.get_rect();
608 (id.clone(), Point::new(rect.x0, rect.y1))
609 })
610 }
611 _ => None,
612 };
613 let Some((item, at)) = target else {
614 return false;
615 };
616 if self.open_menu(&item, at) {
617 *ctx.handled = true;
618 return true;
619 }
620 false
621 }
622}
623
624fn holds_focus(widget: &dyn Widget) -> bool {
626 let mut found = widget.is_focused();
627 widget.for_each_child(&mut |child| found |= holds_focus(child));
628 found
629}
630
631fn px(value: f64) -> f32 {
632 #[allow(
633 clippy::cast_possible_truncation,
634 reason = "list offsets are logical pixels, far below f32::MAX"
635 )]
636 let value = value as f32;
637 value
638}
639
640impl<R: Resource> Widget for Each<R> {
641 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
642 if self.stale.replace(false) {
643 self.reconcile();
644 }
645 self.for_each_mounted(|row| row.widget.sync(state, path));
648 }
649
650 fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
653 self.remounts = 0;
654 let mut changed = false;
655 self.for_each_mounted(|row| changed |= row.widget.update(state, path));
656 changed
657 }
658
659 fn set_platform_conventions(&mut self, conventions: ui_core::platform::PlatformConventions) {
663 self.pushed.conventions = Some(conventions);
664 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
665 view.widget.set_platform_conventions(conventions);
666 }
667 }
668
669 fn set_interaction_environment(
670 &mut self,
671 environment: ui_core::interaction::InteractionEnvironment,
672 ) {
673 self.pushed.environment = Some(environment);
674 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
675 view.widget.set_interaction_environment(environment);
676 }
677 }
678
679 fn set_motion_reduced(&mut self, reduced: bool) {
680 self.pushed.motion_reduced = Some(reduced);
681 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
682 view.widget.set_motion_reduced(reduced);
683 }
684 }
685
686 fn build_layout(
687 &mut self,
688 taffy: &mut TaffyTree<()>,
689 theme: &Theme,
690 node_map: &mut HashMap<NodeId, *mut dyn Widget>,
691 ) -> NodeId {
692 let gap = f64::from(theme.gap_standard);
693 if (gap - self.gap).abs() > f64::EPSILON {
694 self.gap = gap;
695 self.relayout_tops(0);
696 }
697 let window = self.window();
698 self.for_each_mounted(|view| view.node = None);
700 for &index in &window {
702 self.ensure_built(index);
703 }
704 for (index, row) in self.rows.iter().enumerate() {
707 let Some(view) = row.view.as_ref() else {
708 continue;
709 };
710 let shown = window.binary_search(&index).is_ok();
711 view.gate.set_mounted(shown);
712 if shown {
713 self.live
714 .with(|state| self.updaters.catch_up(&view.gate, state));
715 }
716 }
717 let mut nodes = Vec::with_capacity(window.len());
718 for &index in &window {
719 let top = px(self.tops.get(index).copied().unwrap_or_default());
720 let Some(row) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
721 continue;
722 };
723 let _ = row.widget.take_structure_dirty();
726 let child = row.widget.build_layout(taffy, theme, node_map);
727 let mut style = taffy.style(child).cloned().unwrap_or_default();
728 style.position = taffy::Position::Absolute;
729 style.inset.left = length(0.0_f32);
730 style.inset.right = length(0.0_f32);
731 style.inset.top = length(top);
732 style.size.width = taffy::Dimension::auto();
733 let _ = taffy.set_style(child, style);
734 row.node = Some(child);
735 nodes.push(child);
736 }
737 self.mounted = window;
738 let style = Style {
739 size: taffy::Size {
740 width: percent(1.0_f32),
741 height: length(px(self.total)),
742 },
743 flex_shrink: 0.0,
744 ..Default::default()
745 };
746 let node = taffy.parent_node(style, &nodes);
747 node_map.insert(node, std::ptr::from_mut::<dyn Widget>(self));
748 self.node = Some(node);
749 self.structure = StructureChange::None;
750 self.geometry_dirty = false;
751 node
752 }
753
754 fn adjust_layout_style(
757 &mut self,
758 taffy: &mut TaffyTree<()>,
759 node: NodeId,
760 _text: &mut TextRenderer,
761 ) -> bool {
762 if self.node != Some(node) {
763 return false;
764 }
765 let width = taffy
766 .layout(node)
767 .map_or(0.0, |layout| f64::from(layout.size.width));
768 let mut measured = (width - self.width).abs() > 0.5;
769 if measured {
770 self.width = width;
772 for row in &mut self.rows {
773 if row.node().is_none() {
774 row.height = None;
775 }
776 }
777 self.recount();
778 }
779 let mut moved = None;
781 for &index in &self.mounted {
782 let Some(row) = self.rows.get_mut(index) else {
783 continue;
784 };
785 let Some(height) = row
786 .node()
787 .and_then(|child| taffy.layout(child).ok())
788 .map(|layout| f64::from(layout.size.height))
789 else {
790 continue;
791 };
792 if row.height.is_none_or(|known| (known - height).abs() > 0.01) {
793 let (sum, count) = self.measured;
794 self.measured = match row.height.replace(height) {
795 Some(known) => (sum - known + height, count),
796 None => (sum + height, count.saturating_add(1)),
797 };
798 moved = Some(moved.map_or(index, |first: usize| first.min(index)));
799 }
800 }
801 if measured {
802 self.relayout_tops(0);
803 } else if let Some(first) = moved {
804 measured = true;
805 self.relayout_tops(first);
806 }
807 if measured || self.geometry_dirty {
808 self.push_geometry(taffy)
809 } else {
810 false
811 }
812 }
813
814 fn apply_layout(&mut self, taffy: &TaffyTree<()>, node: NodeId, origin: Point) {
815 self.rect = ui_core::widget::layout_rect(taffy, node, origin);
816 let inner = self.rect.origin();
817 self.for_each_mounted(|view| {
818 if let Some(child) = view.node {
819 view.widget.apply_layout(taffy, child, inner);
820 }
821 });
822 self.visible = ui_core::widget::layout_clip()
823 .map(|clip| (clip.y0 - self.rect.y0, clip.y1 - self.rect.y0));
824 self.laid_out = true;
825 if self.window_stale() && self.remounts < MAX_REMOUNTS_PER_FRAME {
826 self.remounts = self.remounts.saturating_add(1);
827 self.structure = self.structure.join(StructureChange::Described);
828 }
829 }
830
831 fn draw(
832 &mut self,
833 scene: &mut Scene,
834 transform: Affine,
835 text: &mut TextRenderer,
836 theme: &Theme,
837 presence: &PresenceCtx,
838 ) {
839 self.remounts = 0;
840 self.for_each_mounted(|view| {
841 draw_child(
842 view.widget.as_mut(),
843 scene,
844 transform,
845 text,
846 theme,
847 presence,
848 );
849 });
850 }
851
852 fn handle_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) {
853 if self.menu_event(event, ctx) {
854 return;
855 }
856 let capturing = self.mounted.iter().copied().find(|&index| {
857 self.rows
858 .get(index)
859 .and_then(Row::widget)
860 .is_some_and(Widget::wants_pointer_capture)
861 });
862 if let Some(index) = capturing {
863 if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut())
864 && let Some(child) = view.node
865 {
866 view.widget
867 .handle_event(event, &mut ctx.reborrow(ctx.path, child));
868 }
869 return;
870 }
871 for &index in &self.mounted {
872 let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
873 continue;
874 };
875 let Some(child) = view.node else {
876 continue;
877 };
878 view.widget
879 .handle_event(event, &mut ctx.reborrow(ctx.path, child));
880 if *ctx.handled {
881 break;
882 }
883 }
884 }
885
886 fn wants_pointer_capture(&self) -> bool {
887 self.menu.menu.is_open()
888 || self
889 .mounted()
890 .any(|(_, view)| view.widget.wants_pointer_capture())
891 }
892
893 fn modal_capture(&self) -> Option<Rect> {
894 let mut capture = self.menu.menu.modal_capture();
895 for (_, view) in self.mounted() {
896 if capture.is_none() {
897 capture = view.widget.modal_capture();
898 }
899 }
900 capture
901 }
902
903 fn draw_overlay(
904 &mut self,
905 scene: &mut Scene,
906 transform: Affine,
907 text: &mut TextRenderer,
908 theme: &Theme,
909 presence: &PresenceCtx,
910 ) {
911 self.for_each_mounted(|row| {
912 row.widget
913 .draw_overlay(scene, transform, text, theme, presence);
914 });
915 if self.menu.menu.is_open() {
916 self.menu
917 .menu
918 .draw_overlay(scene, transform, text, theme, presence);
919 }
920 }
921
922 fn take_action_palette_request(&mut self) -> Option<tilo_actions::ActionAddress> {
923 if let Some(address) = self.menu.palette.take() {
924 return Some(address);
925 }
926 let mut request = None;
927 self.for_each_mounted(|row| {
928 if request.is_none() {
929 request = row.widget.take_action_palette_request();
930 }
931 });
932 request
933 }
934
935 fn take_search_request(&mut self) -> bool {
936 let mut requested = false;
937 self.for_each_mounted(|row| requested |= row.widget.take_search_request());
938 requested
939 }
940
941 fn take_structure_dirty(&mut self) -> bool {
942 let mut dirty = std::mem::take(&mut self.structure).rebuilds();
943 self.for_each_mounted(|row| dirty |= row.widget.take_structure_dirty());
944 dirty
945 }
946
947 fn take_structure_change(&mut self) -> StructureChange {
952 let mut change = std::mem::take(&mut self.structure);
953 self.for_each_mounted(|view| change = change.join(view.widget.take_structure_change()));
954 change
955 }
956
957 fn is_layout_dirty(&self) -> bool {
958 self.geometry_dirty
959 || self
960 .mounted()
961 .any(|(_, view)| view.widget.is_layout_dirty())
962 }
963
964 fn layout_needs_measure(&self) -> bool {
965 self.geometry_dirty
966 || self
967 .mounted()
968 .any(|(_, view)| view.widget.layout_needs_measure())
969 }
970
971 fn collect_damage(&self, regions: &mut Vec<Rect>) {
978 self.for_each_child(&mut |child| child.collect_damage(regions));
979 }
980
981 fn children(&self) -> Vec<&dyn Widget> {
982 let open = self
983 .menu
984 .menu
985 .is_open()
986 .then_some(&self.menu.menu as &dyn Widget);
987 self.mounted()
988 .map(|(_, view)| view.widget.as_ref())
989 .chain(open)
990 .collect()
991 }
992
993 fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> {
994 let open = self.menu.menu.is_open();
995 let mut children: Vec<&mut (dyn Widget + '_)> =
996 Vec::with_capacity(self.mounted.len().saturating_add(1));
997 let mut rest: &mut [Row] = &mut self.rows;
999 let mut start = 0_usize;
1000 for &index in &self.mounted {
1001 let Some((row, tail)) = index.checked_sub(start).and_then(|offset| {
1002 std::mem::take(&mut rest)
1003 .get_mut(offset..)
1004 .and_then(<[Row]>::split_first_mut)
1005 }) else {
1006 break;
1007 };
1008 if let Some(view) = row.view.as_mut() {
1009 children.push(view.widget.as_mut());
1010 }
1011 rest = tail;
1012 start = index.saturating_add(1);
1013 }
1014 if open {
1015 children.push(&mut self.menu.menu);
1016 }
1017 children
1018 }
1019
1020 fn accessibility(&self) -> Option<AccessNode> {
1024 let count = self.rows.len();
1025 let items = self
1026 .mounted
1027 .iter()
1028 .filter_map(|&index| {
1029 let widget = self.rows.get(index)?.widget()?;
1030 Some(
1031 AccessNode::new(Role::ListItem)
1032 .with_position(index.saturating_add(1), count)
1033 .with_bounds(widget.get_rect()),
1034 )
1035 })
1036 .collect();
1037 Some(AccessNode::new(Role::List).with_children(items))
1038 }
1039
1040 fn accessibility_child_parent(&self, child_index: usize) -> Option<usize> {
1041 (child_index < self.mounted.len()).then_some(child_index)
1042 }
1043
1044 fn name(&self) -> &'static str {
1045 "Each"
1046 }
1047
1048 fn get_rect(&self) -> Rect {
1049 self.rect
1050 }
1051}