diff --git a/src/widget.rs b/src/widget.rs index 638234d..9fa4d03 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -5,9 +5,9 @@ use std::clone::Clone; use std::fmt::{self, Debug}; use std::ops::{ControlFlow, Deref, DerefMut}; use std::panic::UnwindSafe; -use std::slice; use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, MutexGuard}; +use std::{slice, vec}; use alot::LotId; use intentional::Assert; @@ -37,7 +37,7 @@ use crate::styles::{ }; use crate::tree::Tree; use crate::utils::IgnorePoison; -use crate::value::{Dynamic, IntoDynamic, IntoValue, Validation, Value}; +use crate::value::{Dynamic, Generation, IntoDynamic, IntoValue, Validation, Value}; use crate::widgets::checkbox::{Checkable, CheckboxState}; use crate::widgets::layers::{OverlayLayer, Tooltipped}; use crate::widgets::{ @@ -1506,6 +1506,12 @@ impl ManagedWidget { self.widget.id() } + /// Returns the underlying widget instance + #[must_use] + pub const fn instance(&self) -> &WidgetInstance { + &self.widget + } + /// Returns the next widget to focus after this widget. /// /// This function returns the value set in @@ -1756,6 +1762,46 @@ impl Children { pub fn into_layers(self) -> Layers { Layers::new(self) } + + /// Synchronizes this list of children with another collection. + /// + /// This function updates `collection` by calling `change_fn` for each + /// operation that needs to be performed to synchronize. The algorithm first + /// mounts/inserts all new children before sending a final change to + /// `change_fn`: [`ChildrenSyncChange::Truncate`]. + pub fn synchronize_with( + &self, + collection: &mut Collection, + get_index: impl Fn(&Collection, usize) -> Option<&WidgetInstance>, + mut change_fn: impl FnMut(&mut Collection, ChildrenSyncChange), + ) { + for (index, widget) in self.iter().enumerate() { + if get_index(collection, index).map_or(true, |child| child != widget) { + // These entries do not match. See if we can find the + // new id somewhere else, if so we can swap the entries. + if let Some(Some(swap_index)) = (index + 1..usize::MAX).find_map(|index| { + if let Some(child) = get_index(collection, index) { + if widget == child { + Some(Some(index)) + } else { + None + } + } else { + Some(None) + } + }) { + change_fn(collection, ChildrenSyncChange::Swap(index, swap_index)); + } else { + change_fn( + collection, + ChildrenSyncChange::Insert(index, widget.clone()), + ); + } + } + } + + change_fn(collection, ChildrenSyncChange::Truncate(self.len())); + } } impl Debug for Children { @@ -1819,6 +1865,118 @@ impl<'a> IntoIterator for &'a Children { } } +/// A change to perform during [`Children::synchronize_with`]. +pub enum ChildrenSyncChange { + /// Insert a new widget at the given index. + Insert(usize, WidgetInstance), + /// Swap the widgets at the given indices. + Swap(usize, usize), + /// Truncate the collection to the length given. + Truncate(usize), +} + +/// A collection of mounted children. +/// +/// This collection is a helper aimed at making it easier to build widgets that +/// contain multiple children widgets. It is used in conjunction with a +/// `Value`. +#[derive(Debug)] +pub struct MountedChildren { + generation: Option, + children: Vec, +} + +impl MountedChildren +where + T: MountableChild, +{ + /// Mounts and unmounts all children needed to be in sync with `children`. + pub fn synchronize_with( + &mut self, + children: &Value, + context: &mut EventContext<'_, '_>, + ) { + let current_generation = children.generation(); + if current_generation.map_or_else( + || children.map(Children::len) != self.children.len(), + |gen| Some(gen) != self.generation, + ) { + self.generation = current_generation; + children.map(|children| { + children.synchronize_with( + self, + |this, index| { + this.children + .get(index) + .map(|mounted| mounted.widget().instance()) + }, + |this, change| match change { + ChildrenSyncChange::Insert(index, widget) => { + this.children + .insert(index, T::mount(context.push_child(widget), this, index)); + } + ChildrenSyncChange::Swap(a, b) => { + this.children.swap(a, b); + } + ChildrenSyncChange::Truncate(length) => { + for removed in this.children.drain(length..) { + context.remove_child(&removed.unmount()); + } + } + }, + ); + }); + } + } + + /// Returns an iterator that contains every widget in this collection. + /// + /// When the iterator is dropped, this collection will be empty. + pub fn drain(&mut self) -> vec::Drain<'_, T> { + self.generation = None; + self.children.drain(..) + } + + /// Returns a reference to the children. + #[must_use] + pub fn children(&self) -> &[T] { + &self.children + } +} + +impl Default for MountedChildren { + fn default() -> Self { + Self { + generation: None, + children: Vec::default(), + } + } +} + +/// A child in a [`MountedChildren`] collection. +pub trait MountableChild: Sized { + /// Returns the mounted representation of `widget`. + fn mount(widget: ManagedWidget, into: &MountedChildren, index: usize) -> Self; + /// Returns the widget and performs any other cleanup for this widget being unmounted.q + fn unmount(self) -> ManagedWidget; + /// Returns a reference to the widget. + fn widget(&self) -> &ManagedWidget; +} + +impl MountableChild for ManagedWidget { + fn mount(widget: ManagedWidget, _into: &MountedChildren, _index: usize) -> Self { + widget + } + + fn widget(&self) -> &ManagedWidget { + self + } + + fn unmount(self) -> ManagedWidget { + self + } +} + /// A child widget #[derive(Clone)] pub enum WidgetRef { diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 3a87cd8..91bc296 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -14,9 +14,10 @@ use crate::animation::easings::EaseOutQuadradic; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::utils::IgnorePoison; -use crate::value::{Dynamic, DynamicGuard, Generation, IntoValue, Value}; +use crate::value::{Dynamic, DynamicGuard, IntoValue, Value}; use crate::widget::{ - Callback, Children, MakeWidget, ManagedWidget, Widget, WidgetId, WidgetRef, WrapperWidget, + Callback, Children, MakeWidget, ManagedWidget, MountedChildren, Widget, WidgetId, WidgetRef, + WrapperWidget, }; use crate::widgets::container::ContainerShadow; use crate::ConstraintLimit; @@ -26,8 +27,7 @@ use crate::ConstraintLimit; pub struct Layers { /// The children that are laid out as layers with index 0 being the lowest (bottom). pub children: Value, - mounted: Vec, - mounted_generation: Option, + mounted: MountedChildren, } impl Layers { @@ -35,51 +35,13 @@ impl Layers { pub fn new(children: impl IntoValue) -> Self { Self { children: children.into_value(), - mounted: Vec::new(), - mounted_generation: None, + mounted: MountedChildren::default(), } } fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { - let current_generation = self.children.generation(); self.children.invalidate_when_changed(context); - if current_generation.map_or_else( - || self.children.map(Children::len) != self.mounted.len(), - |gen| Some(gen) != self.mounted_generation, - ) { - self.mounted_generation = self.children.generation(); - self.children.map(|children| { - for (index, widget) in children.iter().enumerate() { - if self - .mounted - .get(index) - .map_or(true, |child| &child.widget != widget) - { - // These entries do not match. See if we can find the - // new id somewhere else, if so we can swap the entries. - if let Some((swap_index, _)) = self - .mounted - .iter() - .enumerate() - .skip(index + 1) - .find(|(_, child)| &child.widget == widget) - { - self.mounted.swap(index, swap_index); - } else { - // This is a brand new child. - self.mounted - .insert(index, context.push_child(widget.clone())); - } - } - } - - // Any children remaining at the end of this process are ones - // that have been removed. - for removed in self.mounted.drain(children.len()..) { - context.remove_child(&removed); - } - }); - } + self.mounted.synchronize_with(&self.children, context); } } @@ -87,7 +49,7 @@ impl Widget for Layers { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { self.synchronize_children(&mut context.as_event_context()); - for mounted in &self.mounted { + for mounted in self.mounted.children() { context.for_other(mounted).redraw(); } } @@ -112,7 +74,7 @@ impl Widget for Layers { let mut size = Size::ZERO; - for child in &self.mounted { + for child in self.mounted.children() { size = size.max( context .for_other(child) @@ -132,7 +94,7 @@ impl Widget for Layers { .fit_measured(size.height, context.gfx.scale()), ); let layout = Rect::from(size.into_signed()); - for child in &self.mounted { + for child in self.mounted.children() { context .for_other(child) .layout(size.map(ConstraintLimit::Fill)); @@ -147,10 +109,9 @@ impl Widget for Layers { } fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { - for child in self.mounted.drain(..) { + for child in self.mounted.drain() { context.remove_child(&child); } - self.mounted_generation = None; } fn root_behavior( @@ -159,7 +120,7 @@ impl Widget for Layers { ) -> Option<(RootBehavior, WidgetInstance)> { self.synchronize_children(context); - for child in &self.mounted { + for child in self.mounted.children() { let Some((child_behavior, next_in_chain)) = context.for_other(child).root_behavior() else { continue; diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index d3563ee..bffd1f5 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -7,7 +7,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex use crate::styles::components::IntrinsicPadding; use crate::styles::FlexibleDimension; use crate::value::{Generation, IntoValue, Value}; -use crate::widget::{Children, ManagedWidget, Widget, WidgetRef}; +use crate::widget::{Children, ChildrenSyncChange, ManagedWidget, Widget, WidgetRef}; use crate::widgets::grid::{GridDimension, GridLayout, Orientation}; use crate::widgets::{Expand, Resize}; use crate::ConstraintLimit; @@ -66,24 +66,11 @@ impl Stack { ) { self.layout_generation = self.children.generation(); self.children.map(|children| { - for (index, widget) in children.iter().enumerate() { - if self - .synced_children - .get(index) - .map_or(true, |child| child != widget) - { - // These entries do not match. See if we can find the - // new id somewhere else, if so we can swap the entries. - if let Some((swap_index, _)) = self - .synced_children - .iter() - .enumerate() - .skip(index + 1) - .find(|(_, child)| *child == widget) - { - self.synced_children.swap(index, swap_index); - self.layout.swap(index, swap_index); - } else { + children.synchronize_with( + &mut self.synced_children, + |this, index| this.get(index).map(ManagedWidget::instance), + |this, change| match change { + ChildrenSyncChange::Insert(index, widget) => { // This is a brand new child. let guard = widget.lock(); let (mut widget, dimension) = if let Some((weight, expand)) = @@ -112,20 +99,23 @@ impl Stack { ) }; drop(guard); - self.synced_children.insert(index, widget.mounted(context)); + this.insert(index, widget.mounted(context)); self.layout .insert(index, dimension, context.kludgine.scale()); } - } - } - - // Any children remaining at the end of this process are ones - // that have been removed. - for removed in self.synced_children.drain(children.len()..) { - context.remove_child(&removed); - } - self.layout.truncate(children.len()); + ChildrenSyncChange::Swap(a, b) => { + this.swap(a, b); + self.layout.swap(a, b); + } + ChildrenSyncChange::Truncate(length) => { + for removed in this.drain(length..) { + context.remove_child(&removed); + } + self.layout.truncate(length); + } + }, + ); }); } }