1use std::cell::Cell;
5use std::collections::HashMap;
6use std::ops::Range;
7use std::rc::Rc;
8use ui_core::widget::{AppModel, ModelPath, Remount, 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;
126const ROW_SPACE: f32 = 1.0e6;
130
131pub struct Each<R: Resource> {
155 live: Live<R>,
156 dispatch: Dispatcher<R>,
157 menu: RowMenu,
158 ids: Ids<R>,
159 build: BuildRow,
160 rows: Vec<Row>,
161 builds: Builds,
162 pushed: Pushed,
163 stale: Rc<Cell<bool>>,
166 updaters: Updaters<R>,
168 mounted: Vec<usize>,
170 tops: Vec<f64>,
172 total: f64,
173 measured: (f64, u32),
176 estimated: f64,
177 gap: f64,
178 width: f64,
180 node: Option<NodeId>,
181 track: Option<NodeId>,
185 rect: Rect,
186 visible: Option<(f64, f64)>,
189 laid_out: bool,
190 remounts: u8,
191 structure: StructureChange,
194 remount: bool,
199 geometry_dirty: bool,
200}
201
202impl<R: Resource> Each<R> {
203 pub(crate) fn new(
204 live: Live<R>,
205 dispatch: Dispatcher<R>,
206 ids: Ids<R>,
207 stale: Rc<Cell<bool>>,
208 updaters: Updaters<R>,
209 build: BuildRow,
210 ) -> Self {
211 let mut each = Self {
212 stale,
213 updaters,
214 live,
215 dispatch,
216 menu: RowMenu::default(),
217 ids,
218 build,
219 rows: Vec::new(),
220 builds: Builds::Unknown,
221 pushed: Pushed::default(),
222 mounted: Vec::new(),
223 tops: Vec::new(),
224 total: 0.0,
225 measured: (0.0, 0),
226 estimated: ESTIMATED_ROW_HEIGHT,
227 gap: 0.0,
228 width: 0.0,
229 node: None,
230 track: None,
231 rect: Rect::ZERO,
232 visible: None,
233 laid_out: false,
234 remounts: 0,
235 structure: StructureChange::None,
236 remount: false,
237 geometry_dirty: false,
238 };
239 each.reconcile();
240 each
241 }
242
243 fn reconcile(&mut self) {
244 let ids = self.live.with(|state| (self.ids)(state));
245 let unchanged = ids.len() == self.rows.len()
246 && ids.iter().zip(&self.rows).all(|(id, row)| *id == row.id);
247 if unchanged {
248 return;
249 }
250 let was_mounted: Vec<Id> = self
251 .mounted
252 .iter()
253 .filter_map(|&index| self.rows.get(index))
254 .map(|row| row.id.clone())
255 .collect();
256 let mut old: HashMap<Id, Row> = self
257 .rows
258 .drain(..)
259 .map(|row| (row.id.clone(), row))
260 .collect();
261 self.rows = ids
263 .into_iter()
264 .map(|id| {
265 old.remove(&id).unwrap_or(Row {
266 id,
267 height: None,
268 view: None,
269 })
270 })
271 .collect();
272 self.mounted = self
277 .rows
278 .iter()
279 .enumerate()
280 .filter(|(_, row)| row.node().is_some())
281 .map(|(index, _)| index)
282 .collect();
283 let still_mounted = self.mounted().map(|(id, _)| id);
284 if !still_mounted.eq(was_mounted.iter()) {
285 self.structure = StructureChange::Rebuild;
286 }
287 if self.builds == Builds::Every {
288 self.build_all();
289 } else if self.builds == Builds::Unknown && !self.rows.is_empty() {
290 self.ensure_built(0);
293 }
294 self.recount();
295 self.relayout_tops(0);
296 self.geometry_dirty = true;
297 }
298
299 fn ensure_built(&mut self, index: usize) {
302 let Some(row) = self.rows.get_mut(index) else {
303 return;
304 };
305 if row.view.is_some() {
306 return;
307 }
308 let alive = Rc::new(());
309 let gate = Gate::mounted();
310 let mut widget = (self.build)(&row.id, &alive, &gate);
311 self.pushed.apply(widget.as_mut());
312 let reads_unmounted = gate.reads_unmounted();
313 row.view = Some(Box::new(RowView {
314 _alive: alive,
315 gate,
316 widget,
317 node: None,
318 }));
319 if reads_unmounted && self.builds != Builds::Every {
320 self.builds = Builds::Every;
321 self.build_all();
322 } else if self.builds == Builds::Unknown {
323 self.builds = Builds::OnMount;
324 }
325 }
326
327 fn build_all(&mut self) {
328 for index in 0..self.rows.len() {
329 self.ensure_built(index);
330 }
331 }
332
333 fn open_window(&mut self, window: &[usize]) {
338 for &index in window {
339 self.ensure_built(index);
340 }
341 for (index, row) in self.rows.iter().enumerate() {
342 let Some(view) = row.view.as_ref() else {
343 continue;
344 };
345 let shown = window.binary_search(&index).is_ok();
346 view.gate.set_mounted(shown);
347 if shown {
348 self.live
349 .with(|state| self.updaters.catch_up(&view.gate, state));
350 }
351 }
352 }
353
354 fn estimate(&self) -> f64 {
356 let (sum, count) = self.measured;
357 if count == 0 {
358 ESTIMATED_ROW_HEIGHT
359 } else {
360 sum / f64::from(count)
361 }
362 }
363
364 fn recount(&mut self) {
366 self.measured = self
367 .rows
368 .iter()
369 .filter_map(|row| row.height)
370 .fold((0.0, 0_u32), |(sum, count), height| {
371 (sum + height, count.saturating_add(1))
372 });
373 }
374
375 fn relayout_tops(&mut self, from: usize) {
378 let estimate = self.estimate();
379 let from = if (estimate - self.estimated).abs() > f64::EPSILON
380 || self.tops.len() != self.rows.len()
381 {
382 0
383 } else {
384 from.min(self.rows.len())
385 };
386 self.estimated = estimate;
387 self.tops.truncate(from);
388 let mut y = from
389 .checked_sub(1)
390 .and_then(|last| {
391 let top = self.tops.get(last)?;
392 let row = self.rows.get(last)?;
393 Some(top + row.height.unwrap_or(estimate))
394 })
395 .unwrap_or(0.0);
396 for (index, row) in self.rows.iter().enumerate().skip(from) {
397 if index > 0 {
398 y += self.gap;
399 }
400 self.tops.push(y);
401 y += row.height.unwrap_or(estimate);
402 }
403 self.total = y;
404 }
405
406 fn rows_between(&self, top: f64, bottom: f64) -> Range<usize> {
409 let first = self
410 .tops
411 .partition_point(|&row_top| row_top <= top)
412 .saturating_sub(1);
413 let end = self.tops.partition_point(|&row_top| row_top < bottom);
414 first..end.max(first)
415 }
416
417 fn overscan(&self) -> f64 {
419 self.visible.map_or(0.0, |(top, bottom)| {
420 ((bottom - top) * 0.5).max(MIN_OVERSCAN)
421 })
422 }
423
424 fn focused_index(&self) -> Option<usize> {
426 self.mounted.iter().copied().find(|&index| {
427 self.rows
428 .get(index)
429 .and_then(Row::widget)
430 .is_some_and(holds_focus)
431 })
432 }
433
434 fn around_focus(&self) -> Range<usize> {
437 self.focused_index().map_or(0..0, |index| {
438 index.saturating_sub(1)..index.saturating_add(2).min(self.rows.len())
439 })
440 }
441
442 fn window(&self) -> Vec<usize> {
446 let count = self.rows.len();
447 if !self.laid_out {
448 return (0..count.min(INITIAL_ROWS)).collect();
449 }
450 let Some((top, bottom)) = self.visible else {
451 return (0..count).collect();
452 };
453 let overscan = self.overscan();
454 let mut window: Vec<usize> = self
455 .rows_between(top - overscan, bottom + overscan)
456 .collect();
457 window.extend(self.around_focus());
458 window.extend(self.mounted.iter().copied().filter(|&index| {
459 self.rows
460 .get(index)
461 .and_then(Row::widget)
462 .is_some_and(|widget| {
463 widget.wants_pointer_capture() || widget.modal_capture().is_some()
464 })
465 }));
466 window.sort_unstable();
467 window.dedup();
468 window
469 }
470
471 fn window_stale(&self) -> bool {
475 let Some((top, bottom)) = self.visible else {
476 return self.mounted.len() != self.rows.len();
477 };
478 let mounts = |rows: Range<usize>| {
479 rows.is_empty()
480 || self.mounted.binary_search(&rows.start).is_ok_and(|at| {
481 let last = at.saturating_add(rows.len()).saturating_sub(1);
482 self.mounted.get(last) == Some(&rows.end.saturating_sub(1))
483 })
484 };
485 let overscan = self.overscan();
486 let needed = self.rows_between(top - overscan / 4.0, bottom + overscan / 4.0);
487 let wanted = self.rows_between(top - overscan, bottom + overscan).len();
488 !mounts(needed)
489 || !mounts(self.around_focus())
490 || self.mounted.len() > wanted.saturating_mul(2).saturating_add(8)
491 }
492
493 pub fn row_ids(&self) -> impl Iterator<Item = &Id> {
495 self.rows.iter().map(|row| &row.id)
496 }
497
498 #[must_use]
500 pub fn mounted_rows(&self) -> usize {
501 self.mounted.len()
502 }
503
504 fn mounted(&self) -> impl Iterator<Item = (&Id, &RowView)> {
506 self.mounted.iter().filter_map(|&index| {
507 let row = self.rows.get(index)?;
508 Some((&row.id, row.view.as_deref()?))
509 })
510 }
511
512 fn for_each_mounted(&mut self, mut visit: impl FnMut(&mut RowView)) {
513 for &index in &self.mounted {
514 if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) {
515 visit(view);
516 }
517 }
518 }
519
520 fn push_geometry(&mut self, taffy: &mut TaffyTree<()>) -> bool {
523 let mut changed = false;
524 if let Some(node) = self.node
525 && let Ok(style) = taffy.style(node)
526 && style.size.height != length(px(self.total))
527 {
528 let mut style = style.clone();
529 style.size.height = length(px(self.total));
530 changed |= taffy.set_style(node, style).is_ok();
531 }
532 for &index in &self.mounted {
533 let Some(child) = self.rows.get(index).and_then(Row::node) else {
534 continue;
535 };
536 let top = length(px(self.tops.get(index).copied().unwrap_or_default()));
537 if let Ok(style) = taffy.style(child)
538 && style.inset.top != top
539 {
540 let mut style = style.clone();
541 style.inset.top = top;
542 changed |= taffy.set_style(child, style).is_ok();
543 }
544 }
545 self.geometry_dirty = false;
546 changed
547 }
548
549 fn open_menu(&mut self, item: &Id, at: Point) -> bool {
552 let children = self.live.with(|state| R::children(state));
553 let Some(resource) = children.iter().find(|child| {
554 tilo_resource::items::item_of_resource::<R>(child.id.as_str())
555 .is_some_and(|(_, id)| &id == item)
556 }) else {
557 return false;
558 };
559 let read_only = self.dispatch.read_only();
560 let mut menu = ContextMenu::new();
561 let mut entries = Vec::new();
562 for spec in &resource.actions {
563 let address = tilo_actions::ActionAddress::new(resource.id.as_str(), spec.id.as_str());
564 let entry = if spec.contract.risk == tilo_actions::RiskLevel::Destructive {
565 MenuItem::destructive_selection(spec.label.clone())
566 } else {
567 MenuItem::selection(spec.label.clone())
568 };
569 menu.push(entry.for_action(address.clone()).with_enabled(!read_only));
570 entries.push(MenuEntry {
571 address,
572 prompts: !spec.params().is_empty(),
573 });
574 }
575 let selected = ui_core::shared_selection::pointed_selection(
580 ui_core::shared_selection::subtree_selection(&*self),
581 Some(at),
582 );
583 let mut about = ui_core::shared_selection::SharedSelection::of_text(&resource.label)
584 .at(tilo_actions::SelectionPlace::item(item.as_str()))
585 .in_resource(tilo_actions::ActionResource {
586 id: resource.id.0.clone(),
587 kind: resource.kind.clone(),
588 label: resource.label.clone(),
589 });
590 if let Some((_, view)) = self.mounted().find(|(id, _)| *id == item) {
591 about = about.within(view.widget.get_rect());
592 }
593 menu.set_about(Some(selected.unwrap_or(about)));
594 #[allow(
595 clippy::cast_possible_truncation,
596 reason = "a menu anchor is a logical pixel position on screen"
597 )]
598 menu.open_at([at.x as f32, at.y as f32]);
599 if menu.items().is_empty() {
600 return false;
601 }
602 self.menu.menu = menu;
603 self.menu.entries = entries;
604 true
605 }
606
607 fn choose(&mut self, index: usize) {
610 let Some(MenuEntry { address, prompts }) = self.menu.entries.get(index) else {
611 return;
612 };
613 if *prompts {
614 self.menu.palette = Some(address.clone());
615 } else {
616 self.dispatch
617 .invoke_at(address.clone(), serde_json::Map::new());
618 }
619 }
620
621 fn focused_row(&self) -> Option<(&Id, &dyn Widget)> {
623 let row = self.rows.get(self.focused_index()?)?;
624 Some((&row.id, row.widget()?))
625 }
626
627 fn menu_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) -> bool {
629 if let WindowEvent::CursorMoved { position, .. } = event {
630 self.menu.cursor = ctx.logical(position);
631 }
632 if self.menu.menu.is_open() {
633 self.menu.menu.handle_event(event, ctx);
634 if let Some(index) = self.menu.menu.take_selection() {
635 self.choose(index);
636 }
637 return *ctx.handled;
638 }
639 let target = match event {
640 WindowEvent::MouseInput {
641 state: ElementState::Pressed,
642 button: MouseButton::Right,
643 ..
644 } => self
645 .mounted()
646 .find(|(_, view)| view.widget.get_rect().contains(self.menu.cursor))
647 .map(|(id, _)| (id.clone(), self.menu.cursor)),
648 WindowEvent::KeyboardInput {
649 event:
650 KeyEvent {
651 logical_key,
652 state: ElementState::Pressed,
653 ..
654 },
655 ..
656 } if matches!(logical_key, Key::Named(NamedKey::ContextMenu))
657 || (matches!(logical_key, Key::Named(NamedKey::F10))
658 && ctx.modifiers.state().shift_key()) =>
659 {
660 self.focused_row().map(|(id, widget)| {
661 let rect = widget.get_rect();
662 (id.clone(), Point::new(rect.x0, rect.y1))
663 })
664 }
665 _ => None,
666 };
667 let Some((item, at)) = target else {
668 return false;
669 };
670 if self.open_menu(&item, at) {
671 *ctx.handled = true;
672 return true;
673 }
674 false
675 }
676}
677
678fn place_row(taffy: &mut TaffyTree<()>, node: NodeId, top: f32) {
681 let mut style = taffy.style(node).cloned().unwrap_or_default();
682 style.position = taffy::Position::Absolute;
683 style.inset.left = length(0.0_f32);
684 style.inset.right = length(0.0_f32);
685 style.inset.top = length(top);
686 style.size.width = taffy::Dimension::auto();
687 let _ = taffy.set_style(node, style);
688}
689
690fn holds_focus(widget: &dyn Widget) -> bool {
692 let mut found = widget.is_focused();
693 widget.for_each_child(&mut |child| found |= holds_focus(child));
694 found
695}
696
697fn px(value: f64) -> f32 {
698 #[allow(
699 clippy::cast_possible_truncation,
700 reason = "list offsets are logical pixels, far below f32::MAX"
701 )]
702 let value = value as f32;
703 value
704}
705
706impl<R: Resource> Widget for Each<R> {
707 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
708 if self.stale.replace(false) {
709 self.reconcile();
710 }
711 self.for_each_mounted(|row| row.widget.sync(state, path));
714 }
715
716 fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
719 self.remounts = 0;
720 let mut changed = false;
721 self.for_each_mounted(|row| changed |= row.widget.update(state, path));
722 changed
723 }
724
725 fn set_platform_conventions(&mut self, conventions: ui_core::platform::PlatformConventions) {
729 self.pushed.conventions = Some(conventions);
730 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
731 view.widget.set_platform_conventions(conventions);
732 }
733 }
734
735 fn set_interaction_environment(
736 &mut self,
737 environment: ui_core::interaction::InteractionEnvironment,
738 ) {
739 self.pushed.environment = Some(environment);
740 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
741 view.widget.set_interaction_environment(environment);
742 }
743 }
744
745 fn set_motion_reduced(&mut self, reduced: bool) {
746 self.pushed.motion_reduced = Some(reduced);
747 for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
748 view.widget.set_motion_reduced(reduced);
749 }
750 }
751
752 fn build_layout(
753 &mut self,
754 taffy: &mut TaffyTree<()>,
755 theme: &Theme,
756 node_map: &mut HashMap<NodeId, *mut dyn Widget>,
757 ) -> NodeId {
758 let gap = f64::from(theme.gap_standard);
759 if (gap - self.gap).abs() > f64::EPSILON {
760 self.gap = gap;
761 self.relayout_tops(0);
762 }
763 let window = self.window();
764 self.for_each_mounted(|view| view.node = None);
766 self.open_window(&window);
767 let mut nodes = Vec::with_capacity(window.len());
768 for &index in &window {
769 let top = px(self.tops.get(index).copied().unwrap_or_default());
770 let Some(row) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
771 continue;
772 };
773 let _ = row.widget.take_structure_dirty();
776 let child = row.widget.build_layout(taffy, theme, node_map);
777 place_row(taffy, child, top);
778 row.node = Some(child);
779 nodes.push(child);
780 }
781 self.mounted = window;
782 let track = taffy.parent_node(
783 Style {
784 position: taffy::Position::Absolute,
785 inset: taffy::Rect {
786 left: length(0.0_f32),
787 right: length(0.0_f32),
788 top: length(0.0_f32),
789 bottom: taffy::LengthPercentageAuto::auto(),
790 },
791 size: taffy::Size {
792 width: taffy::Dimension::auto(),
793 height: length(ROW_SPACE),
794 },
795 ..Default::default()
796 },
797 &nodes,
798 );
799 let clip = taffy.parent_node(
803 Style {
804 position: taffy::Position::Absolute,
805 inset: taffy::Rect {
806 left: length(0.0_f32),
807 right: length(0.0_f32),
808 top: length(0.0_f32),
809 bottom: length(0.0_f32),
810 },
811 overflow: taffy::Point {
812 x: taffy::Overflow::Visible,
813 y: taffy::Overflow::Clip,
814 },
815 ..Default::default()
816 },
817 &[track],
818 );
819 let style = Style {
820 size: taffy::Size {
821 width: percent(1.0_f32),
822 height: length(px(self.total)),
823 },
824 flex_shrink: 0.0,
825 ..Default::default()
826 };
827 let node = taffy.parent_node(style, &[clip]);
828 node_map.insert(node, std::ptr::from_mut::<dyn Widget>(self));
829 self.node = Some(node);
830 self.track = Some(track);
831 self.structure = StructureChange::None;
832 self.remount = false;
833 self.geometry_dirty = false;
834 node
835 }
836
837 fn remount_children(&mut self, remount: &mut Remount<'_>) -> bool {
841 let mut applied = true;
842 if !self.remount {
843 self.for_each_mounted(|view| {
844 applied = applied && view.widget.remount_children(remount);
845 });
846 return applied;
847 }
848 let (Some(node), Some(track)) = (self.node, self.track) else {
849 return false;
850 };
851 let window = self.window();
852 for &index in &self.mounted {
853 if window.binary_search(&index).is_err()
854 && let Some(child) = self
855 .rows
856 .get_mut(index)
857 .and_then(|row| row.view.as_mut())
858 .and_then(|view| view.node.take())
859 {
860 remount.unmount(child);
861 }
862 }
863 self.open_window(&window);
864 let mut nodes = Vec::with_capacity(window.len());
865 for &index in &window {
866 let top = px(self.tops.get(index).copied().unwrap_or_default());
867 let Some(row) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
868 continue;
869 };
870 let child = if let Some(child) = row.node {
871 applied = applied && row.widget.remount_children(remount);
872 child
873 } else {
874 let _ = row.widget.take_structure_dirty();
875 let child = remount.mount(node, row.widget.as_mut());
876 place_row(remount.taffy(), child, top);
877 row.node = Some(child);
878 child
879 };
880 nodes.push(child);
881 }
882 self.mounted = window;
883 self.remount = false;
884 self.push_geometry(remount.taffy());
887 applied && remount.set_children(track, &nodes).is_ok()
888 }
889
890 fn adjust_layout_style(
893 &mut self,
894 taffy: &mut TaffyTree<()>,
895 node: NodeId,
896 _text: &mut TextRenderer,
897 ) -> bool {
898 if self.node != Some(node) {
899 return false;
900 }
901 let width = taffy
902 .layout(node)
903 .map_or(0.0, |layout| f64::from(layout.size.width));
904 let mut measured = (width - self.width).abs() > 0.5;
905 if measured {
906 self.width = width;
908 for row in &mut self.rows {
909 if row.node().is_none() {
910 row.height = None;
911 }
912 }
913 self.recount();
914 }
915 let mut moved = None;
917 for &index in &self.mounted {
918 let Some(row) = self.rows.get_mut(index) else {
919 continue;
920 };
921 let Some(height) = row
922 .node()
923 .and_then(|child| taffy.layout(child).ok())
924 .map(|layout| f64::from(layout.size.height))
925 else {
926 continue;
927 };
928 if row.height.is_none_or(|known| (known - height).abs() > 0.01) {
929 let (sum, count) = self.measured;
930 self.measured = match row.height.replace(height) {
931 Some(known) => (sum - known + height, count),
932 None => (sum + height, count.saturating_add(1)),
933 };
934 moved = Some(moved.map_or(index, |first: usize| first.min(index)));
935 }
936 }
937 if measured {
938 self.relayout_tops(0);
939 } else if let Some(first) = moved {
940 measured = true;
941 self.relayout_tops(first);
942 }
943 if measured || self.geometry_dirty {
944 self.push_geometry(taffy)
945 } else {
946 false
947 }
948 }
949
950 fn apply_layout(&mut self, taffy: &TaffyTree<()>, node: NodeId, origin: Point) {
951 self.rect = ui_core::widget::layout_rect(taffy, node, origin);
952 let inner = self.rect.origin();
953 self.for_each_mounted(|view| {
954 if let Some(child) = view.node {
955 view.widget.apply_layout(taffy, child, inner);
956 }
957 });
958 self.visible = ui_core::widget::layout_clip()
959 .map(|clip| (clip.y0 - self.rect.y0, clip.y1 - self.rect.y0));
960 self.laid_out = true;
961 if self.window_stale() && self.remounts < MAX_REMOUNTS_PER_FRAME {
962 self.remounts = self.remounts.saturating_add(1);
963 self.remount = true;
964 }
965 }
966
967 fn draw(
968 &mut self,
969 scene: &mut Scene,
970 transform: Affine,
971 text: &mut TextRenderer,
972 theme: &Theme,
973 presence: &PresenceCtx,
974 ) {
975 self.remounts = 0;
976 self.for_each_mounted(|view| {
977 draw_child(
978 view.widget.as_mut(),
979 scene,
980 transform,
981 text,
982 theme,
983 presence,
984 );
985 });
986 }
987
988 fn handle_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) {
989 if self.menu_event(event, ctx) {
990 return;
991 }
992 let capturing = self.mounted.iter().copied().find(|&index| {
993 self.rows
994 .get(index)
995 .and_then(Row::widget)
996 .is_some_and(ui_core::widget::holds_pointer)
997 });
998 if let Some(index) = capturing {
999 if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut())
1000 && let Some(child) = view.node
1001 {
1002 view.widget
1003 .handle_event(event, &mut ctx.reborrow(ctx.path, child));
1004 }
1005 return;
1006 }
1007 for &index in &self.mounted {
1008 let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
1009 continue;
1010 };
1011 let Some(child) = view.node else {
1012 continue;
1013 };
1014 view.widget
1015 .handle_event(event, &mut ctx.reborrow(ctx.path, child));
1016 if *ctx.handled {
1017 break;
1018 }
1019 }
1020 }
1021
1022 fn wants_pointer_capture(&self) -> bool {
1023 self.menu.menu.is_open()
1024 || self
1025 .mounted()
1026 .any(|(_, view)| view.widget.wants_pointer_capture())
1027 }
1028
1029 fn modal_capture(&self) -> Option<Rect> {
1030 let mut capture = self.menu.menu.modal_capture();
1031 for (_, view) in self.mounted() {
1032 if capture.is_none() {
1033 capture = view.widget.modal_capture();
1034 }
1035 }
1036 capture
1037 }
1038
1039 fn draw_overlay(
1040 &mut self,
1041 scene: &mut Scene,
1042 transform: Affine,
1043 text: &mut TextRenderer,
1044 theme: &Theme,
1045 presence: &PresenceCtx,
1046 ) {
1047 self.for_each_mounted(|row| {
1048 row.widget
1049 .draw_overlay(scene, transform, text, theme, presence);
1050 });
1051 self.menu
1054 .menu
1055 .draw_overlay(scene, transform, text, theme, presence);
1056 }
1057
1058 fn take_action_palette_request(&mut self) -> Option<tilo_actions::ActionAddress> {
1059 if let Some(address) = self.menu.palette.take() {
1060 return Some(address);
1061 }
1062 let mut request = None;
1063 self.for_each_mounted(|row| {
1064 if request.is_none() {
1065 request = row.widget.take_action_palette_request();
1066 }
1067 });
1068 request
1069 }
1070
1071 fn take_search_request(&mut self) -> bool {
1072 let mut requested = false;
1073 self.for_each_mounted(|row| requested |= row.widget.take_search_request());
1074 requested
1075 }
1076
1077 fn take_structure_dirty(&mut self) -> bool {
1078 let mut dirty = std::mem::take(&mut self.structure).rebuilds() || self.remount;
1079 self.for_each_mounted(|row| dirty |= row.widget.take_structure_dirty());
1080 dirty
1081 }
1082
1083 fn take_structure_change(&mut self) -> StructureChange {
1089 let mut change = std::mem::take(&mut self.structure);
1090 if self.remount {
1091 change = change.join(StructureChange::Children);
1092 }
1093 self.for_each_mounted(|view| change = change.join(view.widget.take_structure_change()));
1094 change
1095 }
1096
1097 fn is_layout_dirty(&self) -> bool {
1098 self.geometry_dirty
1099 || self
1100 .mounted()
1101 .any(|(_, view)| view.widget.is_layout_dirty())
1102 }
1103
1104 fn layout_needs_measure(&self) -> bool {
1105 self.geometry_dirty
1106 || self
1107 .mounted()
1108 .any(|(_, view)| view.widget.layout_needs_measure())
1109 }
1110
1111 fn collect_damage(&self, regions: &mut Vec<Rect>) {
1118 self.for_each_child(&mut |child| child.collect_damage(regions));
1119 }
1120
1121 fn children(&self) -> Vec<&dyn Widget> {
1122 let open = self
1123 .menu
1124 .menu
1125 .is_open()
1126 .then_some(&self.menu.menu as &dyn Widget);
1127 self.mounted()
1128 .map(|(_, view)| view.widget.as_ref())
1129 .chain(open)
1130 .collect()
1131 }
1132
1133 fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> {
1134 let open = self.menu.menu.is_open();
1135 let mut children: Vec<&mut (dyn Widget + '_)> =
1136 Vec::with_capacity(self.mounted.len().saturating_add(1));
1137 let mut rest: &mut [Row] = &mut self.rows;
1139 let mut start = 0_usize;
1140 for &index in &self.mounted {
1141 let Some((row, tail)) = index.checked_sub(start).and_then(|offset| {
1142 std::mem::take(&mut rest)
1143 .get_mut(offset..)
1144 .and_then(<[Row]>::split_first_mut)
1145 }) else {
1146 break;
1147 };
1148 if let Some(view) = row.view.as_mut() {
1149 children.push(view.widget.as_mut());
1150 }
1151 rest = tail;
1152 start = index.saturating_add(1);
1153 }
1154 if open {
1155 children.push(&mut self.menu.menu);
1156 }
1157 children
1158 }
1159
1160 fn accessibility(&self) -> Option<AccessNode> {
1164 let count = self.rows.len();
1165 let items = self
1166 .mounted
1167 .iter()
1168 .filter_map(|&index| {
1169 let widget = self.rows.get(index)?.widget()?;
1170 Some(
1171 AccessNode::new(Role::ListItem)
1172 .with_position(index.saturating_add(1), count)
1173 .with_bounds(widget.get_rect()),
1174 )
1175 })
1176 .collect();
1177 Some(AccessNode::new(Role::List).with_children(items))
1178 }
1179
1180 fn accessibility_child_parent(&self, child_index: usize) -> Option<usize> {
1181 (child_index < self.mounted.len()).then_some(child_index)
1182 }
1183
1184 fn name(&self) -> &'static str {
1185 "Each"
1186 }
1187
1188 fn get_rect(&self) -> Rect {
1189 self.rect
1190 }
1191}