From 277160b484806b76ceed4d088418a92fd484b42e Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Tue, 27 Feb 2024 13:06:03 +0100 Subject: [PATCH] Add fine-grained tree-structure tracking in `xilem` and effcient tree-updates in `xilem_web` (#160) * Generalize `VecSplice` that is used in the ViewSequences as `ElementsSplice` trait and optimized tree-structure diffing in `xilem_web` with it * Implement tree-structure tracking * Moved tree-structure tracking into view and added tests and comments * Deleted bloom filter * Refactor tree structure tracking using methods like `Cx::with_pod` * Add doc comments describing situations where the tree-structure will panic * Deleted bloom filter (again whoops) * Add doc comments for panic situations --- crates/xilem_core/src/sequence.rs | 141 +++++++++++++++--------- crates/xilem_core/src/vec_splice.rs | 8 ++ crates/xilem_web/src/context.rs | 33 +++++- crates/xilem_web/src/elements.rs | 163 +++++++++++++++++++--------- crates/xilem_web/src/lib.rs | 4 +- crates/xilem_web/src/one_of.rs | 12 +- crates/xilem_web/src/view.rs | 8 +- src/app.rs | 46 +++++--- src/bloom.rs | 163 ---------------------------- src/lib.rs | 2 - src/view/linear_layout.rs | 13 ++- src/view/list.rs | 18 +-- src/view/mod.rs | 4 +- src/view/taffy_layout.rs | 9 +- src/view/tree_structure_tracking.rs | 61 +++++++++++ src/view/view.rs | 58 +++++++++- src/widget/contexts.rs | 20 +++- src/widget/core.rs | 23 +--- src/widget/mod.rs | 2 + src/widget/tree_structure.rs | 143 ++++++++++++++++++++++++ 20 files changed, 594 insertions(+), 337 deletions(-) delete mode 100644 src/bloom.rs create mode 100644 src/view/tree_structure_tracking.rs create mode 100644 src/widget/tree_structure.rs diff --git a/crates/xilem_core/src/sequence.rs b/crates/xilem_core/src/sequence.rs index 60444b7c5..622cb3dce 100644 --- a/crates/xilem_core/src/sequence.rs +++ b/crates/xilem_core/src/sequence.rs @@ -4,11 +4,11 @@ #[doc(hidden)] #[macro_export] macro_rules! impl_view_tuple { - ( $viewseq:ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { + ( $viewseq:ident, $elements_splice: ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { impl ),* > $viewseq for ( $( $t, )* ) { type State = ( $( $t::State, )*); - fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State { + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { let b = ( $( self.$i.build(cx, elements), )* ); let state = ( $( b.$i, )*); state @@ -19,7 +19,7 @@ macro_rules! impl_view_tuple { cx: &mut $cx, prev: &Self, state: &mut Self::State, - els: &mut $crate::VecSplice<$pod>, + els: &mut dyn $elements_splice, ) -> ChangeFlags { let mut changed = <$changeflags>::default(); $( @@ -53,10 +53,56 @@ macro_rules! impl_view_tuple { } } } - #[macro_export] macro_rules! generate_viewsequence_trait { - ($viewseq:ident, $view:ident, $viewmarker: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { + ($viewseq:ident, $view:ident, $viewmarker: ident, $elements_splice: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { + + /// A temporary "splice" to add, update, delete and monitor elements in a sequence of elements. + /// It is mainly intended for view sequences + /// + /// Usually it's backed by a collection (e.g. `Vec`) that holds all the (existing) elements. + /// It sweeps over the element collection and does updates in place. + /// Internally it works by having a pointer/index to the current/old element (0 at the beginning), + /// and the pointer is incremented by basically all methods that mutate that sequence. + pub trait $elements_splice { + /// Insert a new element at the current index in the resulting collection (and increment the index by 1) + fn push(&mut self, element: $pod, cx: &mut $cx); + /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) + fn mutate(&mut self, cx: &mut $cx) -> &mut $pod; + // TODO(#160) this could also track view id changes (old_id, new_id) + /// Mark any changes done by `mutate` on the current element (this doesn't change the index) + fn mark(&mut self, changeflags: $changeflags, cx: &mut $cx) -> $changeflags; + /// Delete the next n existing elements (this doesn't change the index) + fn delete(&mut self, n: usize, cx: &mut $cx); + /// Current length of the elements collection + fn len(&self) -> usize; + // TODO(#160) add a skip method when it is necessary (e.g. relevant for immutable ViewSequences like ropes) + } + + impl<'a, 'b> $elements_splice for $crate::VecSplice<'a, 'b, $pod> { + fn push(&mut self, element: $pod, _cx: &mut $cx) { + self.push(element); + } + + fn mutate(&mut self, _cx: &mut $cx) -> &mut $pod + { + self.mutate() + } + + fn mark(&mut self, changeflags: $changeflags, _cx: &mut $cx) -> $changeflags + { + self.peek_mut().map(|pod| pod.mark(changeflags)).unwrap_or_default() + } + + fn delete(&mut self, n: usize, _cx: &mut $cx) { + self.delete(n) + } + + fn len(&self) -> usize { + self.len() + } + } + /// This trait represents a (possibly empty) sequence of views. /// /// It is up to the parent view how to lay out and display them. @@ -65,7 +111,10 @@ macro_rules! generate_viewsequence_trait { type State $( $ss )*; /// Build the associated widgets and initialize all states. - fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State; + /// + /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, + /// this takes an element splice as well (when it could be just a `Vec` otherwise) + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State; /// Update the associated widget. /// @@ -75,7 +124,7 @@ macro_rules! generate_viewsequence_trait { cx: &mut $cx, prev: &Self, state: &mut Self::State, - element: &mut $crate::VecSplice<$pod>, + elements: &mut dyn $elements_splice, ) -> $changeflags; /// Propagate a message. @@ -100,9 +149,9 @@ macro_rules! generate_viewsequence_trait { { type State = (>::State, $crate::Id); - fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State { - let (id, state, element) = >::build(self, cx); - elements.push(<$pod>::new(element)); + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { + let (id, state, pod) = cx.with_new_pod(|cx| >::build(self, cx)); + elements.push(pod, cx); (state, id) } @@ -111,20 +160,20 @@ macro_rules! generate_viewsequence_trait { cx: &mut $cx, prev: &Self, state: &mut Self::State, - element: &mut $crate::VecSplice<$pod>, + elements: &mut dyn $elements_splice, ) -> $changeflags { - let el = element.mutate(); - let downcast = el.downcast_mut().unwrap(); - let flags = >::rebuild( - self, - cx, - prev, - &mut state.1, - &mut state.0, - downcast, - ); - - el.mark(flags) + let pod = elements.mutate(cx); + let flags = cx.with_pod(pod, |el, cx| { + >::rebuild( + self, + cx, + prev, + &mut state.1, + &mut state.0, + el, + ) + }); + elements.mark(flags, cx) } fn message( @@ -156,7 +205,7 @@ macro_rules! generate_viewsequence_trait { impl> $viewseq for Option { type State = Option; - fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State { + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { match self { None => None, Some(vt) => { @@ -171,20 +220,19 @@ macro_rules! generate_viewsequence_trait { cx: &mut $cx, prev: &Self, state: &mut Self::State, - element: &mut $crate::VecSplice<$pod>, + elements: &mut dyn $elements_splice, ) -> $changeflags { match (self, &mut *state, prev) { - (Some(this), Some(state), Some(prev)) => this.rebuild(cx, prev, state, element), + (Some(this), Some(state), Some(prev)) => this.rebuild(cx, prev, state, elements), (None, Some(seq_state), Some(prev)) => { let count = prev.count(&seq_state); - element.delete(count); + elements.delete(count, cx); *state = None; <$changeflags>::tree_structure() } (Some(this), None, None) => { - let seq_state = element.as_vec(|vec| this.build(cx, vec)); - *state = Some(seq_state); + *state = Some(this.build(cx, elements)); <$changeflags>::tree_structure() } @@ -219,7 +267,7 @@ macro_rules! generate_viewsequence_trait { impl> $viewseq for Vec { type State = Vec; - fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State { + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { self.iter().map(|child| child.build(cx, elements)).collect() } @@ -228,7 +276,7 @@ macro_rules! generate_viewsequence_trait { cx: &mut $cx, prev: &Self, state: &mut Self::State, - elements: &mut $crate::VecSplice<$pod>, + elements: &mut dyn $elements_splice, ) -> $changeflags { let mut changed = <$changeflags>::default(); for ((child, child_prev), child_state) in self.iter().zip(prev).zip(state.iter_mut()) { @@ -242,16 +290,11 @@ macro_rules! generate_viewsequence_trait { .enumerate() .map(|(i, state)| prev[n + i].count(&state)) .sum(); - elements.delete(n_delete); + elements.delete(n_delete, cx); changed |= <$changeflags>::tree_structure(); } else if n > prev.len() { - let mut child_elements = vec![]; for i in prev.len()..n { - state.push(self[i].build(cx, &mut child_elements)); - } - // Discussion question: should VecSplice get an extend method? - for element in child_elements { - elements.push(element); + state.push(self[i].build(cx, elements)); } changed |= <$changeflags>::tree_structure(); } @@ -295,26 +338,26 @@ macro_rules! generate_viewsequence_trait { #[doc = concat!("`", stringify!($viewmarker), "`.")] pub trait $viewmarker {} - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, ;); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, ;); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0; 0); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1; 0, 1); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2; 0, 1, 2); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3; 0, 1, 2, 3); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4; 0, 1, 2, 3, 4); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4, V5, V6, V7; 0, 1, 2, 3, 4, 5, 6, 7); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4, V5, V6, V7, V8; 0, 1, 2, 3, 4, 5, 6, 7, 8); - $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, V0, V1, V2, V3, V4, V5, V6, V7, V8, V9; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); }; } diff --git a/crates/xilem_core/src/vec_splice.rs b/crates/xilem_core/src/vec_splice.rs index aa0af8b75..b460744fe 100644 --- a/crates/xilem_core/src/vec_splice.rs +++ b/crates/xilem_core/src/vec_splice.rs @@ -50,6 +50,14 @@ impl<'a, 'b, T> VecSplice<'a, 'b, T> { &mut self.v[ix] } + pub fn peek(&self) -> Option<&T> { + self.v.last() + } + + pub fn peek_mut(&mut self) -> Option<&mut T> { + self.v.last_mut() + } + pub fn len(&self) -> usize { self.ix } diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 90088edd0..8f14a8e5e 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -10,7 +10,8 @@ use crate::{ app::AppRunner, diff::{diff_kv_iterables, Diff}, vecmap::VecMap, - AttributeValue, Message, + view::DomNode, + AttributeValue, Message, Pod, }; type CowStr = std::borrow::Cow<'static, str>; @@ -106,6 +107,36 @@ impl Cx { (id, result) } + /// Run some logic within a new Pod context and return the newly created Pod, + /// + /// This logic is usually `View::build` to wrap the returned element into a Pod. + pub fn with_new_pod(&mut self, f: F) -> (Id, S, Pod) + where + E: DomNode, + F: FnOnce(&mut Cx) -> (Id, S, E), + { + let (id, state, element) = f(self); + (id, state, Pod::new(element)) + } + + /// Run some logic within the context of a given Pod, + /// + /// This logic is usually `View::rebuild` + /// + /// # Panics + /// + /// When the element type `E` is not the same type as the inner `DomNode` of the `Pod` + pub fn with_pod(&mut self, pod: &mut Pod, f: F) -> T + where + E: DomNode, + F: FnOnce(&mut E, &mut Cx) -> T, + { + let element = pod + .downcast_mut() + .expect("Element type has changed, this should never happen!"); + f(element, self) + } + pub fn document(&self) -> &Document { &self.document } diff --git a/crates/xilem_web/src/elements.rs b/crates/xilem_web/src/elements.rs index eb5e82bbc..10111bb97 100644 --- a/crates/xilem_web/src/elements.rs +++ b/crates/xilem_web/src/elements.rs @@ -5,7 +5,7 @@ use xilem_core::{Id, MessageResult, VecSplice}; use crate::{ interfaces::sealed::Sealed, vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx, - Pod, View, ViewMarker, ViewSequence, HTML_NS, + ElementsSplice, Pod, View, ViewMarker, ViewSequence, HTML_NS, }; use super::interfaces::Element; @@ -19,6 +19,9 @@ pub struct ElementState { pub(crate) children_states: ViewSeqState, pub(crate) attributes: VecMap, pub(crate) child_elements: Vec, + /// This is temporary cache for elements while updating/diffing, + /// after usage it shouldn't contain any elements, + /// and is mainly here to avoid unnecessary allocations pub(crate) scratch: Vec, } @@ -49,6 +52,90 @@ impl CustomElement { } } +/// An `ElementsSplice` that does DOM updates in place +struct ChildrenSplice<'a, 'b, 'c> { + children: VecSplice<'a, 'b, Pod>, + child_idx: u32, + parent: &'c web_sys::Node, + node_list: Option, + prev_element_count: usize, +} + +impl<'a, 'b, 'c> ChildrenSplice<'a, 'b, 'c> { + fn new( + children: &'a mut Vec, + scratch: &'b mut Vec, + parent: &'c web_sys::Node, + ) -> Self { + let prev_element_count = children.len(); + Self { + children: VecSplice::new(children, scratch), + child_idx: 0, + parent, + node_list: None, + prev_element_count, + } + } +} + +impl<'a, 'b, 'c> ElementsSplice for ChildrenSplice<'a, 'b, 'c> { + fn push(&mut self, element: Pod, _cx: &mut Cx) { + self.parent + .append_child(element.0.as_node_ref()) + .unwrap_throw(); + self.child_idx += 1; + self.children.push(element); + } + + fn mutate(&mut self, _cx: &mut Cx) -> &mut Pod { + self.children.mutate() + } + + fn delete(&mut self, n: usize, _cx: &mut Cx) { + // Optimization in case all elements are deleted at once + if n == self.prev_element_count { + self.parent.set_text_content(None); + } else { + // lazy NodeList access, in case it's not necessary at all, which is slightly faster when there's no need for the NodeList + let node_list = if let Some(node_list) = &self.node_list { + node_list + } else { + self.node_list = Some(self.parent.child_nodes()); + self.node_list.as_ref().unwrap() + }; + for _ in 0..n { + let child = node_list.get(self.child_idx).unwrap_throw(); + self.parent.remove_child(&child).unwrap_throw(); + } + } + self.children.delete(n); + } + + fn len(&self) -> usize { + self.children.len() + } + + fn mark(&mut self, mut changeflags: ChangeFlags, _cx: &mut Cx) -> ChangeFlags { + if changeflags.contains(ChangeFlags::STRUCTURE) { + let node_list = if let Some(node_list) = &self.node_list { + node_list + } else { + self.node_list = Some(self.parent.child_nodes()); + self.node_list.as_ref().unwrap() + }; + let cur_child = self.children.peek_mut().unwrap_throw(); + let old_child = node_list.get(self.child_idx).unwrap_throw(); + self.parent + .replace_child(cur_child.0.as_node_ref(), &old_child) + .unwrap_throw(); + // TODO(#160) do something else with the structure information? + changeflags.remove(ChangeFlags::STRUCTURE); + } + self.child_idx += 1; + changeflags + } +} + impl ViewMarker for CustomElement {} impl Sealed for CustomElement {} @@ -66,12 +153,12 @@ where let (el, attributes) = cx.build_element(HTML_NS, &self.name); let mut child_elements = vec![]; - let (id, children_states) = - cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); + let mut scratch = vec![]; + let mut splice = ChildrenSplice::new(&mut child_elements, &mut scratch, &el); - for child in &child_elements { - el.append_child(child.0.as_node_ref()).unwrap_throw(); - } + let (id, children_states) = cx.with_new_id(|cx| self.children.build(cx, &mut splice)); + + debug_assert!(scratch.is_empty()); // Set the id used internally to the `data-debugid` attribute. // This allows the user to see if an element has been re-created or only altered. @@ -83,7 +170,7 @@ where let state = ElementState { children_states, child_elements, - scratch: vec![], + scratch, attributes, }; (id, state, el) @@ -109,10 +196,8 @@ where let (new_element, attributes) = cx.build_element(HTML_NS, self.node_name()); state.attributes = attributes; // TODO could this be combined with child updates? - while element.child_element_count() > 0 { - new_element - .append_child(&element.child_nodes().get(0).unwrap_throw()) - .unwrap_throw(); + while let Some(child) = element.child_nodes().get(0) { + new_element.append_child(&child).unwrap_throw(); } *element = new_element.dyn_into().unwrap_throw(); changed |= ChangeFlags::STRUCTURE; @@ -121,23 +206,14 @@ where changed |= cx.rebuild_element(element, &mut state.attributes); // update children - let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + let mut splice = + ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); changed |= cx.with_id(*id, |cx| { self.children .rebuild(cx, &prev.children, &mut state.children_states, &mut splice) }); - if changed.contains(ChangeFlags::STRUCTURE) { - // This is crude and will result in more DOM traffic than needed. - // The right thing to do is diff the new state of the children id - // vector against the old, and derive DOM mutations from that. - while let Some(child) = element.first_child() { - element.remove_child(&child).unwrap_throw(); - } - for child in &state.child_elements { - element.append_child(child.0.as_node_ref()).unwrap_throw(); - } - changed.remove(ChangeFlags::STRUCTURE); - } + debug_assert!(state.scratch.is_empty()); + changed.remove(ChangeFlags::STRUCTURE); changed } @@ -207,11 +283,11 @@ macro_rules! define_element { let (el, attributes) = cx.build_element($ns, $tag_name); let mut child_elements = vec![]; - let (id, children_states) = - cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); - for child in &child_elements { - el.append_child(child.0.as_node_ref()).unwrap_throw(); - } + let mut scratch = vec![]; + let mut splice = ChildrenSplice::new(&mut child_elements, &mut scratch, &el); + + let (id, children_states) = cx.with_new_id(|cx| self.0.build(cx, &mut splice)); + debug_assert!(scratch.is_empty()); // Set the id used internally to the `data-debugid` attribute. // This allows the user to see if an element has been re-created or only altered. @@ -223,7 +299,7 @@ macro_rules! define_element { let state = ElementState { children_states, child_elements, - scratch: vec![], + scratch, attributes, }; (id, state, el) @@ -239,26 +315,15 @@ macro_rules! define_element { ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); - changed |= cx.apply_attribute_changes(element, &mut state.attributes); + changed |= cx.rebuild_element(element, &mut state.attributes); // update children - let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); changed |= cx.with_id(*id, |cx| { - self.0 - .rebuild(cx, &prev.0, &mut state.children_states, &mut splice) + self.0.rebuild(cx, &prev.0, &mut state.children_states, &mut splice) }); - if changed.contains(ChangeFlags::STRUCTURE) { - // This is crude and will result in more DOM traffic than needed. - // The right thing to do is diff the new state of the children id - // vector against the old, and derive DOM mutations from that. - while let Some(child) = element.first_child() { - element.remove_child(&child).unwrap_throw(); - } - for child in &state.child_elements { - element.append_child(child.0.as_node_ref()).unwrap_throw(); - } - changed.remove(ChangeFlags::STRUCTURE); - } + debug_assert!(state.scratch.is_empty()); + changed.remove(ChangeFlags::STRUCTURE); // this is handled by the ChildrenSplice already changed } @@ -293,11 +358,11 @@ macro_rules! define_elements { ($ns:ident, $($element_def:tt,)*) => { use std::marker::PhantomData; use wasm_bindgen::{JsCast, UnwrapThrowExt}; - use xilem_core::{Id, MessageResult, VecSplice}; - use super::ElementState; + use xilem_core::{Id, MessageResult}; + use super::{ElementState, ChildrenSplice}; use crate::{ - interfaces::sealed::Sealed, view::DomNode, + interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker, ViewSequence, }; diff --git a/crates/xilem_web/src/lib.rs b/crates/xilem_web/src/lib.rs index 9c77c2966..a7c6d1e3d 100644 --- a/crates/xilem_web/src/lib.rs +++ b/crates/xilem_web/src/lib.rs @@ -36,8 +36,8 @@ pub use one_of::{ pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; pub use view::{ - memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, Memoize, MemoizeState, - Pod, View, ViewMarker, ViewSequence, + memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, ElementsSplice, + Memoize, MemoizeState, Pod, View, ViewMarker, ViewSequence, }; pub use view_ext::ViewExt; diff --git a/crates/xilem_web/src/one_of.rs b/crates/xilem_web/src/one_of.rs index 7696080dc..7d912b99f 100644 --- a/crates/xilem_web/src/one_of.rs +++ b/crates/xilem_web/src/one_of.rs @@ -1,7 +1,8 @@ use wasm_bindgen::throw_str; use crate::{ - interfaces::for_all_element_descendents, ChangeFlags, Cx, Pod, View, ViewMarker, ViewSequence, + interfaces::for_all_element_descendents, ChangeFlags, Cx, ElementsSplice, View, ViewMarker, + ViewSequence, }; macro_rules! impl_dom_traits { @@ -166,7 +167,7 @@ macro_rules! one_of_sequence { )+ { type State = $ident<$($vars::State),+>; - fn build(&self, cx: &mut Cx, elements: &mut Vec) -> Self::State { + fn build(&self, cx: &mut Cx, elements: &mut dyn ElementsSplice) -> Self::State { match self { $( $ident::$vars(view_sequence) => { @@ -181,7 +182,7 @@ macro_rules! one_of_sequence { cx: &mut Cx, prev: &Self, state: &mut Self::State, - element: &mut xilem_core::VecSplice, + elements: &mut dyn ElementsSplice, ) -> ChangeFlags { match (prev, self) { $( @@ -194,12 +195,11 @@ macro_rules! one_of_sequence { " (unreachable)", )); }; - view_sequence.rebuild(cx, prev_view, state, element) + view_sequence.rebuild(cx, prev_view, state, elements) } // Variant has changed (_, $ident::$vars(view_sequence)) => { - let new_state = - element.as_vec(|elements| view_sequence.build(cx, elements)); + let new_state = view_sequence.build(cx, elements); *state = $ident::$vars(new_state); ChangeFlags::STRUCTURE } diff --git a/crates/xilem_web/src/view.rs b/crates/xilem_web/src/view.rs index b9d79003f..6a53e613e 100644 --- a/crates/xilem_web/src/view.rs +++ b/crates/xilem_web/src/view.rs @@ -71,21 +71,21 @@ impl DomNode for Box { pub struct Pod(pub Box); impl Pod { - fn new(node: impl DomNode) -> Self { + pub(crate) fn new(node: impl DomNode) -> Self { node.into_pod() } - fn downcast_mut(&mut self) -> Option<&mut T> { + pub(crate) fn downcast_mut(&mut self) -> Option<&mut T> { self.0.as_any_mut().downcast_mut() } - fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags { + pub(crate) fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags { flags } } xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} -xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} +xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, ElementsSplice, DomNode, Cx, ChangeFlags, Pod;} xilem_core::generate_anyview_trait! {AnyView, View, ViewMarker, Cx, ChangeFlags, AnyNode, BoxedView;} xilem_core::generate_memoize_view! {Memoize, MemoizeState, View, ViewMarker, Cx, ChangeFlags, static_view, memoize;} xilem_core::generate_adapt_view! {View, Cx, ChangeFlags;} diff --git a/src/app.rs b/src/app.rs index 6f9e48b10..070c5dcc2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -157,7 +157,7 @@ where id: None, root_pod: None, events: Vec::new(), - root_state: WidgetState::new(), + root_state: WidgetState::new(crate::id::Id::next()), size: Default::default(), new_size: Default::default(), cursor_pos: None, @@ -181,7 +181,8 @@ where // TODO: be more lazy re-rendering self.render(); let root_pod = self.root_pod.as_mut().unwrap(); - let mut cx_state = CxState::new(&mut self.font_cx, &mut self.events); + let mut cx_state = + CxState::new(&mut self.font_cx, &self.cx.tree_structure, &mut self.events); let mut lifecycle_cx = LifeCycleCx::new(&mut cx_state, &mut self.root_state); root_pod.lifecycle(&mut lifecycle_cx, &LifeCycle::TreeUpdate); @@ -225,7 +226,8 @@ where // Borrow again to avoid multiple borrows. // TODO: maybe make accessibility a method on CxState? let root_pod = self.root_pod.as_mut().unwrap(); - let mut cx_state = CxState::new(&mut self.font_cx, &mut self.events); + let mut cx_state = + CxState::new(&mut self.font_cx, &self.cx.tree_structure, &mut self.events); let mut paint_cx = PaintCx::new(&mut cx_state, &mut self.root_state); root_pod.paint_impl(&mut paint_cx); break; @@ -247,7 +249,8 @@ where self.ensure_root(); let root_pod = self.root_pod.as_mut().unwrap(); - let mut cx_state = CxState::new(&mut self.font_cx, &mut self.events); + let mut cx_state = + CxState::new(&mut self.font_cx, &self.cx.tree_structure, &mut self.events); let mut event_cx = EventCx::new(&mut cx_state, &mut self.root_state); root_pod.event(&mut event_cx, &event); self.send_events(); @@ -283,23 +286,30 @@ where if let Some(response) = self.response_chan.blocking_recv() { let state = if let Some(root_pod) = self.root_pod.as_mut() { let mut state = response.state.unwrap(); - let changes = response.view.rebuild( - &mut self.cx, - response.prev.as_ref().unwrap(), - self.id.as_mut().unwrap(), - &mut state, - //TODO: fail more gracefully but make it explicit that this is a bug - root_pod - .downcast_mut() - .expect("the root widget changed its type, this should never happen!"), - ); + let changes = self.cx.with_pod(root_pod, |root_el, cx| { + response.view.rebuild( + cx, + response.prev.as_ref().unwrap(), + self.id.as_mut().unwrap(), + &mut state, + root_el, + ) + }); let _ = root_pod.mark(changes); - assert!(self.cx.is_empty(), "id path imbalance on rebuild"); + assert!(self.cx.id_path_is_empty(), "id path imbalance on rebuild"); + assert!( + self.cx.element_id_path_is_empty(), + "element id path imbalance on rebuild" + ); state } else { - let (id, state, root_widget) = response.view.build(&mut self.cx); - assert!(self.cx.is_empty(), "id path imbalance on build"); - self.root_pod = Some(Pod::new(root_widget)); + let (id, state, root_pod) = self.cx.with_new_pod(|cx| response.view.build(cx)); + assert!(self.cx.id_path_is_empty(), "id path imbalance on build"); + assert!( + self.cx.element_id_path_is_empty(), + "element id path imbalance on rebuild" + ); + self.root_pod = Some(root_pod); self.id = Some(id); state }; diff --git a/src/bloom.rs b/src/bloom.rs deleted file mode 100644 index 205ed2f58..000000000 --- a/src/bloom.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2020 The Druid Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! A simple Bloom filter, used to track child widgets. - -use std::hash::{Hash, Hasher}; -use std::marker::PhantomData; - -use fnv::FnvHasher; - -const NUM_BITS: u64 = 64; - -// the 'offset_basis' for the fnv-1a hash algorithm. -// see http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param -// -// The first of these is the one described in the algorithm, the second is random. -const OFFSET_ONE: u64 = 0xcbf2_9ce4_8422_2325; -const OFFSET_TWO: u64 = 0xe10_3ad8_2dad_8028; - -/// A very simple Bloom filter optimized for small values. -#[derive(Clone, Copy)] -pub(crate) struct Bloom { - bits: u64, - data: PhantomData, - entry_count: usize, -} - -impl Bloom { - /// Returns the number of items that have been added to the filter. - /// - /// Does not count unique entries; this is just the number of times - /// `add()` was called since the filter was created or last `clear()`ed. - // it feels wrong to call this 'len'? - #[allow(dead_code)] - pub fn entry_count(&self) -> usize { - self.entry_count - } - - /// Return the raw bits of this filter. - #[allow(dead_code)] - pub fn to_raw(&self) -> u64 { - self.bits - } - - /// Remove all entries from the filter. - pub fn clear(&mut self) { - self.bits = 0; - self.entry_count = 0; - } - - /// Add an item to the filter. - pub fn add(&mut self, item: &T) { - let mask = self.make_bit_mask(item); - self.bits |= mask; - self.entry_count += 1; - } - - /// Returns `true` if the item may have been added to the filter. - /// - /// This can return false positives, but never false negatives. - /// Thus `true` means that the item may have been added - or not, - /// while `false` means that the item has definitely not been added. - #[allow(unused)] - pub fn may_contain(&self, item: &T) -> bool { - let mask = self.make_bit_mask(item); - self.bits & mask == mask - } - - /// Create a new `Bloom` with the items from both filters. - pub fn union(&self, other: Bloom) -> Bloom { - Bloom { - bits: self.bits | other.bits, - data: PhantomData, - entry_count: self.entry_count + other.entry_count, - } - } - - #[inline] - fn make_bit_mask(&self, item: &T) -> u64 { - //NOTE: we use two hash functions, which performs better than a single hash - // with smaller numbers of items, but poorer with more items. Threshold - // (given 64 bits) is ~30 items. - // The reasoning is that with large numbers of items we're already in bad shape; - // optimize for fewer false positives as we get closer to the leaves. - // This can be tweaked after profiling. - let hash1 = self.make_hash(item, OFFSET_ONE); - let hash2 = self.make_hash(item, OFFSET_TWO); - (1 << (hash1 % NUM_BITS)) | (1 << (hash2 % NUM_BITS)) - } - - #[inline] - fn make_hash(&self, item: &T, seed: u64) -> u64 { - let mut hasher = FnvHasher::with_key(seed); - item.hash(&mut hasher); - hasher.finish() - } -} - -impl std::fmt::Debug for Bloom { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Bloom: {:064b}: ({})", self.bits, self.entry_count) - } -} - -impl Default for Bloom { - fn default() -> Self { - Bloom { - bits: 0, - data: PhantomData, - entry_count: 0, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn very_good_test() { - let mut bloom = Bloom::default(); - for i in 0..100 { - bloom.add(&i); - assert!(bloom.may_contain(&i)); - } - bloom.clear(); - for i in 0..100 { - assert!(!bloom.may_contain(&i)); - } - } - - #[test] - fn union() { - let mut bloom1 = Bloom::default(); - bloom1.add(&0); - bloom1.add(&1); - assert!(!bloom1.may_contain(&2)); - assert!(!bloom1.may_contain(&3)); - let mut bloom2 = Bloom::default(); - bloom2.add(&2); - bloom2.add(&3); - assert!(!bloom2.may_contain(&0)); - assert!(!bloom2.may_contain(&1)); - - let bloom3 = bloom1.union(bloom2); - assert!(bloom3.may_contain(&0)); - assert!(bloom3.may_contain(&1)); - assert!(bloom3.may_contain(&2)); - assert!(bloom3.may_contain(&3)); - } -} diff --git a/src/lib.rs b/src/lib.rs index f20da417e..68ffe75d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ extern crate core; mod app; mod app_main; -mod bloom; mod geometry; mod id; pub mod text; @@ -15,7 +14,6 @@ pub use xilem_core::{IdPath, MessageResult}; pub use app::App; pub use app_main::AppLauncher; -pub(crate) use bloom::Bloom; pub use geometry::Axis; pub use parley; diff --git a/src/view/linear_layout.rs b/src/view/linear_layout.rs index 09f24dcec..30ee4764e 100644 --- a/src/view/linear_layout.rs +++ b/src/view/linear_layout.rs @@ -15,11 +15,11 @@ use std::{any::Any, marker::PhantomData}; use crate::geometry::Axis; -use crate::view::{Id, VecSplice, ViewMarker, ViewSequence}; +use crate::view::{Id, ViewMarker, ViewSequence}; use crate::widget::{self, ChangeFlags}; use crate::MessageResult; -use super::{Cx, View}; +use super::{Cx, TreeStructureSplice, View}; /// LinearLayout is a simple view which does layout for the specified ViewSequence. /// @@ -70,7 +70,9 @@ impl> View for LinearLayout { fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let mut elements = vec![]; - let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut elements)); + let mut scratch = vec![]; + let mut splice = TreeStructureSplice::new(&mut elements, &mut scratch); + let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut splice)); let column = widget::LinearLayout::new(elements, self.spacing, self.axis); (id, state, column) } @@ -83,9 +85,8 @@ impl> View for LinearLayout { state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { - let mut scratch = vec![]; - let mut splice = VecSplice::new(&mut element.children, &mut scratch); - + let mut scratch = vec![]; // TODO(#160) could save some allocations by using View::State + let mut splice = TreeStructureSplice::new(&mut element.children, &mut scratch); let mut flags = cx.with_id(*id, |cx| { self.children .rebuild(cx, &prev.children, state, &mut splice) diff --git a/src/view/list.rs b/src/view/list.rs index 073887d6f..1fb1edb3f 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::view::{Cx, VecSplice, ViewSequence}; -use crate::widget::{ChangeFlags, Pod}; +use crate::view::{Cx, ElementsSplice, ViewSequence}; +use crate::widget::ChangeFlags; use crate::MessageResult; use std::any::Any; use std::marker::PhantomData; @@ -49,7 +49,7 @@ impl, F: Fn(usize) -> VT + Send> ViewSequence { type State = ListState; - fn build(&self, cx: &mut Cx, elements: &mut Vec) -> Self::State { + fn build(&self, cx: &mut Cx, elements: &mut dyn ElementsSplice) -> Self::State { let leading = elements.len(); let views = @@ -72,29 +72,29 @@ impl, F: Fn(usize) -> VT + Send> ViewSequence cx: &mut Cx, prev: &Self, state: &mut Self::State, - element: &mut VecSplice, + elements: &mut dyn ElementsSplice, ) -> ChangeFlags { // Common length - let leading = element.len(); + let leading = elements.len(); let mut flags = (0..(self.items.min(prev.items))) .zip(&mut state.views) .fold(ChangeFlags::empty(), |flags, (index, (prev, state))| { let vt = (self.build)(index); - let vt_flags = vt.rebuild(cx, prev, state, element); + let vt_flags = vt.rebuild(cx, prev, state, elements); *prev = vt; flags | vt_flags }); if self.items < prev.items { for (prev, state) in state.views.splice(self.items.., []) { - element.delete(prev.count(&state)); + elements.delete(prev.count(&state), cx); } } while self.items > state.views.len() { let vt = (self.build)(state.views.len()); - let vt_state = element.as_vec(|vec| vt.build(cx, vec)); + let vt_state = vt.build(cx, elements); state.views.push((vt, vt_state)); } @@ -104,7 +104,7 @@ impl, F: Fn(usize) -> VT + Send> ViewSequence flags |= ChangeFlags::all(); } - state.element_count = element.len() - leading; + state.element_count = elements.len() - leading; flags } diff --git a/src/view/mod.rs b/src/view/mod.rs index 3b026bf2c..7a1d38e9e 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -22,6 +22,7 @@ mod text; mod linear_layout; mod list; mod switch; +mod tree_structure_tracking; #[allow(clippy::module_inception)] mod view; @@ -31,7 +32,8 @@ pub use button::button; pub use linear_layout::{h_stack, v_stack, LinearLayout}; pub use list::{list, List}; pub use switch::switch; -pub use view::{Adapt, AdaptState, Cx, Memoize, View, ViewMarker, ViewSequence}; +pub use tree_structure_tracking::TreeStructureSplice; +pub use view::{Adapt, AdaptState, Cx, ElementsSplice, Memoize, View, ViewMarker, ViewSequence}; #[cfg(feature = "taffy")] mod taffy_layout; diff --git a/src/view/taffy_layout.rs b/src/view/taffy_layout.rs index 38adb0ff1..1b8e67f4a 100644 --- a/src/view/taffy_layout.rs +++ b/src/view/taffy_layout.rs @@ -16,7 +16,7 @@ use std::{any::Any, marker::PhantomData}; use vello::peniko::Color; -use crate::view::{Id, VecSplice, ViewMarker, ViewSequence}; +use crate::view::{Id, TreeStructureSplice, ViewMarker, ViewSequence}; use crate::widget::{self, ChangeFlags}; use crate::MessageResult; @@ -103,7 +103,9 @@ impl> View for TaffyLayout { fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let mut elements = vec![]; - let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut elements)); + let mut scratch = vec![]; + let mut splice = TreeStructureSplice::new(&mut elements, &mut scratch); + let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut splice)); let column = widget::TaffyLayout::new(elements, self.style.clone(), self.background_color); (id, state, column) } @@ -117,8 +119,7 @@ impl> View for TaffyLayout { element: &mut Self::Element, ) -> ChangeFlags { let mut scratch = vec![]; - let mut splice = VecSplice::new(&mut element.children, &mut scratch); - + let mut splice = TreeStructureSplice::new(&mut element.children, &mut scratch); let mut flags = cx.with_id(*id, |cx| { self.children .rebuild(cx, &prev.children, state, &mut splice) diff --git a/src/view/tree_structure_tracking.rs b/src/view/tree_structure_tracking.rs new file mode 100644 index 000000000..a05ada475 --- /dev/null +++ b/src/view/tree_structure_tracking.rs @@ -0,0 +1,61 @@ +use crate::{ + id::Id, + view::ElementsSplice, + widget::{ChangeFlags, Pod}, +}; +use xilem_core::VecSplice; + +use super::Cx; + +/// An ElementsSplice that tracks the widget tree structure +pub struct TreeStructureSplice<'a, 'b> { + current_child_id: Option, + splice: VecSplice<'a, 'b, Pod>, +} + +impl<'a, 'b> TreeStructureSplice<'a, 'b> { + pub fn new(elements: &'a mut Vec, scratch: &'b mut Vec) -> Self { + Self { + splice: VecSplice::new(elements, scratch), + current_child_id: None, + } + } +} + +impl<'a, 'b> ElementsSplice for TreeStructureSplice<'a, 'b> { + fn push(&mut self, element: Pod, cx: &mut Cx) { + cx.tree_structure + .append_child(cx.element_id(), element.id()); + self.splice.push(element); + } + + fn mutate(&mut self, _cx: &mut Cx) -> &mut Pod { + let pod = self.splice.mutate(); + self.current_child_id = Some(pod.id()); + pod + } + + fn mark(&mut self, changeflags: ChangeFlags, cx: &mut Cx) -> ChangeFlags { + if changeflags.contains(ChangeFlags::tree_structure()) { + let current_id = self.current_child_id.take().unwrap(); + let new_id = self.splice.peek().unwrap().id(); + if current_id != new_id { + cx.tree_structure + .change_child(cx.element_id(), self.splice.len() - 1, new_id); + } + } + + self.splice.mark(changeflags, cx) + } + + fn delete(&mut self, n: usize, cx: &mut Cx) { + let ix = self.splice.len(); + cx.tree_structure + .delete_children(cx.element_id(), ix..ix + n); + self.splice.delete(n); + } + + fn len(&self) -> usize { + self.splice.len() + } +} diff --git a/src/view/view.rs b/src/view/view.rs index 3a1bfacac..89cb81cb6 100644 --- a/src/view/view.rs +++ b/src/view/view.rs @@ -21,10 +21,10 @@ use futures_task::{ArcWake, Waker}; use xilem_core::{Id, IdPath}; -use crate::widget::{AnyWidget, ChangeFlags, Pod, Widget}; +use crate::widget::{tree_structure::TreeStructure, AnyWidget, ChangeFlags, Pod, Widget}; xilem_core::generate_view_trait! {View, Widget, Cx, ChangeFlags; : Send} -xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, Widget, Cx, ChangeFlags, Pod; : Send} +xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, ElementsSplice, Widget, Cx, ChangeFlags, Pod; : Send} xilem_core::generate_anyview_trait! {AnyView, View, ViewMarker, Cx, ChangeFlags, AnyWidget, BoxedView; + Send} xilem_core::generate_memoize_view! {Memoize, MemoizeState, View, ViewMarker, Cx, ChangeFlags, s, memoize; + Send} xilem_core::generate_adapt_view! {View, Cx, ChangeFlags; + Send} @@ -33,7 +33,9 @@ xilem_core::generate_adapt_state_view! {View, Cx, ChangeFlags; + Send} #[derive(Clone)] pub struct Cx { id_path: IdPath, + element_id_path: Vec, // Note that this is the widget id type. req_chan: SyncSender, + pub(crate) tree_structure: TreeStructure, pub(crate) pending_async: HashSet, } @@ -53,8 +55,10 @@ impl Cx { pub(crate) fn new(req_chan: &SyncSender) -> Self { Cx { id_path: Vec::new(), + element_id_path: Vec::new(), req_chan: req_chan.clone(), pending_async: HashSet::new(), + tree_structure: TreeStructure::default(), } } @@ -66,7 +70,7 @@ impl Cx { self.id_path.pop(); } - pub fn is_empty(&self) -> bool { + pub fn id_path_is_empty(&self) -> bool { self.id_path.is_empty() } @@ -74,6 +78,18 @@ impl Cx { &self.id_path } + pub fn element_id_path_is_empty(&self) -> bool { + self.element_id_path.is_empty() + } + + /// Return the element id of the current element/widget + pub fn element_id(&self) -> crate::id::Id { + *self + .element_id_path + .last() + .expect("element_id path imbalance, there should be an element id") + } + /// Run some logic with an id added to the id path. /// /// This is an ergonomic helper that ensures proper nesting of the id path. @@ -95,6 +111,42 @@ impl Cx { (id, result) } + /// Run some logic within a new Pod context and return the newly created Pod, + /// + /// This logic is usually `View::build` to wrap the returned element into a Pod. + pub fn with_new_pod(&mut self, f: F) -> (Id, S, Pod) + where + E: Widget + 'static, + F: FnOnce(&mut Cx) -> (Id, S, E), + { + let pod_id = crate::id::Id::next(); + self.element_id_path.push(pod_id); + let (id, state, element) = f(self); + self.element_id_path.pop(); + (id, state, Pod::new(element, pod_id)) + } + + /// Run some logic within the context of a given Pod, + /// + /// This logic is usually `View::rebuild` + /// + /// # Panics + /// + /// When the element type `E` is not the same type as the inner `Widget` of the `Pod`. + pub fn with_pod(&mut self, pod: &mut Pod, f: F) -> T + where + E: Widget + 'static, + F: FnOnce(&mut E, &mut Cx) -> T, + { + self.element_id_path.push(pod.id()); + let element = pod + .downcast_mut() + .expect("Element type has changed, this should never happen!"); + let result = f(element, self); + self.element_id_path.pop(); + result + } + pub fn waker(&self) -> Waker { futures_task::waker(Arc::new(MyWaker { id_path: self.id_path.clone(), diff --git a/src/widget/contexts.rs b/src/widget/contexts.rs index a9568cb7d..2edb87c62 100644 --- a/src/widget/contexts.rs +++ b/src/widget/contexts.rs @@ -20,7 +20,7 @@ use parley::FontContext; use vello::kurbo::{Point, Size}; -use super::{PodFlags, WidgetState}; +use super::{tree_structure::TreeStructure, PodFlags, WidgetState}; use crate::Message; // These contexts loosely follow Druid. @@ -28,6 +28,7 @@ use crate::Message; /// Static state that is shared between most contexts. pub struct CxState<'a> { #[allow(unused)] + tree_structure: &'a TreeStructure, font_cx: &'a mut FontContext, messages: &'a mut Vec, } @@ -103,8 +104,16 @@ macro_rules! impl_context_method { } impl<'a> CxState<'a> { - pub fn new(font_cx: &'a mut FontContext, messages: &'a mut Vec) -> Self { - CxState { font_cx, messages } + pub fn new( + font_cx: &'a mut FontContext, + tree_structure: &'a TreeStructure, + messages: &'a mut Vec, + ) -> Self { + CxState { + font_cx, + messages, + tree_structure, + } } pub(crate) fn has_messages(&self) -> bool { @@ -203,6 +212,11 @@ impl_context_method!( pub fn is_active(&self) -> bool { self.widget_state.flags.contains(PodFlags::IS_ACTIVE) } + + /// Returns the pure structure (parent/children relations via ids) of the widget tree + pub fn tree_structure(&self) -> &TreeStructure { + self.cx_state.tree_structure + } } ); diff --git a/src/widget/core.rs b/src/widget/core.rs index 68e308e11..210e5bf90 100644 --- a/src/widget/core.rs +++ b/src/widget/core.rs @@ -24,8 +24,8 @@ use vello::{ }; use super::widget::{AnyWidget, Widget}; +use crate::id::Id; use crate::Axis; -use crate::{id::Id, Bloom}; use super::{ contexts::LifeCycleCx, BoxConstraints, CxState, Event, EventCx, LayoutCx, LifeCycle, PaintCx, @@ -106,12 +106,6 @@ pub(crate) struct WidgetState { pub(crate) parent_window_origin: Point, /// The size of the widget. pub(crate) size: Size, - /// A bloom filter containing this widgets is and the ones of its children. - // TODO: decide the final solution for this. This is probably going to be a global structure - // tracking parent child relations in the tree: - // parents: HashMap, - // children: HashMap>, - pub(crate) sub_tree: Bloom, } impl PodFlags { @@ -140,21 +134,18 @@ impl ChangeFlags { } impl WidgetState { - pub(crate) fn new() -> Self { - let id = Id::next(); + pub(crate) fn new(id: Id) -> Self { WidgetState { id, flags: PodFlags::INIT_FLAGS, origin: Default::default(), parent_window_origin: Default::default(), size: Default::default(), - sub_tree: Default::default(), } } fn merge_up(&mut self, child_state: &mut WidgetState) { self.flags |= child_state.flags.upwards(); - self.sub_tree = self.sub_tree.union(child_state.sub_tree); } fn request(&mut self, flags: PodFlags) { @@ -167,17 +158,17 @@ impl Pod { /// /// In a widget hierarchy, each widget is wrapped in a `Pod` /// so it can participate in layout and event flow. - pub fn new(widget: impl Widget + 'static) -> Self { - Self::new_from_box(Box::new(widget)) + pub fn new(widget: impl Widget + 'static, id: Id) -> Self { + Self::new_from_box(Box::new(widget), id) } /// Create a new pod. /// /// In a widget hierarchy, each widget is wrapped in a `Pod` /// so it can participate in layout and event flow. - pub fn new_from_box(widget: Box) -> Self { + pub fn new_from_box(widget: Box, id: Id) -> Self { Pod { - state: WidgetState::new(), + state: WidgetState::new(id), fragment: Scene::default(), widget, } @@ -329,8 +320,6 @@ impl Pod { } LifeCycle::TreeUpdate => { if self.state.flags.contains(PodFlags::TREE_CHANGED) { - self.state.sub_tree.clear(); - self.state.sub_tree.add(&self.state.id); self.state.flags.remove(PodFlags::TREE_CHANGED); true } else { diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 7da04cc9f..4f1327b04 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,6 +24,7 @@ mod raw_event; mod switch; //mod scroll_view; mod text; +pub mod tree_structure; #[allow(clippy::module_inception)] mod widget; @@ -36,6 +37,7 @@ pub use linear_layout::LinearLayout; pub use raw_event::{Event, LifeCycle, MouseEvent, PointerCrusher, ViewContext}; pub use switch::Switch; pub use text::TextWidget; +pub use tree_structure::TreeStructure; pub use widget::{AnyWidget, Widget}; #[cfg(feature = "taffy")] diff --git a/src/widget/tree_structure.rs b/src/widget/tree_structure.rs new file mode 100644 index 000000000..3c07af3cf --- /dev/null +++ b/src/widget/tree_structure.rs @@ -0,0 +1,143 @@ +use crate::id::Id; +use std::collections::HashMap; + +/// The pure structure (parent/children relations via ids) of the widget tree. +#[derive(Debug, Default, Clone)] +pub struct TreeStructure { + parent: HashMap, + children: HashMap>, +} + +impl TreeStructure { + pub fn parent(&self, id: Id) -> Option { + self.parent.get(&id).copied() + } + + pub fn children(&self, id: Id) -> Option<&[Id]> { + self.children.get(&id).map(Vec::as_slice) + } + + pub fn is_descendant_of(&self, mut id: Id, ancestor: Id) -> bool { + while let Some(parent) = self.parent(id) { + if parent == ancestor { + return true; + } + id = parent; + } + false + } + + pub(crate) fn append_child(&mut self, parent_id: Id, id: Id) { + self.parent + .entry(id) + .and_modify(|parent| { + *parent = parent_id; + }) + .or_insert(parent_id); + self.children + .entry(parent_id) + .and_modify(|children| { + children.push(id); + }) + .or_insert_with(|| vec![id]); + } + + /// # Panics + /// + /// When the `parent_id` doesn't exist in the structure or `idx` is out of bounds this will panic + pub(crate) fn change_child(&mut self, parent_id: Id, idx: usize, new_id: Id) { + let children = self + .children + .get_mut(&parent_id) + .unwrap_or_else(|| panic!("{parent_id:?} doesn't have any child")); + let old_id = children[idx]; + children[idx] = new_id; + + self.parent.remove(&old_id); + self.parent + .entry(new_id) + .and_modify(|parent| { + *parent = parent_id; + }) + .or_insert(parent_id); + } + + /// # Panics + /// + /// When the `parent_id` doesn't exist in the structure or `range` is out of bounds this will panic + pub(crate) fn delete_children(&mut self, parent_id: Id, range: std::ops::Range) { + let children = &self.children[&parent_id][range.clone()]; + for child in children { + self.parent.remove(child); + } + self.children.get_mut(&parent_id).unwrap().drain(range); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mutates_simple_tree_structure() { + let mut tree_structure = TreeStructure::default(); + + let parent = Id::next(); + let child1 = Id::next(); + let child2 = Id::next(); + let child3 = Id::next(); + + // append children + tree_structure.append_child(parent, child1); + tree_structure.append_child(parent, child2); + tree_structure.append_child(parent, child3); + let children = tree_structure.children(parent).unwrap(); + assert_eq!(children, [child1, child2, child3]); + assert_eq!(tree_structure.parent(child1), Some(parent)); + assert_eq!(tree_structure.parent(child2), Some(parent)); + assert_eq!(tree_structure.parent(child3), Some(parent)); + + // change children + let child2_new = Id::next(); + tree_structure.change_child(parent, 1, child2_new); + let children = tree_structure.children(parent).unwrap(); + assert_eq!(children, [child1, child2_new, child3]); + assert_eq!(tree_structure.parent(child1), Some(parent)); + assert_eq!(tree_structure.parent(child2), None); + assert_eq!(tree_structure.parent(child2_new), Some(parent)); + assert_eq!(tree_structure.parent(child3), Some(parent)); + + // delete children + tree_structure.delete_children(parent, 0..2); + let children = tree_structure.children(parent).unwrap(); + assert_eq!(children, [child3]); + assert_eq!(tree_structure.parent(child1), None); + assert_eq!(tree_structure.parent(child2), None); + assert_eq!(tree_structure.parent(child2_new), None); + assert_eq!(tree_structure.parent(child3), Some(parent)); + } + + #[test] + fn is_descendant_of() { + let mut tree_structure = TreeStructure::default(); + let parent = Id::next(); + let child1 = Id::next(); + let child2 = Id::next(); + let child3 = Id::next(); + tree_structure.append_child(parent, child1); + tree_structure.append_child(parent, child2); + tree_structure.append_child(parent, child3); + + let child3_child1 = Id::next(); + let child3_child2 = Id::next(); + tree_structure.append_child(child3, child3_child1); + tree_structure.append_child(child3, child3_child2); + let child3_child1_child1 = Id::next(); + tree_structure.append_child(child3_child1, child3_child1_child1); + assert!(tree_structure.is_descendant_of(child3_child1_child1, child3_child1)); + assert!(tree_structure.is_descendant_of(child3_child1_child1, child3)); + assert!(tree_structure.is_descendant_of(child3_child1, parent)); + assert!(!tree_structure.is_descendant_of(child3_child1, child2)); + assert!(!tree_structure.is_descendant_of(parent, child3_child1)); + } +}