polish: change in cx to cx
This commit is contained in:
1 parent
0ded32e184
commit
9971ff215d
21 files changed
+426
-320
No files matched your search
@@ -113,9 +113,9 @@ pub static Example: FC<()> = |cx| {
|
|||||||
// rsx! is lazy, and the underlying closures cannot have the same type
|
// rsx! is lazy, and the underlying closures cannot have the same type
|
||||||
// Rendering produces the VNode type
|
// Rendering produces the VNode type
|
||||||
{match rand::gen_range::<i32>(1..3) {
|
{match rand::gen_range::<i32>(1..3) {
|
||||||
1 => rsx!(in cx, h1 { "big" })
|
1 => rsx!(cx, h1 { "big" })
|
||||||
2 => rsx!(in cx, h2 { "medium" })
|
2 => rsx!(cx, h2 { "medium" })
|
||||||
_ => rsx!(in cx, h3 { "small" })
|
_ => rsx!(cx, h3 { "small" })
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Optionals
|
// Optionals
|
||||||
@@ -128,7 +128,7 @@ pub static Example: FC<()> = |cx| {
|
|||||||
// Duplicating nodes
|
// Duplicating nodes
|
||||||
// Clones the nodes by reference, so they are literally identical
|
// Clones the nodes by reference, so they are literally identical
|
||||||
{{
|
{{
|
||||||
let node = rsx!(in cx, h1{ "TopNode" });
|
let node = rsx!(cx, h1{ "TopNode" });
|
||||||
(0..10).map(|_| node.clone())
|
(0..10).map(|_| node.clone())
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ Sometimes you want a signal to propagate across your app, either through far-awa
|
|||||||
const TITLE: Atom<String> = || "".to_string();
|
const TITLE: Atom<String> = || "".to_string();
|
||||||
const Provider: FC<()> = |cx| {
|
const Provider: FC<()> = |cx| {
|
||||||
let title = use_signal(&cx, &TITLE);
|
let title = use_signal(&cx, &TITLE);
|
||||||
rsx!(in cx, input { value: title })
|
rsx!(cx, input { value: title })
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ If we use the `TITLE` atom in another component, we can cause updates to flow be
|
|||||||
const Receiver: FC<()> = |cx| {
|
const Receiver: FC<()> = |cx| {
|
||||||
let title = use_signal(&cx, &TITLE);
|
let title = use_signal(&cx, &TITLE);
|
||||||
log::info!("This will only be called once!");
|
log::info!("This will only be called once!");
|
||||||
rsx!(in cx,
|
rsx!(cx,
|
||||||
div {
|
div {
|
||||||
h1 { "{title}" }
|
h1 { "{title}" }
|
||||||
div {}
|
div {}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ struct NoKeysProps {
|
|||||||
}
|
}
|
||||||
static AntipatternNoKeys: FC<NoKeysProps> = |cx| {
|
static AntipatternNoKeys: FC<NoKeysProps> = |cx| {
|
||||||
// WRONG: Make sure to add keys!
|
// WRONG: Make sure to add keys!
|
||||||
rsx!(in cx, ul {
|
rsx!(cx, ul {
|
||||||
{cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))}
|
{cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))}
|
||||||
});
|
});
|
||||||
// RIGHT: Like this:
|
// RIGHT: Like this:
|
||||||
rsx!(in cx, ul {
|
rsx!(cx, ul {
|
||||||
{cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))}
|
{cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -56,7 +56,7 @@ static AntipatternNoKeys: FC<NoKeysProps> = |cx| {
|
|||||||
/// an API for registering shared state without the ContextProvider pattern.
|
/// an API for registering shared state without the ContextProvider pattern.
|
||||||
static AntipatternNestedFragments: FC<()> = |cx| {
|
static AntipatternNestedFragments: FC<()> = |cx| {
|
||||||
// Try to avoid heavily nesting fragments
|
// Try to avoid heavily nesting fragments
|
||||||
rsx!(in cx,
|
rsx!(cx,
|
||||||
Fragment {
|
Fragment {
|
||||||
Fragment {
|
Fragment {
|
||||||
Fragment {
|
Fragment {
|
||||||
@@ -125,9 +125,9 @@ static AntipatternMisusedHooks: FC<MisuedHooksProps> = |cx| {
|
|||||||
// do not place a hook in the conditional!
|
// do not place a hook in the conditional!
|
||||||
// prefer to move it out of the conditional
|
// prefer to move it out of the conditional
|
||||||
let (state, set_state) = use_state(cx, || "hello world").classic();
|
let (state, set_state) = use_state(cx, || "hello world").classic();
|
||||||
rsx!(in cx, div { "{state}" })
|
rsx!(cx, div { "{state}" })
|
||||||
} else {
|
} else {
|
||||||
rsx!(in cx, div { "Not rendering state" })
|
rsx!(cx, div { "Not rendering state" })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub static Example0: FC<MyProps> = |cx| {
|
|||||||
// which will do essentially the same thing as `cx.render`.
|
// which will do essentially the same thing as `cx.render`.
|
||||||
//
|
//
|
||||||
// In short:
|
// In short:
|
||||||
// `rsx!(in cx, ...)` is shorthand for `cx.render(rsx!(...))`
|
// `rsx!(cx, ...)` is shorthand for `cx.render(rsx!(...))`
|
||||||
#[derive(PartialEq, Props)]
|
#[derive(PartialEq, Props)]
|
||||||
pub struct MyProps1 {
|
pub struct MyProps1 {
|
||||||
should_show: bool,
|
should_show: bool,
|
||||||
@@ -45,21 +45,21 @@ pub static Example1: FC<MyProps1> = |cx| {
|
|||||||
// With matching
|
// With matching
|
||||||
{match cx.should_show {
|
{match cx.should_show {
|
||||||
true => cx.render(rsx!(div {"it is true!"})),
|
true => cx.render(rsx!(div {"it is true!"})),
|
||||||
false => rsx!(in cx, div {"it is false!"}),
|
false => rsx!(cx, div {"it is false!"}),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// or with just regular conditions
|
// or with just regular conditions
|
||||||
{if cx.should_show {
|
{if cx.should_show {
|
||||||
rsx!(in cx, div {"it is true!"})
|
rsx!(cx, div {"it is true!"})
|
||||||
} else {
|
} else {
|
||||||
rsx!(in cx, div {"it is false!"})
|
rsx!(cx, div {"it is false!"})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// or with optional chaining
|
// or with optional chaining
|
||||||
{
|
{
|
||||||
cx.should_show
|
cx.should_show
|
||||||
.then(|| rsx!(in cx, div {"it is false!"}))
|
.then(|| rsx!(cx, div {"it is false!"}))
|
||||||
.unwrap_or_else(|| rsx!(in cx, div {"it is false!"}))
|
.unwrap_or_else(|| rsx!(cx, div {"it is false!"}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -81,9 +81,9 @@ pub static Example2: FC<MyProps2> = |cx| {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
{match cx.color {
|
{match cx.color {
|
||||||
Color::Green => rsx!(in cx, div {"it is Green!"}),
|
Color::Green => rsx!(cx, div {"it is Green!"}),
|
||||||
Color::Yellow => rsx!(in cx, div {"it is Yellow!"}),
|
Color::Yellow => rsx!(cx, div {"it is Yellow!"}),
|
||||||
Color::Red => rsx!(in cx, div {"it is Red!"}),
|
Color::Red => rsx!(cx, div {"it is Red!"}),
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ static ButtonList: FC<()> = |cx| {
|
|||||||
/// This shows how listeners may be without a visible change in the display.
|
/// This shows how listeners may be without a visible change in the display.
|
||||||
/// Check the console.
|
/// Check the console.
|
||||||
static NonUpdatingEvents: FC<()> = |cx| {
|
static NonUpdatingEvents: FC<()> = |cx| {
|
||||||
rsx!(in cx, div {
|
rsx!(cx, div {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| log::info!("Did not cause any updates!")
|
onclick: move |_| log::info!("Did not cause any updates!")
|
||||||
"Click me to log!"
|
"Click me to log!"
|
||||||
@@ -43,7 +43,7 @@ static NonUpdatingEvents: FC<()> = |cx| {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static DisablePropogation: FC<()> = |cx| {
|
static DisablePropogation: FC<()> = |cx| {
|
||||||
rsx!(in cx,
|
rsx!(cx,
|
||||||
div {
|
div {
|
||||||
onclick: move |_| log::info!("event propogated to the div!")
|
onclick: move |_| log::info!("event propogated to the div!")
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pub static Example: FC<()> = |cx| {
|
|||||||
cx,
|
cx,
|
||||||
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|
||||||
|cx, res| match res {
|
|cx, res| match res {
|
||||||
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
|
Ok(res) => rsx!(cx, img { src: "{res.message}" }),
|
||||||
Err(_) => rsx!(in cx, div { "No doggos for you :(" }),
|
Err(_) => rsx!(cx, div { "No doggos for you :(" }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ pub static Example: FC<()> = |cx| {
|
|||||||
// To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
// To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
||||||
// There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
// There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
||||||
{match true {
|
{match true {
|
||||||
true => rsx!(in cx, h1 {"Top text"}),
|
true => rsx!(cx, h1 {"Top text"}),
|
||||||
false => cx.render(rsx!( h1 {"Bottom text"}))
|
false => cx.render(rsx!( h1 {"Bottom text"}))
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -117,9 +117,9 @@ pub static Example: FC<()> = |cx| {
|
|||||||
|
|
||||||
// True conditions need to be rendered (same reasons as matching)
|
// True conditions need to be rendered (same reasons as matching)
|
||||||
{if true {
|
{if true {
|
||||||
rsx!(in cx, h1 {"Top text"})
|
rsx!(cx, h1 {"Top text"})
|
||||||
} else {
|
} else {
|
||||||
rsx!(in cx, h1 {"Bottom text"})
|
rsx!(cx, h1 {"Bottom text"})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// returning "None" is a bit noisy... but rare in practice
|
// returning "None" is a bit noisy... but rare in practice
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ static App: FC<()> = |cx| {
|
|||||||
let (selection, set_selection) = use_state(cx, || None as Option<usize>).classic();
|
let (selection, set_selection) = use_state(cx, || None as Option<usize>).classic();
|
||||||
|
|
||||||
let body = match selection {
|
let body = match selection {
|
||||||
Some(id) => rsx!(in cx, ReferenceItem { selected: *id }),
|
Some(id) => rsx!(cx, ReferenceItem { selected: *id }),
|
||||||
None => rsx!(in cx, div { "Select an concept to explore" }),
|
None => rsx!(cx, div { "Select an concept to explore" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
/// // To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
/// // To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
||||||
/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
||||||
/// {match true {
|
/// {match true {
|
||||||
/// true => rsx!(in cx, h1 {"Top text"}),
|
/// true => rsx!(cx, h1 {"Top text"}),
|
||||||
/// false => cx.render(rsx!( h1 {"Bottom text"}))
|
/// false => cx.render(rsx!( h1 {"Bottom text"}))
|
||||||
/// }}
|
/// }}
|
||||||
///
|
///
|
||||||
@@ -98,9 +98,9 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
///
|
///
|
||||||
/// // True conditions need to be rendered (same reasons as matching)
|
/// // True conditions need to be rendered (same reasons as matching)
|
||||||
/// {if true {
|
/// {if true {
|
||||||
/// rsx!(in cx, h1 {"Top text"})
|
/// rsx!(cx, h1 {"Top text"})
|
||||||
/// } else {
|
/// } else {
|
||||||
/// rsx!(in cx, h1 {"Bottom text"})
|
/// rsx!(cx, h1 {"Bottom text"})
|
||||||
/// }}
|
/// }}
|
||||||
///
|
///
|
||||||
/// // returning "None" is a bit noisy... but rare in practice
|
/// // returning "None" is a bit noisy... but rare in practice
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ impl Parse for RsxBody<AS_HTML> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
|
fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
|
||||||
let res = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) {
|
let res = if input.peek(Ident) && input.peek2(Token![,]) {
|
||||||
let _ = input.parse::<Token![in]>()?;
|
|
||||||
let name = input.parse::<Ident>()?;
|
let name = input.parse::<Ident>()?;
|
||||||
input.parse::<Token![,]>()?;
|
input.parse::<Token![,]>()?;
|
||||||
Some(name)
|
Some(name)
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"rust-analyzer.inlayHints.enable": false
|
"rust-analyzer.inlayHints.enable": true
|
||||||
}
|
}
|
||||||
@@ -162,13 +162,84 @@ impl<'src, P> Context<'src, P> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
|
pub fn try_consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
|
||||||
let getter = &self.scope.shared.get_shared_context;
|
let getter = &self.scope.shared.get_shared_context;
|
||||||
let ty = TypeId::of::<T>();
|
let ty = TypeId::of::<T>();
|
||||||
let idx = self.scope.our_arena_idx;
|
let idx = self.scope.our_arena_idx;
|
||||||
getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated"))
|
getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
|
||||||
|
///
|
||||||
|
/// This is a hook, so it should not be called conditionally!
|
||||||
|
///
|
||||||
|
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
|
||||||
|
/// so don't put it in a conditional.
|
||||||
|
///
|
||||||
|
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
|
||||||
|
/// the context via Rc/Weak.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// struct SharedState(&'static str);
|
||||||
|
///
|
||||||
|
/// static App: FC<()> = |cx| {
|
||||||
|
/// cx.provide_state(|| SharedState("world"));
|
||||||
|
/// rsx!(cx, Child {})
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// static Child: FC<()> = |cx| {
|
||||||
|
/// let state = cx.consume_state::<SharedState>();
|
||||||
|
/// rsx!(cx, div { "hello {state.0}" })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn provide_state<T, F>(self, init: F) -> &'src Rc<T>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
F: FnOnce() -> T,
|
||||||
|
{
|
||||||
|
//
|
||||||
|
let ty = TypeId::of::<T>();
|
||||||
|
let contains_key = self.scope.shared_contexts.borrow().contains_key(&ty);
|
||||||
|
|
||||||
|
let is_initialized = self.use_hook(
|
||||||
|
|_| false,
|
||||||
|
|s| {
|
||||||
|
let i = s.clone();
|
||||||
|
*s = true;
|
||||||
|
i
|
||||||
|
},
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
|
||||||
|
match (is_initialized, contains_key) {
|
||||||
|
// Do nothing, already initialized and already exists
|
||||||
|
(true, true) => {}
|
||||||
|
|
||||||
|
// Needs to be initialized
|
||||||
|
(false, false) => self.add_shared_state(init()),
|
||||||
|
|
||||||
|
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.consume_state().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses a context, storing the cached value around
|
||||||
|
///
|
||||||
|
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
|
||||||
|
/// context was added later. This allows using another hook as a fallback
|
||||||
|
///
|
||||||
|
pub fn consume_state<T: 'static>(self) -> Option<&'src Rc<T>> {
|
||||||
|
struct UseContextHook<C>(Option<Rc<C>>);
|
||||||
|
self.use_hook(
|
||||||
|
move |_| UseContextHook(self.try_consume_shared_state::<T>()),
|
||||||
|
move |hook| hook.0.as_ref(),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Store a value between renders
|
/// Store a value between renders
|
||||||
///
|
///
|
||||||
/// This is *the* foundational hook for all other hooks.
|
/// This is *the* foundational hook for all other hooks.
|
||||||
|
|||||||
@@ -18,69 +18,6 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
|
|
||||||
///
|
|
||||||
/// This is a hook, so it may not be called conditionally!
|
|
||||||
///
|
|
||||||
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
|
|
||||||
/// so don't put it in a conditional.
|
|
||||||
///
|
|
||||||
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
|
|
||||||
/// the context via Rc/Weak.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc<T>
|
|
||||||
where
|
|
||||||
T: 'static,
|
|
||||||
F: FnOnce() -> T,
|
|
||||||
{
|
|
||||||
let ty = TypeId::of::<T>();
|
|
||||||
let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty);
|
|
||||||
|
|
||||||
let is_initialized = cx.use_hook(
|
|
||||||
|_| false,
|
|
||||||
|s| {
|
|
||||||
let i = s.clone();
|
|
||||||
*s = true;
|
|
||||||
i
|
|
||||||
},
|
|
||||||
|_| {},
|
|
||||||
);
|
|
||||||
|
|
||||||
match (is_initialized, contains_key) {
|
|
||||||
// Do nothing, already initialized and already exists
|
|
||||||
(true, true) => {}
|
|
||||||
|
|
||||||
// Needs to be initialized
|
|
||||||
(false, false) => cx.add_shared_state(init()),
|
|
||||||
|
|
||||||
_ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
|
|
||||||
};
|
|
||||||
|
|
||||||
use_consume_state::<T, _>(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// There are hooks going on here!
|
|
||||||
pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc<T> {
|
|
||||||
use_try_consume_state::<T, _>(cx).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses a context, storing the cached value around
|
|
||||||
///
|
|
||||||
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
|
|
||||||
/// context was added later. This allows using another hook as a fallback
|
|
||||||
///
|
|
||||||
pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc<T>> {
|
|
||||||
struct UseContextHook<C>(Option<Rc<C>>);
|
|
||||||
|
|
||||||
cx.use_hook(
|
|
||||||
move |_| UseContextHook(cx.consume_shared_state::<T>()),
|
|
||||||
move |hook| hook.0.as_ref(),
|
|
||||||
|_| {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Awaits the given task, forcing the component to re-render when the value is ready.
|
/// Awaits the given task, forcing the component to re-render when the value is ready.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|||||||
+245
-204
@@ -151,6 +151,8 @@ pub(crate) struct Scheduler {
|
|||||||
|
|
||||||
pub pending_tasks: VecDeque<UserEvent>,
|
pub pending_tasks: VecDeque<UserEvent>,
|
||||||
|
|
||||||
|
pub batched_events: VecDeque<UserEvent>,
|
||||||
|
|
||||||
pub garbage_scopes: HashSet<ScopeId>,
|
pub garbage_scopes: HashSet<ScopeId>,
|
||||||
|
|
||||||
pub lanes: [PriorityLane; 4],
|
pub lanes: [PriorityLane; 4],
|
||||||
@@ -214,7 +216,7 @@ impl Scheduler {
|
|||||||
channel,
|
channel,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut async_tasks = FuturesUnordered::new();
|
let async_tasks = FuturesUnordered::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
@@ -233,6 +235,8 @@ impl Scheduler {
|
|||||||
|
|
||||||
pending_tasks: VecDeque::new(),
|
pending_tasks: VecDeque::new(),
|
||||||
|
|
||||||
|
batched_events: VecDeque::new(),
|
||||||
|
|
||||||
garbage_scopes: HashSet::new(),
|
garbage_scopes: HashSet::new(),
|
||||||
|
|
||||||
current_priority: EventPriority::Low,
|
current_priority: EventPriority::Low,
|
||||||
@@ -249,89 +253,70 @@ impl Scheduler {
|
|||||||
|
|
||||||
pub fn manually_poll_events(&mut self) {
|
pub fn manually_poll_events(&mut self) {
|
||||||
while let Ok(Some(msg)) = self.receiver.try_next() {
|
while let Ok(Some(msg)) = self.receiver.try_next() {
|
||||||
self.handle_channel_msg(msg);
|
match msg {
|
||||||
|
SchedulerMsg::UiEvent(event) => self.ui_events.push_front(event),
|
||||||
|
SchedulerMsg::Immediate(im) => self.pending_immediates.push_front(im),
|
||||||
|
|
||||||
|
// task stuff
|
||||||
|
SchedulerMsg::SubmitTask(_, _) => todo!(),
|
||||||
|
SchedulerMsg::ToggleTask(_) => todo!(),
|
||||||
|
SchedulerMsg::PauseTask(_) => todo!(),
|
||||||
|
SchedulerMsg::ResumeTask(_) => todo!(),
|
||||||
|
SchedulerMsg::DropTask(_) => todo!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts UI events into dirty scopes with various priorities
|
// returns true if the event is discrete
|
||||||
pub fn consume_pending_events(&mut self) {
|
pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
|
||||||
|
let (discrete, priority) = event_meta(&event);
|
||||||
|
|
||||||
|
if let Some(scope) = self.pool.get_scope_mut(event.scope) {
|
||||||
|
if let Some(element) = event.mounted_dom_id {
|
||||||
|
// TODO: bubble properly here
|
||||||
|
scope.call_listener(event.event, element);
|
||||||
|
|
||||||
|
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
|
||||||
|
//
|
||||||
|
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use EventPriority::*;
|
||||||
|
|
||||||
|
match priority {
|
||||||
|
Immediate => todo!(),
|
||||||
|
High => todo!(),
|
||||||
|
Medium => todo!(),
|
||||||
|
Low => todo!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
discrete
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_work(&mut self) {
|
||||||
// consume all events that are "continuous" to be batched
|
// consume all events that are "continuous" to be batched
|
||||||
// if we run into a discrete event, then bail early
|
// if we run into a discrete event, then bail early
|
||||||
|
|
||||||
while let Some(trigger) = self.ui_events.pop_back() {
|
self.current_priority = match (
|
||||||
if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
|
self.lanes[0].has_work(),
|
||||||
if let Some(element) = trigger.mounted_dom_id {
|
self.lanes[1].has_work(),
|
||||||
let priority = match trigger.name {
|
self.lanes[2].has_work(),
|
||||||
// clipboard
|
self.lanes[3].has_work(),
|
||||||
"copy" | "cut" | "paste" => EventPriority::Medium,
|
) {
|
||||||
|
(true, _, _, _) => EventPriority::Immediate,
|
||||||
// Composition
|
(false, true, _, _) => EventPriority::High,
|
||||||
"compositionend" | "compositionstart" | "compositionupdate" => {
|
(false, false, true, _) => EventPriority::Medium,
|
||||||
EventPriority::Low
|
(false, false, false, true) => EventPriority::Low,
|
||||||
}
|
(false, false, false, false) => {
|
||||||
|
// no work to do, process events
|
||||||
// Keyboard
|
EventPriority::Low
|
||||||
"keydown" | "keypress" | "keyup" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Focus
|
|
||||||
"focus" | "blur" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Form
|
|
||||||
"change" | "input" | "invalid" | "reset" | "submit" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Mouse
|
|
||||||
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend"
|
|
||||||
| "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart"
|
|
||||||
| "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove"
|
|
||||||
| "mouseout" | "mouseover" | "mouseup" => EventPriority::Low,
|
|
||||||
|
|
||||||
"mousemove" => EventPriority::Medium,
|
|
||||||
|
|
||||||
// Pointer
|
|
||||||
"pointerdown" | "pointermove" | "pointerup" | "pointercancel"
|
|
||||||
| "gotpointercapture" | "lostpointercapture" | "pointerenter"
|
|
||||||
| "pointerleave" | "pointerover" | "pointerout" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Selection
|
|
||||||
"select" | "touchcancel" | "touchend" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Touch
|
|
||||||
"touchmove" | "touchstart" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Wheel
|
|
||||||
"scroll" | "wheel" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Media
|
|
||||||
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
|
|
||||||
| "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata"
|
|
||||||
| "loadstart" | "pause" | "play" | "playing" | "progress"
|
|
||||||
| "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
|
||||||
| "timeupdate" | "volumechange" | "waiting" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Animation
|
|
||||||
"animationstart" | "animationend" | "animationiteration" => {
|
|
||||||
EventPriority::Low
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transition
|
|
||||||
"transitionend" => EventPriority::Low,
|
|
||||||
|
|
||||||
// Toggle
|
|
||||||
"toggle" => EventPriority::Low,
|
|
||||||
|
|
||||||
_ => EventPriority::Low,
|
|
||||||
};
|
|
||||||
|
|
||||||
scope.call_listener(trigger.event, element);
|
|
||||||
// let receiver = self.immediate_receiver.clone();
|
|
||||||
// let mut receiver = receiver.borrow_mut();
|
|
||||||
|
|
||||||
// // Drain the immediates into the dirty scopes, setting the appropiate priorities
|
|
||||||
// while let Ok(Some(dirty_scope)) = receiver.try_next() {
|
|
||||||
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Some(trigger) = self.ui_events.pop_back() {
|
||||||
|
if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,20 +330,6 @@ impl Scheduler {
|
|||||||
self.ui_events.len() > 0
|
self.ui_events.len() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift_priorities(&mut self) {
|
|
||||||
self.current_priority = match (
|
|
||||||
self.lanes[0].has_work(),
|
|
||||||
self.lanes[1].has_work(),
|
|
||||||
self.lanes[2].has_work(),
|
|
||||||
self.lanes[3].has_work(),
|
|
||||||
) {
|
|
||||||
(true, _, _, _) => EventPriority::Immediate,
|
|
||||||
(false, true, _, _) => EventPriority::High,
|
|
||||||
(false, false, true, _) => EventPriority::Medium,
|
|
||||||
(false, false, false, _) => EventPriority::Low,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
|
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
|
||||||
fn balance_lanes(&mut self) {}
|
fn balance_lanes(&mut self) {}
|
||||||
|
|
||||||
@@ -385,102 +356,45 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Work the scheduler down, not polling any ongoing tasks.
|
pub fn current_lane(&mut self) -> &mut PriorityLane {
|
||||||
///
|
match self.current_priority {
|
||||||
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
EventPriority::Immediate => &mut self.lanes[0],
|
||||||
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
EventPriority::High => &mut self.lanes[1],
|
||||||
let mut committed_mutations = Vec::new();
|
EventPriority::Medium => &mut self.lanes[2],
|
||||||
|
EventPriority::Low => &mut self.lanes[3],
|
||||||
self.manually_poll_events();
|
|
||||||
|
|
||||||
if !self.has_any_work() {
|
|
||||||
return committed_mutations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.consume_pending_events();
|
|
||||||
|
|
||||||
while self.has_any_work() {
|
|
||||||
self.shift_priorities();
|
|
||||||
self.work_on_current_lane(|| false, &mut committed_mutations);
|
|
||||||
}
|
|
||||||
|
|
||||||
committed_mutations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The primary workhorse of the VirtualDOM.
|
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
||||||
///
|
match msg {
|
||||||
/// Uses some fairly complex logic to schedule what work should be produced.
|
SchedulerMsg::Immediate(_) => todo!(),
|
||||||
///
|
|
||||||
/// Returns a list of successful mutations.
|
|
||||||
pub async fn work_with_deadline<'a>(
|
|
||||||
&'a mut self,
|
|
||||||
deadline: impl Future<Output = ()>,
|
|
||||||
) -> Vec<Mutations<'a>> {
|
|
||||||
/*
|
|
||||||
Strategy:
|
|
||||||
- When called, check for any UI events that might've been received since the last frame.
|
|
||||||
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
|
|
||||||
|
|
||||||
- If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high,"
|
SchedulerMsg::UiEvent(event) => {
|
||||||
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
|
//
|
||||||
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
|
|
||||||
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
|
|
||||||
'static components have a leg up here since their work can be re-used among multiple scopes.
|
|
||||||
"High priority" is only for blocking! Should only be used on "clicks"
|
|
||||||
|
|
||||||
- If there are no pending discrete events, then check for continuous events. These can be completely batched
|
self.handle_ui_event(event);
|
||||||
|
|
||||||
- we batch completely until we run into a discrete event
|
|
||||||
- all continuous events are batched together
|
|
||||||
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
|
|
||||||
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
|
|
||||||
|
|
||||||
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
|
|
||||||
- we need to keep track of element parents to be able to traverse properly
|
|
||||||
|
|
||||||
|
|
||||||
Open questions:
|
|
||||||
- what if we get two clicks from the component during the same slice?
|
|
||||||
- should we batch?
|
|
||||||
- react says no - they are continuous
|
|
||||||
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
|
||||||
*/
|
|
||||||
let mut committed_mutations = Vec::<Mutations<'static>>::new();
|
|
||||||
pin_mut!(deadline);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Internalize any pending work since the last time we ran
|
|
||||||
self.manually_poll_events();
|
|
||||||
|
|
||||||
// Wait for any new events if we have nothing to do
|
|
||||||
// todo: poll the events once even if there is work to do to prevent starvation
|
|
||||||
if !self.has_any_work() {
|
|
||||||
futures_util::select! {
|
|
||||||
msg = self.async_tasks.next() => {}
|
|
||||||
msg = self.receiver.next() => self.handle_channel_msg(msg.unwrap()),
|
|
||||||
_ = (&mut deadline).fuse() => return committed_mutations,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create work from the pending event queue
|
//
|
||||||
self.consume_pending_events();
|
SchedulerMsg::SubmitTask(_, _) => todo!(),
|
||||||
|
SchedulerMsg::ToggleTask(_) => todo!(),
|
||||||
// shift to the correct lane
|
SchedulerMsg::PauseTask(_) => todo!(),
|
||||||
self.shift_priorities();
|
SchedulerMsg::ResumeTask(_) => todo!(),
|
||||||
|
SchedulerMsg::DropTask(_) => todo!(),
|
||||||
let mut deadline_reached = || (&mut deadline).now_or_never().is_some();
|
|
||||||
|
|
||||||
let finished_before_deadline =
|
|
||||||
self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
|
|
||||||
|
|
||||||
if !finished_before_deadline {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
committed_mutations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
|
||||||
|
todo!()
|
||||||
|
// match priority {
|
||||||
|
// EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
|
||||||
|
// EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
|
||||||
|
// EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_any_work(&mut self) {}
|
||||||
|
|
||||||
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
||||||
///
|
///
|
||||||
/// Returns true if the lane is finished before the deadline could be met.
|
/// Returns true if the lane is finished before the deadline could be met.
|
||||||
@@ -535,40 +449,105 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_lane(&mut self) -> &mut PriorityLane {
|
/// The primary workhorse of the VirtualDOM.
|
||||||
match self.current_priority {
|
///
|
||||||
EventPriority::Immediate => &mut self.lanes[0],
|
/// Uses some fairly complex logic to schedule what work should be produced.
|
||||||
EventPriority::High => &mut self.lanes[1],
|
///
|
||||||
EventPriority::Medium => &mut self.lanes[2],
|
/// Returns a list of successful mutations.
|
||||||
EventPriority::Low => &mut self.lanes[3],
|
pub async fn work_with_deadline<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
deadline: impl Future<Output = ()>,
|
||||||
|
) -> Vec<Mutations<'a>> {
|
||||||
|
/*
|
||||||
|
Strategy:
|
||||||
|
- When called, check for any UI events that might've been received since the last frame.
|
||||||
|
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
|
||||||
|
|
||||||
|
- If there are any pending discrete events, then elevate our priorty level. If our priority level is already "high,"
|
||||||
|
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
|
||||||
|
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
|
||||||
|
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
|
||||||
|
'static components have a leg up here since their work can be re-used among multiple scopes.
|
||||||
|
"High priority" is only for blocking! Should only be used on "clicks"
|
||||||
|
|
||||||
|
- If there are no pending discrete events, then check for continuous events. These can be completely batched
|
||||||
|
|
||||||
|
- we batch completely until we run into a discrete event
|
||||||
|
- all continuous events are batched together
|
||||||
|
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
|
||||||
|
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
|
||||||
|
|
||||||
|
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
|
||||||
|
- we need to keep track of element parents to be able to traverse properly
|
||||||
|
|
||||||
|
|
||||||
|
Open questions:
|
||||||
|
- what if we get two clicks from the component during the same slice?
|
||||||
|
- should we batch?
|
||||||
|
- react says no - they are continuous
|
||||||
|
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
||||||
|
*/
|
||||||
|
let mut committed_mutations = Vec::<Mutations<'static>>::new();
|
||||||
|
pin_mut!(deadline);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Move work out of the scheduler message receiver and into dedicated message lanes
|
||||||
|
self.manually_poll_events();
|
||||||
|
|
||||||
|
// Wait for any new events if we have nothing to do
|
||||||
|
// todo: poll the events once even if there is work to do to prevent starvation
|
||||||
|
if !self.has_any_work() {
|
||||||
|
futures_util::select! {
|
||||||
|
// todo: find a solution for the exhausted queue problem
|
||||||
|
// msg = self.async_tasks.next() => {}
|
||||||
|
msg = self.receiver.next() => {
|
||||||
|
log::debug!("received work in scheduler");
|
||||||
|
self.handle_channel_msg(msg.unwrap())
|
||||||
|
},
|
||||||
|
_ = (&mut deadline).fuse() => return committed_mutations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch our priority, pop off any work
|
||||||
|
self.prepare_work();
|
||||||
|
|
||||||
|
let mut deadline_reached = || (&mut deadline).now_or_never().is_some();
|
||||||
|
|
||||||
|
let finished_before_deadline =
|
||||||
|
self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
|
||||||
|
|
||||||
|
if !finished_before_deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
committed_mutations
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
/// Work the scheduler down, not polling any ongoing tasks.
|
||||||
match msg {
|
///
|
||||||
SchedulerMsg::Immediate(_) => todo!(),
|
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
||||||
SchedulerMsg::UiEvent(_) => todo!(),
|
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
||||||
|
let mut committed_mutations = Vec::new();
|
||||||
|
|
||||||
//
|
self.manually_poll_events();
|
||||||
SchedulerMsg::SubmitTask(_, _) => todo!(),
|
|
||||||
SchedulerMsg::ToggleTask(_) => todo!(),
|
if !self.has_any_work() {
|
||||||
SchedulerMsg::PauseTask(_) => todo!(),
|
return committed_mutations;
|
||||||
SchedulerMsg::ResumeTask(_) => todo!(),
|
|
||||||
SchedulerMsg::DropTask(_) => todo!(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while self.has_any_work() {
|
||||||
|
self.prepare_work();
|
||||||
|
self.work_on_current_lane(|| false, &mut committed_mutations);
|
||||||
|
}
|
||||||
|
|
||||||
|
committed_mutations
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
|
/// Restart the entire VirtualDOM from scratch, wiping away any old state and components.
|
||||||
todo!()
|
///
|
||||||
// match priority {
|
/// Typically used to kickstart the VirtualDOM after initialization.
|
||||||
// EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
|
|
||||||
// EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
|
|
||||||
// EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
|
|
||||||
// };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
|
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
|
||||||
//
|
|
||||||
let mut shared = self.pool.clone();
|
let mut shared = self.pool.clone();
|
||||||
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
|
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
|
||||||
|
|
||||||
@@ -615,6 +594,8 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Scheduler {}
|
||||||
|
|
||||||
pub(crate) struct PriorityLane {
|
pub(crate) struct PriorityLane {
|
||||||
pub dirty_scopes: IndexSet<ScopeId>,
|
pub dirty_scopes: IndexSet<ScopeId>,
|
||||||
pub saved_state: Option<SavedDiffWork<'static>>,
|
pub saved_state: Option<SavedDiffWork<'static>>,
|
||||||
@@ -631,7 +612,7 @@ impl PriorityLane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn has_work(&self) -> bool {
|
fn has_work(&self) -> bool {
|
||||||
todo!()
|
self.dirty_scopes.len() > 0 || self.in_progress == true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn work(&mut self) {
|
fn work(&mut self) {
|
||||||
@@ -824,3 +805,63 @@ impl ResourcePool {
|
|||||||
|
|
||||||
pub fn borrow_bumpframe(&self) {}
|
pub fn borrow_bumpframe(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn event_meta(event: &UserEvent) -> (bool, EventPriority) {
|
||||||
|
use EventPriority::*;
|
||||||
|
|
||||||
|
match event.name {
|
||||||
|
// clipboard
|
||||||
|
"copy" | "cut" | "paste" => (true, Medium),
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
"compositionend" | "compositionstart" | "compositionupdate" => (true, Low),
|
||||||
|
|
||||||
|
// Keyboard
|
||||||
|
"keydown" | "keypress" | "keyup" => (true, High),
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
"focus" | "blur" => (true, Low),
|
||||||
|
|
||||||
|
// Form
|
||||||
|
"change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
|
||||||
|
|
||||||
|
// Mouse
|
||||||
|
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
||||||
|
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
||||||
|
| "mouseleave" | "mouseout" | "mouseover" | "mouseup" => (true, High),
|
||||||
|
|
||||||
|
"mousemove" => (false, Medium),
|
||||||
|
|
||||||
|
// Pointer
|
||||||
|
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
||||||
|
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
|
||||||
|
(true, Medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
"select" | "touchcancel" | "touchend" => (true, Medium),
|
||||||
|
|
||||||
|
// Touch
|
||||||
|
"touchmove" | "touchstart" => (true, Medium),
|
||||||
|
|
||||||
|
// Wheel
|
||||||
|
"scroll" | "wheel" => (false, Medium),
|
||||||
|
|
||||||
|
// Media
|
||||||
|
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
|
||||||
|
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
|
||||||
|
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
||||||
|
| "timeupdate" | "volumechange" | "waiting" => (true, Medium),
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
"animationstart" | "animationend" | "animationiteration" => (true, Medium),
|
||||||
|
|
||||||
|
// Transition
|
||||||
|
"transitionend" => (true, Medium),
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
"toggle" => (true, Medium),
|
||||||
|
|
||||||
|
_ => (true, Low),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -361,7 +361,7 @@ impl<'a> ScopeRenderer<'a> {
|
|||||||
write_indent(f, il);
|
write_indent(f, il);
|
||||||
write!(f, "\"{}\"\n", text.text)?
|
write!(f, "\"{}\"\n", text.text)?
|
||||||
}
|
}
|
||||||
VNode::Anchor(anchor) => {
|
VNode::Anchor(_anchor) => {
|
||||||
write_indent(f, il);
|
write_indent(f, il);
|
||||||
write!(f, "Anchor {{}}\n")?;
|
write!(f, "Anchor {{}}\n")?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ impl VirtualDom {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} );
|
/// static App: FC<()> = |cx| rsx!(cx, div {"hello"} );
|
||||||
/// let mut dom = VirtualDom::new(App);
|
/// let mut dom = VirtualDom::new(App);
|
||||||
/// loop {
|
/// loop {
|
||||||
/// let deadline = TimeoutFuture::from_ms(16);
|
/// let deadline = TimeoutFuture::from_ms(16);
|
||||||
@@ -345,13 +345,17 @@ impl VirtualDom {
|
|||||||
/// This lets us poll async tasks during idle periods without blocking the main thread.
|
/// This lets us poll async tasks during idle periods without blocking the main thread.
|
||||||
pub async fn wait_for_work(&mut self) {
|
pub async fn wait_for_work(&mut self) {
|
||||||
if self.scheduler.has_any_work() {
|
if self.scheduler.has_any_work() {
|
||||||
|
log::debug!("No need to wait for work, we already have some");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("No active work.... waiting for some...");
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
futures_util::select! {
|
futures_util::select! {
|
||||||
// hmm - will this resolve to none if there are no async tasks?
|
// // hmm - will this resolve to none if there are no async tasks?
|
||||||
_ = self.scheduler.async_tasks.next() => {}
|
// _ = self.scheduler.async_tasks.next() => {
|
||||||
|
// log::debug!("async task completed!");
|
||||||
|
// }
|
||||||
msg = self.scheduler.receiver.next() => self.scheduler.handle_channel_msg(msg.unwrap()),
|
msg = self.scheduler.receiver.next() => self.scheduler.handle_channel_msg(msg.unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom};
|
||||||
|
use dioxus_core as dioxus;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use DomEdit::*;
|
||||||
|
|
||||||
|
mod test_logging;
|
||||||
|
|
||||||
|
fn new_dom() -> TestDom {
|
||||||
|
const IS_LOGGING_ENABLED: bool = false;
|
||||||
|
test_logging::set_up_logging(IS_LOGGING_ENABLED);
|
||||||
|
TestDom::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_state_test() {
|
||||||
|
struct MySharedState(&'static str);
|
||||||
|
|
||||||
|
static App: FC<()> = |cx| {
|
||||||
|
cx.provide_state(|| MySharedState("world!"));
|
||||||
|
rsx!(cx, Child {})
|
||||||
|
};
|
||||||
|
|
||||||
|
static Child: FC<()> = |cx| {
|
||||||
|
let shared = cx.consume_state::<MySharedState>()?;
|
||||||
|
rsx!(cx, "Hello, {shared.0}")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(App);
|
||||||
|
let Mutations { edits, .. } = dom.rebuild();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
edits,
|
||||||
|
[
|
||||||
|
CreateTextNode {
|
||||||
|
id: 0,
|
||||||
|
text: "Hello, world!"
|
||||||
|
},
|
||||||
|
AppendChildren { many: 1 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,8 +35,13 @@ static App: FC<()> = |cx| {
|
|||||||
cx,
|
cx,
|
||||||
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
|
||||||
|cx, res| match res {
|
|cx, res| match res {
|
||||||
Ok(res) => rsx!(in cx, img { src: "{res.message}" }),
|
Ok(res) => rsx!(
|
||||||
Err(_err) => rsx!(in cx, div { "No doggos for you :(" }),
|
cx,
|
||||||
|
img {
|
||||||
|
src: "{res.message}"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Err(_err) => rsx!(cx, div { "No doggos for you :(" }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ static APP: FC<()> = |cx| {
|
|||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
button { onclick: move |_| count += 1, "add" }
|
button {
|
||||||
|
onclick: move |_| count += 1,
|
||||||
|
"Click to add."
|
||||||
|
"Current count: {count}"
|
||||||
|
}
|
||||||
ul {
|
ul {
|
||||||
{(0..*count).map(|f| rsx!{
|
{(0..*count).map(|f| rsx!{
|
||||||
li { "a - {f}" }
|
li { "a - {f}" }
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ impl WebsysDom {
|
|||||||
|
|
||||||
pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
|
pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
|
||||||
for edit in edits.drain(..) {
|
for edit in edits.drain(..) {
|
||||||
log::info!("Handling edit: {:#?}", edit);
|
// log::info!("Handling edit: {:#?}", edit);
|
||||||
match edit {
|
match edit {
|
||||||
DomEdit::PushRoot { id: root } => self.push(root),
|
DomEdit::PushRoot { id: root } => self.push(root),
|
||||||
DomEdit::PopRoot => self.pop(),
|
DomEdit::PopRoot => self.pop(),
|
||||||
@@ -530,6 +530,8 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
|||||||
|
|
||||||
let typ = event.type_();
|
let typ = event.type_();
|
||||||
|
|
||||||
|
log::debug!("Event type is {:?}", typ);
|
||||||
|
|
||||||
// let attrs = target.attributes();
|
// let attrs = target.attributes();
|
||||||
// for x in 0..attrs.length() {
|
// for x in 0..attrs.length() {
|
||||||
// let attr = attrs.item(x).unwrap();
|
// let attr = attrs.item(x).unwrap();
|
||||||
@@ -569,10 +571,6 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_websys_dom() -> Element {
|
|
||||||
load_document().get_element_by_id("dioxusroot").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_document() -> Document {
|
pub fn load_document() -> Document {
|
||||||
web_sys::window()
|
web_sys::window()
|
||||||
.expect("should have access to the Window")
|
.expect("should have access to the Window")
|
||||||
|
|||||||
+11
-5
@@ -117,10 +117,12 @@ pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T,
|
|||||||
|
|
||||||
intern_cached_strings();
|
intern_cached_strings();
|
||||||
|
|
||||||
let hydrating = cfg.hydrate;
|
let should_hydrate = cfg.hydrate;
|
||||||
|
|
||||||
let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
|
||||||
|
|
||||||
let tasks = dom.get_event_sender();
|
let tasks = dom.get_event_sender();
|
||||||
|
|
||||||
let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap());
|
let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap());
|
||||||
|
|
||||||
let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
|
let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
|
||||||
@@ -129,8 +131,10 @@ pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T,
|
|||||||
|
|
||||||
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
|
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
|
||||||
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
|
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
|
||||||
if !hydrating {
|
if !should_hydrate {
|
||||||
websys_dom.process_edits(&mut mutations.edits);
|
websys_dom.process_edits(&mut mutations.edits);
|
||||||
|
} else {
|
||||||
|
// websys dom processed the config and hydrated the dom already
|
||||||
}
|
}
|
||||||
|
|
||||||
let work_loop = ric_raf::RafLoop::new();
|
let work_loop = ric_raf::RafLoop::new();
|
||||||
@@ -144,12 +148,14 @@ pub async fn run_with_props<T: Properties + 'static>(root: FC<T>, root_props: T,
|
|||||||
let deadline = work_loop.wait_for_idle_time().await;
|
let deadline = work_loop.wait_for_idle_time().await;
|
||||||
|
|
||||||
// run the virtualdom work phase until the frame deadline is reached
|
// run the virtualdom work phase until the frame deadline is reached
|
||||||
let mut mutations = dom.run_with_deadline(deadline).await;
|
let mutations = dom.run_with_deadline(deadline).await;
|
||||||
|
|
||||||
// wait for the animation frame to fire so we can apply our changes
|
// wait for the animation frame to fire so we can apply our changes
|
||||||
work_loop.wait_for_raf().await;
|
work_loop.wait_for_raf().await;
|
||||||
|
|
||||||
// actually apply our changes during the animation frame
|
for mut edit in mutations {
|
||||||
websys_dom.process_edits(&mut mutations[0].edits);
|
// actually apply our changes during the animation frame
|
||||||
|
websys_dom.process_edits(&mut edit.edits);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user