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.
This commit is contained in:
Philipp Mildenberger 2024-08-05 15:03:19 +02:00 committed by GitHub
parent e27b3ce0c2
commit bb13f1a760
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 48 deletions

View File

@ -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<T, V: DomView<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>);
pub struct App<State, Fragment: DomFragment<State>, InitFragment>(
Rc<RefCell<AppInner<State, Fragment, InitFragment>>>,
);
struct AppInner<T, V: DomView<T>, F: FnMut(&mut T) -> V> {
data: T,
struct AppInner<State, Fragment: DomFragment<State>, InitFragment> {
data: State,
root: web_sys::Node,
app_logic: F,
view: Option<V>,
state: Option<V::ViewState>,
element: Option<V::Element>,
app_logic: InitFragment,
fragment: Option<Fragment>,
fragment_state: Option<Fragment::SeqState>,
fragment_append_scratch: AppendVec<AnyPod>,
vec_splice_scratch: Vec<AnyPod>,
elements: Vec<AnyPod>,
cx: ViewCtx,
}
@ -31,15 +36,22 @@ pub(crate) trait AppRunner {
fn clone_box(&self) -> Box<dyn AppRunner>;
}
impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App<T, V, F> {
impl<State, Fragment: DomFragment<State>, InitFragment> Clone
for App<State, Fragment, InitFragment>
{
fn clone(&self) -> Self {
App(self.0.clone())
}
}
impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T, V, F> {
impl<State, Fragment, InitFragment> App<State, Fragment, InitFragment>
where
State: 'static,
Fragment: DomFragment<State> + '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<web_sys::Node>, data: T, app_logic: F) -> Self {
pub fn new(root: impl AsRef<web_sys::Node>, 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<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T
}
}
impl<T, V: DomView<T>, F: FnMut(&mut T) -> V> AppInner<T, V, F> {
pub fn new(root: web_sys::Node, data: T, app_logic: F) -> Self {
impl<State, Fragment: DomFragment<State>, InitFragment: FnMut(&mut State) -> Fragment>
AppInner<State, Fragment, InitFragment>
{
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<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner
for App<T, V, F>
impl<State, Fragment, InitFragment> AppRunner for App<State, Fragment, InitFragment>
where
State: 'static,
Fragment: DomFragment<State> + '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;
}
}

View File

@ -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<AppState> {
(state.clicks >= 5).then_some("Huzzah, clicked at least 5 times")
}
fn app_logic(state: &mut AppState) -> impl HtmlDivElement<AppState> {
el::div((
/// Even the root `app_logic` can return a sequence of views
fn app_logic(state: &mut AppState) -> impl DomFragment<AppState> {
(
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<AppState> {
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() {