polish: change in cx to cx

This commit is contained in:
Jonathan Kelley committed 2021-09-16 13:20:04 -04:00
1 parent 0ded32e184
commit 9971ff215d
21 files changed
+426 -320

No files matched your search

+4 -4
View File
@@ -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())
}} }}
+2 -2
View File
@@ -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 {}
+5 -5
View File
@@ -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" })
} }
}; };
+9 -9
View File
@@ -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!"}),
}} }}
} }
}) })
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 :(" }),
}, },
); );
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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! {
+3 -3
View File
@@ -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
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"rust-analyzer.inlayHints.enable": false "rust-analyzer.inlayHints.enable": true
} }
+72 -1
View File
@@ -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.
-63
View File
@@ -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
View File
@@ -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),
}
}
+1 -1
View File
@@ -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")?;
} }
+7 -3
View File
@@ -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()),
} }
} }
+41
View File
@@ -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 },
]
);
}
+7 -2
View File
@@ -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 :(" }),
}, },
); );
+5 -1
View File
@@ -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}" }
+3 -5
View File
@@ -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
View File
@@ -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);
}
} }
} }