-
Notifications
You must be signed in to change notification settings - Fork 120
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: Add a await_once
view, and a simple example suspense
showing it in action.
#452
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,118 @@ | ||||||||||
// Copyright 2024 the Xilem Authors and the Druid Authors | ||||||||||
// SPDX-License-Identifier: Apache-2.0 | ||||||||||
|
||||||||||
use std::{future::Future, marker::PhantomData}; | ||||||||||
|
||||||||||
use wasm_bindgen::UnwrapThrowExt; | ||||||||||
use wasm_bindgen_futures::spawn_local; | ||||||||||
use xilem_core::{MessageResult, Mut, NoElement, View, ViewId}; | ||||||||||
|
||||||||||
use crate::{DynMessage, OptionalAction, ViewCtx}; | ||||||||||
|
||||||||||
/// Await a future returned by `init_future`, `callback` is called with the output of the future. `init_future` will only be invoked once. Use [`await_once`] for construction of this [`View`] | ||||||||||
pub struct AwaitOnce<InitFuture, Callback, State, Action> { | ||||||||||
init_future: InitFuture, | ||||||||||
callback: Callback, | ||||||||||
phantom: PhantomData<fn() -> (State, Action)>, | ||||||||||
} | ||||||||||
|
||||||||||
/// Await a future returned by `init_future`, `callback` is called with the output of the future. `init_future` will only be invoked once. | ||||||||||
/// | ||||||||||
/// # Examples | ||||||||||
/// | ||||||||||
/// ``` | ||||||||||
/// use xilem_web::{core::fork, concurrent::await_once, elements::html::div, interfaces::Element}; | ||||||||||
/// | ||||||||||
/// fn app_logic(state: &mut i32) -> impl Element<i32> { | ||||||||||
/// fork( | ||||||||||
/// div(*state), | ||||||||||
/// await_once( | ||||||||||
/// |_state: &mut i32| std::future::ready(42), | ||||||||||
/// |state: &mut i32, meaning_of_life| *state = meaning_of_life, | ||||||||||
/// ) | ||||||||||
/// ) | ||||||||||
/// } | ||||||||||
/// ``` | ||||||||||
pub fn await_once<State, Action, FOut, F, InitFuture, OA, Callback>( | ||||||||||
init_future: InitFuture, | ||||||||||
callback: Callback, | ||||||||||
) -> AwaitOnce<InitFuture, Callback, State, Action> | ||||||||||
where | ||||||||||
State: 'static, | ||||||||||
Action: 'static, | ||||||||||
FOut: std::fmt::Debug + 'static, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
....and of course |
||||||||||
F: Future<Output = FOut> + 'static, | ||||||||||
InitFuture: Fn(&mut State) -> F + 'static, | ||||||||||
OA: OptionalAction<Action> + 'static, | ||||||||||
Callback: Fn(&mut State, FOut) -> OA + 'static, | ||||||||||
{ | ||||||||||
AwaitOnce { | ||||||||||
init_future, | ||||||||||
callback, | ||||||||||
phantom: PhantomData, | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
pub struct AwaitOnceState<F> { | ||||||||||
future: Option<F>, | ||||||||||
} | ||||||||||
|
||||||||||
impl<State, Action, InitFuture, F, FOut, Callback, OA> View<State, Action, ViewCtx, DynMessage> | ||||||||||
for AwaitOnce<InitFuture, Callback, State, Action> | ||||||||||
where | ||||||||||
State: 'static, | ||||||||||
Action: 'static, | ||||||||||
FOut: std::fmt::Debug + 'static, | ||||||||||
F: Future<Output = FOut> + 'static, | ||||||||||
InitFuture: Fn(&mut State) -> F + 'static, | ||||||||||
OA: OptionalAction<Action> + 'static, | ||||||||||
Callback: Fn(&mut State, FOut) -> OA + 'static, | ||||||||||
{ | ||||||||||
type Element = NoElement; | ||||||||||
|
||||||||||
type ViewState = AwaitOnceState<F>; | ||||||||||
|
||||||||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { | ||||||||||
let thunk = ctx.message_thunk(); | ||||||||||
// we can't directly push the initial message, as this would be executed directly (not in the next microtick), which in turn means that the already mutably borrowed `App` would be borrowed again. | ||||||||||
// So we have to delay this with a spawn_local | ||||||||||
spawn_local(async move { thunk.push_message(None::<FOut>) }); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can remember, we already have this situation in several use cases. I wonder if it is time to either extend the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I've thought about this as well, makes sense. Maybe we want to rename |
||||||||||
(NoElement, AwaitOnceState { future: None }) | ||||||||||
} | ||||||||||
|
||||||||||
fn rebuild<'el>( | ||||||||||
&self, | ||||||||||
_prev: &Self, | ||||||||||
view_state: &mut Self::ViewState, | ||||||||||
ctx: &mut ViewCtx, | ||||||||||
(): Mut<'el, Self::Element>, | ||||||||||
) -> Mut<'el, Self::Element> { | ||||||||||
if let Some(future) = view_state.future.take() { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
let thunk = ctx.message_thunk(); | ||||||||||
spawn_local(async move { thunk.push_message(Some(future.await)) }); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
fn teardown(&self, _: &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {} | ||||||||||
|
||||||||||
fn message( | ||||||||||
&self, | ||||||||||
view_state: &mut Self::ViewState, | ||||||||||
id_path: &[ViewId], | ||||||||||
message: DynMessage, | ||||||||||
app_state: &mut State, | ||||||||||
) -> MessageResult<Action, DynMessage> { | ||||||||||
assert!(id_path.is_empty()); // `debug_assert!` instead? to save some bytes in the release binary? | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ..yes, I almost always use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, my thinkings similar, it's probably better to get bug reports when this is somehow triggered. |
||||||||||
match *message.downcast().unwrap_throw() { | ||||||||||
Some(future_output) => match (self.callback)(app_state, future_output).action() { | ||||||||||
Some(action) => MessageResult::Action(action), | ||||||||||
None => MessageResult::Nop, | ||||||||||
}, | ||||||||||
// Initial trigger in build, invoke the init_future and spawn it in `View::rebuild` | ||||||||||
None => { | ||||||||||
view_state.future = Some((self.init_future)(app_state)); | ||||||||||
MessageResult::RequestRebuild | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "suspense" | ||
version = "0.1.0" | ||
publish = false | ||
license.workspace = true | ||
edition.workspace = true | ||
|
||
[lints] | ||
workspace = true | ||
|
||
[dependencies] | ||
console_error_panic_hook = "0.1" | ||
wasm-bindgen = "0.2.92" | ||
web-sys = "0.3.69" | ||
xilem_web = { path = "../.." } | ||
gloo-timers = { version = "0.3.0", features = ["futures"] } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
|
||
<head> | ||
<title>Suspense | Xilem Web</title> | ||
<style> | ||
.blink { | ||
animation: blinker 1s infinite; | ||
} | ||
|
||
@keyframes blinker { | ||
from { | ||
color: blueviolet; | ||
} | ||
|
||
50% { | ||
color: hotpink; | ||
} | ||
|
||
to { | ||
color: blueviolet; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body></body> | ||
|
||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright 2023 the Xilem Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
use gloo_timers::future::TimeoutFuture; | ||
use xilem_web::{ | ||
concurrent::await_once, | ||
core::{fork, one_of::Either}, | ||
document_body, | ||
elements::html::{h1, p}, | ||
interfaces::Element, | ||
App, | ||
}; | ||
|
||
fn app_logic(view_has_resolved: &mut bool) -> impl Element<bool> { | ||
let view = if !*view_has_resolved { | ||
Either::A(p("This will be replaced soon")) | ||
} else { | ||
Either::B(h1("The time has come for fanciness")) | ||
}; | ||
fork( | ||
// note how the `Class` view is applied to either the p or the h1 element | ||
view.class(view_has_resolved.then_some("blink")), | ||
await_once( | ||
|_| TimeoutFuture::new(5000), | ||
|view_has_resolved: &mut bool, _| { | ||
*view_has_resolved = true; | ||
}, | ||
), | ||
) | ||
} | ||
|
||
pub fn main() { | ||
console_error_panic_hook::set_once(); | ||
App::new(document_body(), false, app_logic).run(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a sentence about the fact that a change of
&mut AppState
has no effect at all and that if it should be so, thenasync_repeat
must be used for it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean a change in
AppState
has effect (in a rerender)? It just won't have an effect when using interior mutability within e.g. the future. (Which we should probably generally document).That was actually a reason why I wanted to include it in the
init_future
so that some kind ofis_loading
state could be set.