Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xilem_web: Allow DomFragment instead of DomView as app_logic #482

Merged
merged 1 commit into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the name InitFragment for the app_logic, I'm open for other suggestions, it's also related to this

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