Skip to content

Commit

Permalink
xilem_web: Allow DomFragment instead of DomView as app_logic (#482
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
Philipp-M authored Aug 5, 2024
1 parent e27b3ce commit bb13f1a
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 48 deletions.
114 changes: 71 additions & 43 deletions xilem_web/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
// 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]>,
pub body: DynMessage,
}

/// 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,
}

Expand All @@ -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());
Expand All @@ -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;
}
}

Expand Down
12 changes: 7 additions & 5 deletions xilem_web/web_examples/counter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use xilem_web::{
document_body,
elements::html as el,
interfaces::{Element, HtmlButtonElement, HtmlDivElement},
interfaces::{Element, HtmlButtonElement},
App, DomFragment,
};

Expand Down Expand Up @@ -55,19 +55,21 @@ 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()),
btn("reset clicks", |state, _| state.reset()),
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() {
Expand Down

0 comments on commit bb13f1a

Please sign in to comment.