From bb13f1a7601a01008bfe9b4a7ffba4d259b6c1a6 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 5 Aug 2024 15:03:19 +0200 Subject: [PATCH] xilem_web: Allow `DomFragment` instead of `DomView` as `app_logic` (#482) Should fix #461. This allows a `ViewSequence` (called `DomFragment`) of `DomView`s as root component. The `counter` example is updated to show this new behavior. --- xilem_web/src/app.rs | 114 +++++++++++++-------- xilem_web/web_examples/counter/src/main.rs | 12 ++- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/xilem_web/src/app.rs b/xilem_web/src/app.rs index 1d6a54318..a0d80c6f8 100644 --- a/xilem_web/src/app.rs +++ b/xilem_web/src/app.rs @@ -1,11 +1,12 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use crate::{DomNode, ViewCtx}; +use crate::{elements::DomChildrenSplice, AnyPod, DomFragment, ViewCtx}; use std::{cell::RefCell, rc::Rc}; -use crate::{DomView, DynMessage, PodMut}; -use xilem_core::{MessageResult, ViewId}; +use crate::DynMessage; +use wasm_bindgen::UnwrapThrowExt; +use xilem_core::{AppendVec, MessageResult, ViewId}; pub(crate) struct AppMessage { pub id_path: Rc<[ViewId]>, @@ -13,15 +14,19 @@ pub(crate) struct AppMessage { } /// The type responsible for running your app. -pub struct App, F: FnMut(&mut T) -> V>(Rc>>); +pub struct App, InitFragment>( + Rc>>, +); -struct AppInner, F: FnMut(&mut T) -> V> { - data: T, +struct AppInner, InitFragment> { + data: State, root: web_sys::Node, - app_logic: F, - view: Option, - state: Option, - element: Option, + app_logic: InitFragment, + fragment: Option, + fragment_state: Option, + fragment_append_scratch: AppendVec, + vec_splice_scratch: Vec, + elements: Vec, cx: ViewCtx, } @@ -31,15 +36,22 @@ pub(crate) trait AppRunner { fn clone_box(&self) -> Box; } -impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App { +impl, InitFragment> Clone + for App +{ fn clone(&self) -> Self { App(self.0.clone()) } } -impl + 'static, F: FnMut(&mut T) -> V + 'static> App { +impl App +where + State: 'static, + Fragment: DomFragment + 'static, + InitFragment: FnMut(&mut State) -> Fragment + 'static, +{ /// Create an instance of your app with the given logic and initial state. - pub fn new(root: impl AsRef, data: T, app_logic: F) -> Self { + pub fn new(root: impl AsRef, data: State, app_logic: InitFragment) -> Self { let inner = AppInner::new(root.as_ref().clone(), data, app_logic); let app = App(Rc::new(RefCell::new(inner))); app.0.borrow_mut().cx.set_runner(app.clone()); @@ -57,69 +69,85 @@ impl + 'static, F: FnMut(&mut T) -> V + 'static> App, F: FnMut(&mut T) -> V> AppInner { - pub fn new(root: web_sys::Node, data: T, app_logic: F) -> Self { +impl, InitFragment: FnMut(&mut State) -> Fragment> + AppInner +{ + pub fn new(root: web_sys::Node, data: State, app_logic: InitFragment) -> Self { let cx = ViewCtx::default(); AppInner { data, root, app_logic, - view: None, - state: None, - element: None, + fragment: None, + fragment_state: None, + elements: Vec::new(), cx, + fragment_append_scratch: Default::default(), + vec_splice_scratch: Default::default(), } } fn ensure_app(&mut self) { - if self.view.is_none() { - let view = (self.app_logic)(&mut self.data); - let (mut element, state) = view.build(&mut self.cx); - element.node.apply_props(&mut element.props); - self.view = Some(view); - self.state = Some(state); + if self.fragment.is_none() { + let fragment = (self.app_logic)(&mut self.data); + let state = fragment.seq_build(&mut self.cx, &mut self.fragment_append_scratch); + self.fragment = Some(fragment); + self.fragment_state = Some(state); // TODO should the element provide a separate method to access reference instead? - let node: &web_sys::Node = element.node.as_ref(); - self.root.append_child(node).unwrap(); - self.element = Some(element); + let append_vec = std::mem::take(&mut self.fragment_append_scratch); + + self.elements = append_vec.into_inner(); + for pod in &self.elements { + self.root.append_child(pod.node.as_ref()).unwrap_throw(); + } } } } -impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner - for App +impl AppRunner for App +where + State: 'static, + Fragment: DomFragment + 'static, + InitFragment: FnMut(&mut State) -> Fragment + 'static, { // For now we handle the message synchronously, but it would also // make sense to to batch them (for example with requestAnimFrame). fn handle_message(&self, message: AppMessage) { let mut inner_guard = self.0.borrow_mut(); let inner = &mut *inner_guard; - if let Some(view) = &mut inner.view { - let message_result = view.message( - inner.state.as_mut().unwrap(), + if let Some(fragment) = &mut inner.fragment { + let message_result = fragment.seq_message( + inner.fragment_state.as_mut().unwrap(), &message.id_path, message.body, &mut inner.data, ); + // Each of those results are currently resulting in a rebuild, that may be subject to change match message_result { - MessageResult::Nop | MessageResult::Action(_) => { - // Nothing to do. - } - MessageResult::RequestRebuild => { - // TODO force a rebuild? - } + MessageResult::RequestRebuild | MessageResult::Nop | MessageResult::Action(_) => {} MessageResult::Stale(_) => { // TODO perhaps inform the user that a stale request bubbled to the top? } } - let new_view = (inner.app_logic)(&mut inner.data); - let el = inner.element.as_mut().unwrap(); - let pod_mut = PodMut::new(&mut el.node, &mut el.props, &inner.root, false); - new_view.rebuild(view, inner.state.as_mut().unwrap(), &mut inner.cx, pod_mut); - *view = new_view; + let new_fragment = (inner.app_logic)(&mut inner.data); + let mut dom_children_splice = DomChildrenSplice::new( + &mut inner.fragment_append_scratch, + &mut inner.elements, + &mut inner.vec_splice_scratch, + &inner.root, + inner.cx.fragment.clone(), + false, + ); + new_fragment.seq_rebuild( + fragment, + inner.fragment_state.as_mut().unwrap(), + &mut inner.cx, + &mut dom_children_splice, + ); + *fragment = new_fragment; } } diff --git a/xilem_web/web_examples/counter/src/main.rs b/xilem_web/web_examples/counter/src/main.rs index a4cc0b19e..d9dfda7e8 100644 --- a/xilem_web/web_examples/counter/src/main.rs +++ b/xilem_web/web_examples/counter/src/main.rs @@ -4,7 +4,7 @@ use xilem_web::{ document_body, elements::html as el, - interfaces::{Element, HtmlButtonElement, HtmlDivElement}, + interfaces::{Element, HtmlButtonElement}, App, DomFragment, }; @@ -55,10 +55,10 @@ fn huzzah(state: &mut AppState) -> impl DomFragment { (state.clicks >= 5).then_some("Huzzah, clicked at least 5 times") } -fn app_logic(state: &mut AppState) -> impl HtmlDivElement { - el::div(( +/// Even the root `app_logic` can return a sequence of views +fn app_logic(state: &mut AppState) -> impl DomFragment { + ( el::span(format!("clicked {} times", state.clicks)).class(state.class), - huzzah(state), el::br(()), btn("+1 click", |state, _| state.increment()), btn("-1 click", |state, _| state.decrement()), @@ -66,8 +66,10 @@ fn app_logic(state: &mut AppState) -> impl HtmlDivElement { btn("a different class", |state, _| state.change_class()), btn("change text", |state, _| state.change_text()), el::br(()), + huzzah(state), + el::br(()), state.text.clone(), - )) + ) } pub fn main() {