From 9971ff215db6f771b7ec1cae2517c85d47d38622 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 16 Sep 2021 13:20:04 -0400 Subject: [PATCH] polish: change in cx to cx --- docs/main-concepts/03-rsx.md | 8 +- docs/main-concepts/12-signals.md | 4 +- examples/reference/antipatterns.rs | 10 +- examples/reference/conditional_rendering.rs | 18 +- examples/reference/listener.rs | 4 +- examples/reference/suspense.rs | 4 +- examples/rsx_usage.rs | 6 +- examples/showcase.rs | 4 +- packages/core-macro/src/lib.rs | 6 +- packages/core-macro/src/rsx/body.rs | 3 +- packages/core/.vscode/settings.json | 2 +- packages/core/src/context.rs | 73 +++- packages/core/src/hooks.rs | 63 --- packages/core/src/scheduler.rs | 449 +++++++++++--------- packages/core/src/scope.rs | 2 +- packages/core/src/virtual_dom.rs | 10 +- packages/core/tests/sharedstate.rs | 41 ++ packages/web/examples/async_web.rs | 9 +- packages/web/examples/basic.rs | 6 +- packages/web/src/dom.rs | 8 +- packages/web/src/lib.rs | 16 +- 21 files changed, 426 insertions(+), 320 deletions(-) create mode 100644 packages/core/tests/sharedstate.rs diff --git a/docs/main-concepts/03-rsx.md b/docs/main-concepts/03-rsx.md index e4f377d98..a615eb986 100644 --- a/docs/main-concepts/03-rsx.md +++ b/docs/main-concepts/03-rsx.md @@ -113,9 +113,9 @@ pub static Example: FC<()> = |cx| { // rsx! is lazy, and the underlying closures cannot have the same type // Rendering produces the VNode type {match rand::gen_range::(1..3) { - 1 => rsx!(in cx, h1 { "big" }) - 2 => rsx!(in cx, h2 { "medium" }) - _ => rsx!(in cx, h3 { "small" }) + 1 => rsx!(cx, h1 { "big" }) + 2 => rsx!(cx, h2 { "medium" }) + _ => rsx!(cx, h3 { "small" }) }} // Optionals @@ -128,7 +128,7 @@ pub static Example: FC<()> = |cx| { // Duplicating nodes // 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()) }} diff --git a/docs/main-concepts/12-signals.md b/docs/main-concepts/12-signals.md index aca4675c2..bfe6e9c3c 100644 --- a/docs/main-concepts/12-signals.md +++ b/docs/main-concepts/12-signals.md @@ -98,7 +98,7 @@ Sometimes you want a signal to propagate across your app, either through far-awa const TITLE: Atom = || "".to_string(); const Provider: FC<()> = |cx| { 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| { let title = use_signal(&cx, &TITLE); log::info!("This will only be called once!"); - rsx!(in cx, + rsx!(cx, div { h1 { "{title}" } div {} diff --git a/examples/reference/antipatterns.rs b/examples/reference/antipatterns.rs index 90b694a63..e6eff4df7 100644 --- a/examples/reference/antipatterns.rs +++ b/examples/reference/antipatterns.rs @@ -34,11 +34,11 @@ struct NoKeysProps { } static AntipatternNoKeys: FC = |cx| { // WRONG: Make sure to add keys! - rsx!(in cx, ul { + rsx!(cx, ul { {cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} }); // RIGHT: Like this: - rsx!(in cx, ul { + rsx!(cx, ul { {cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))} }) }; @@ -56,7 +56,7 @@ static AntipatternNoKeys: FC = |cx| { /// an API for registering shared state without the ContextProvider pattern. static AntipatternNestedFragments: FC<()> = |cx| { // Try to avoid heavily nesting fragments - rsx!(in cx, + rsx!(cx, Fragment { Fragment { Fragment { @@ -125,9 +125,9 @@ static AntipatternMisusedHooks: FC = |cx| { // do not place a hook in the conditional! // prefer to move it out of the conditional let (state, set_state) = use_state(cx, || "hello world").classic(); - rsx!(in cx, div { "{state}" }) + rsx!(cx, div { "{state}" }) } else { - rsx!(in cx, div { "Not rendering state" }) + rsx!(cx, div { "Not rendering state" }) } }; diff --git a/examples/reference/conditional_rendering.rs b/examples/reference/conditional_rendering.rs index 6a349d54e..0a5360826 100644 --- a/examples/reference/conditional_rendering.rs +++ b/examples/reference/conditional_rendering.rs @@ -34,7 +34,7 @@ pub static Example0: FC = |cx| { // which will do essentially the same thing as `cx.render`. // // In short: -// `rsx!(in cx, ...)` is shorthand for `cx.render(rsx!(...))` +// `rsx!(cx, ...)` is shorthand for `cx.render(rsx!(...))` #[derive(PartialEq, Props)] pub struct MyProps1 { should_show: bool, @@ -45,21 +45,21 @@ pub static Example1: FC = |cx| { // With matching {match cx.should_show { 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 {if cx.should_show { - rsx!(in cx, div {"it is true!"}) + rsx!(cx, div {"it is true!"}) } else { - rsx!(in cx, div {"it is false!"}) + rsx!(cx, div {"it is false!"}) }} // or with optional chaining { cx.should_show - .then(|| rsx!(in cx, div {"it is false!"})) - .unwrap_or_else(|| rsx!(in cx, div {"it is false!"})) + .then(|| rsx!(cx, div {"it is false!"})) + .unwrap_or_else(|| rsx!(cx, div {"it is false!"})) } } }) @@ -81,9 +81,9 @@ pub static Example2: FC = |cx| { cx.render(rsx! { div { {match cx.color { - Color::Green => rsx!(in cx, div {"it is Green!"}), - Color::Yellow => rsx!(in cx, div {"it is Yellow!"}), - Color::Red => rsx!(in cx, div {"it is Red!"}), + Color::Green => rsx!(cx, div {"it is Green!"}), + Color::Yellow => rsx!(cx, div {"it is Yellow!"}), + Color::Red => rsx!(cx, div {"it is Red!"}), }} } }) diff --git a/examples/reference/listener.rs b/examples/reference/listener.rs index 454b0087e..269811a02 100644 --- a/examples/reference/listener.rs +++ b/examples/reference/listener.rs @@ -34,7 +34,7 @@ static ButtonList: FC<()> = |cx| { /// This shows how listeners may be without a visible change in the display. /// Check the console. static NonUpdatingEvents: FC<()> = |cx| { - rsx!(in cx, div { + rsx!(cx, div { button { onclick: move |_| log::info!("Did not cause any updates!") "Click me to log!" @@ -43,7 +43,7 @@ static NonUpdatingEvents: FC<()> = |cx| { }; static DisablePropogation: FC<()> = |cx| { - rsx!(in cx, + rsx!(cx, div { onclick: move |_| log::info!("event propogated to the div!") button { diff --git a/examples/reference/suspense.rs b/examples/reference/suspense.rs index d27572d30..c0d8d54ec 100644 --- a/examples/reference/suspense.rs +++ b/examples/reference/suspense.rs @@ -19,8 +19,8 @@ pub static Example: FC<()> = |cx| { cx, || surf::get(ENDPOINT).recv_json::(), |cx, res| match res { - Ok(res) => rsx!(in cx, img { src: "{res.message}" }), - Err(_) => rsx!(in cx, div { "No doggos for you :(" }), + Ok(res) => rsx!(cx, img { src: "{res.message}" }), + Err(_) => rsx!(cx, div { "No doggos for you :(" }), }, ); diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index ba2e9b2f5..5a1227f75 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -106,7 +106,7 @@ pub static Example: FC<()> = |cx| { // 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) {match true { - true => rsx!(in cx, h1 {"Top text"}), + true => rsx!(cx, h1 {"Top 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) {if true { - rsx!(in cx, h1 {"Top text"}) + rsx!(cx, h1 {"Top text"}) } else { - rsx!(in cx, h1 {"Bottom text"}) + rsx!(cx, h1 {"Bottom text"}) }} // returning "None" is a bit noisy... but rare in practice diff --git a/examples/showcase.rs b/examples/showcase.rs index 12652c887..dd9370fd6 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -15,8 +15,8 @@ static App: FC<()> = |cx| { let (selection, set_selection) = use_state(cx, || None as Option).classic(); let body = match selection { - Some(id) => rsx!(in cx, ReferenceItem { selected: *id }), - None => rsx!(in cx, div { "Select an concept to explore" }), + Some(id) => rsx!(cx, ReferenceItem { selected: *id }), + None => rsx!(cx, div { "Select an concept to explore" }), }; cx.render(rsx! { diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 8d124800a..6e3c1f537 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -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. /// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) /// {match true { -/// true => rsx!(in cx, h1 {"Top text"}), +/// true => rsx!(cx, h1 {"Top 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) /// {if true { -/// rsx!(in cx, h1 {"Top text"}) +/// rsx!(cx, h1 {"Top text"}) /// } else { -/// rsx!(in cx, h1 {"Bottom text"}) +/// rsx!(cx, h1 {"Bottom text"}) /// }} /// /// // returning "None" is a bit noisy... but rare in practice diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index 8e0e97d46..718c1c83d 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -39,8 +39,7 @@ impl Parse for RsxBody { } fn try_parse_custom_context(input: ParseStream) -> Result> { - let res = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { - let _ = input.parse::()?; + let res = if input.peek(Ident) && input.peek2(Token![,]) { let name = input.parse::()?; input.parse::()?; Some(name) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 21d89436e..3e86a3c88 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -162,13 +162,84 @@ impl<'src, P> Context<'src, P> { }); } - pub fn consume_shared_state(self) -> Option> { + pub fn try_consume_shared_state(self) -> Option> { let getter = &self.scope.shared.get_shared_context; let ty = TypeId::of::(); let idx = self.scope.our_arena_idx; 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::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn provide_state(self, init: F) -> &'src Rc + where + T: 'static, + F: FnOnce() -> T, + { + // + let ty = TypeId::of::(); + 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(self) -> Option<&'src Rc> { + struct UseContextHook(Option>); + self.use_hook( + move |_| UseContextHook(self.try_consume_shared_state::()), + move |hook| hook.0.as_ref(), + |_| {}, + ) + } + /// Store a value between renders /// /// This is *the* foundational hook for all other hooks. diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 47e7fcda9..24f8ff8e7 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -18,69 +18,6 @@ use std::{ 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 -where - T: 'static, - F: FnOnce() -> T, -{ - let ty = TypeId::of::(); - 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::(cx) -} - -/// There are hooks going on here! -pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc { - use_try_consume_state::(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> { - struct UseContextHook(Option>); - - cx.use_hook( - move |_| UseContextHook(cx.consume_shared_state::()), - move |hook| hook.0.as_ref(), - |_| {}, - ) -} - /// Awaits the given task, forcing the component to re-render when the value is ready. /// /// diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 3f732af48..dfcac8519 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -151,6 +151,8 @@ pub(crate) struct Scheduler { pub pending_tasks: VecDeque, + pub batched_events: VecDeque, + pub garbage_scopes: HashSet, pub lanes: [PriorityLane; 4], @@ -214,7 +216,7 @@ impl Scheduler { channel, }; - let mut async_tasks = FuturesUnordered::new(); + let async_tasks = FuturesUnordered::new(); Self { pool, @@ -233,6 +235,8 @@ impl Scheduler { pending_tasks: VecDeque::new(), + batched_events: VecDeque::new(), + garbage_scopes: HashSet::new(), current_priority: EventPriority::Low, @@ -249,89 +253,70 @@ impl Scheduler { pub fn manually_poll_events(&mut self) { 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 - pub fn consume_pending_events(&mut self) { + // returns true if the event is discrete + 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 // if we run into a discrete event, then bail early - while let Some(trigger) = self.ui_events.pop_back() { - if let Some(scope) = self.pool.get_scope_mut(trigger.scope) { - if let Some(element) = trigger.mounted_dom_id { - let priority = match trigger.name { - // clipboard - "copy" | "cut" | "paste" => EventPriority::Medium, - - // Composition - "compositionend" | "compositionstart" | "compositionupdate" => { - EventPriority::Low - } - - // Keyboard - "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) - // } - } + 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, true) => EventPriority::Low, + (false, false, false, false) => { + // no work to do, process events + EventPriority::Low } + }; + + 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 } - 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 fn balance_lanes(&mut self) {} @@ -385,102 +356,45 @@ impl Scheduler { } } - /// Work the scheduler down, not polling any ongoing tasks. - /// - /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. - pub fn work_sync<'a>(&'a mut self) -> Vec> { - let mut committed_mutations = Vec::new(); - - self.manually_poll_events(); - - if !self.has_any_work() { - return committed_mutations; + pub fn current_lane(&mut self) -> &mut PriorityLane { + match self.current_priority { + EventPriority::Immediate => &mut self.lanes[0], + EventPriority::High => &mut self.lanes[1], + EventPriority::Medium => &mut self.lanes[2], + EventPriority::Low => &mut self.lanes[3], } - - 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. - /// - /// Uses some fairly complex logic to schedule what work should be produced. - /// - /// Returns a list of successful mutations. - pub async fn work_with_deadline<'a>( - &'a mut self, - deadline: impl Future, - ) -> Vec> { - /* - 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. + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + match msg { + SchedulerMsg::Immediate(_) => todo!(), - - 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" + SchedulerMsg::UiEvent(event) => { + // - - 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::>::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, - } + self.handle_ui_event(event); } - // Create work from the pending event queue - self.consume_pending_events(); - - // shift to the correct lane - self.shift_priorities(); - - 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; - } + // + SchedulerMsg::SubmitTask(_, _) => todo!(), + SchedulerMsg::ToggleTask(_) => todo!(), + SchedulerMsg::PauseTask(_) => todo!(), + SchedulerMsg::ResumeTask(_) => todo!(), + SchedulerMsg::DropTask(_) => todo!(), } - - 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. /// /// 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 { - match self.current_priority { - EventPriority::Immediate => &mut self.lanes[0], - EventPriority::High => &mut self.lanes[1], - EventPriority::Medium => &mut self.lanes[2], - EventPriority::Low => &mut self.lanes[3], + /// The primary workhorse of the VirtualDOM. + /// + /// Uses some fairly complex logic to schedule what work should be produced. + /// + /// Returns a list of successful mutations. + pub async fn work_with_deadline<'a>( + &'a mut self, + deadline: impl Future, + ) -> Vec> { + /* + 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::>::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) { - match msg { - SchedulerMsg::Immediate(_) => todo!(), - SchedulerMsg::UiEvent(_) => todo!(), + /// Work the scheduler down, not polling any ongoing tasks. + /// + /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. + pub fn work_sync<'a>(&'a mut self) -> Vec> { + let mut committed_mutations = Vec::new(); - // - SchedulerMsg::SubmitTask(_, _) => todo!(), - SchedulerMsg::ToggleTask(_) => todo!(), - SchedulerMsg::PauseTask(_) => todo!(), - SchedulerMsg::ResumeTask(_) => todo!(), - SchedulerMsg::DropTask(_) => todo!(), + self.manually_poll_events(); + + if !self.has_any_work() { + return committed_mutations; } + + 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) { - 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), - // }; - } - + /// Restart the entire VirtualDOM from scratch, wiping away any old state and components. + /// + /// Typically used to kickstart the VirtualDOM after initialization. pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations { - // let mut shared = self.pool.clone(); let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); @@ -615,6 +594,8 @@ impl Scheduler { } } +impl Scheduler {} + pub(crate) struct PriorityLane { pub dirty_scopes: IndexSet, pub saved_state: Option>, @@ -631,7 +612,7 @@ impl PriorityLane { } fn has_work(&self) -> bool { - todo!() + self.dirty_scopes.len() > 0 || self.in_progress == true } fn work(&mut self) { @@ -824,3 +805,63 @@ impl ResourcePool { 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), + } +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index af69429de..357cf2e2e 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -361,7 +361,7 @@ impl<'a> ScopeRenderer<'a> { write_indent(f, il); write!(f, "\"{}\"\n", text.text)? } - VNode::Anchor(anchor) => { + VNode::Anchor(_anchor) => { write_indent(f, il); write!(f, "Anchor {{}}\n")?; } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6333c0817..dfc4f75ea 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -314,7 +314,7 @@ impl VirtualDom { /// # Example /// /// ```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); /// loop { /// 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. pub async fn wait_for_work(&mut self) { if self.scheduler.has_any_work() { + log::debug!("No need to wait for work, we already have some"); return; } + log::debug!("No active work.... waiting for some..."); use futures_util::StreamExt; futures_util::select! { - // hmm - will this resolve to none if there are no async tasks? - _ = self.scheduler.async_tasks.next() => {} + // // hmm - will this resolve to none if there are no async tasks? + // _ = self.scheduler.async_tasks.next() => { + // log::debug!("async task completed!"); + // } msg = self.scheduler.receiver.next() => self.scheduler.handle_channel_msg(msg.unwrap()), } } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs new file mode 100644 index 000000000..059549db5 --- /dev/null +++ b/packages/core/tests/sharedstate.rs @@ -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::()?; + 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 }, + ] + ); +} diff --git a/packages/web/examples/async_web.rs b/packages/web/examples/async_web.rs index 6c1a8f439..e1d1d49e6 100644 --- a/packages/web/examples/async_web.rs +++ b/packages/web/examples/async_web.rs @@ -35,8 +35,13 @@ static App: FC<()> = |cx| { cx, || surf::get(ENDPOINT).recv_json::(), |cx, res| match res { - Ok(res) => rsx!(in cx, img { src: "{res.message}" }), - Err(_err) => rsx!(in cx, div { "No doggos for you :(" }), + Ok(res) => rsx!( + cx, + img { + src: "{res.message}" + } + ), + Err(_err) => rsx!(cx, div { "No doggos for you :(" }), }, ); diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 451a29606..7561d2784 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -27,7 +27,11 @@ static APP: FC<()> = |cx| { cx.render(rsx! { div { - button { onclick: move |_| count += 1, "add" } + button { + onclick: move |_| count += 1, + "Click to add." + "Current count: {count}" + } ul { {(0..*count).map(|f| rsx!{ li { "a - {f}" } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 0d5da1dc4..3957a1f74 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -93,7 +93,7 @@ impl WebsysDom { pub fn process_edits(&mut self, edits: &mut Vec) { for edit in edits.drain(..) { - log::info!("Handling edit: {:#?}", edit); + // log::info!("Handling edit: {:#?}", edit); match edit { DomEdit::PushRoot { id: root } => self.push(root), DomEdit::PopRoot => self.pop(), @@ -530,6 +530,8 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let typ = event.type_(); + log::debug!("Event type is {:?}", typ); + // let attrs = target.attributes(); // for x in 0..attrs.length() { // let attr = attrs.item(x).unwrap(); @@ -569,10 +571,6 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { }) } -pub fn prepare_websys_dom() -> Element { - load_document().get_element_by_id("dioxusroot").unwrap() -} - pub fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index cb4e08d8a..a6588f07a 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -117,10 +117,12 @@ pub async fn run_with_props(root: FC, root_props: T, 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 tasks = dom.get_event_sender(); + let sender_callback = Rc::new(move |event| tasks.unbounded_send(event).unwrap()); let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); @@ -129,8 +131,10 @@ pub async fn run_with_props(root: FC, root_props: T, // 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 - if !hydrating { + if !should_hydrate { 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(); @@ -144,12 +148,14 @@ pub async fn run_with_props(root: FC, root_props: T, let deadline = work_loop.wait_for_idle_time().await; // 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 work_loop.wait_for_raf().await; - // actually apply our changes during the animation frame - websys_dom.process_edits(&mut mutations[0].edits); + for mut edit in mutations { + // actually apply our changes during the animation frame + websys_dom.process_edits(&mut edit.edits); + } } }