diff --git a/Cargo.toml b/Cargo.toml index cd2b460ff..a9a3842f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ parking_lot = { version = "0.12.1" } swash = { version = "0.1.17" } muda = { git = "https://github.com/tauri-apps/muda", rev = "8e986af3cea96a729413abc75c3702dec3990bd2", default-features = false } winit = { git = "https://github.com/rust-windowing/winit", rev = "ee245c569d65fdeacf705ee5eedb564508d10ebe" } +dioxus-devtools = "=0.7.0-alpha.0" [dependencies] slotmap = "1.0.7" @@ -81,6 +82,9 @@ im = { workspace = true } winit = { workspace = true } futures = { version = "0.3.30", optional = true } crossbeam = { version = "0.8", optional = true } +tungstenite = "0.26.2" +serde_json = "1.0.140" +dioxus-devtools = {workspace = true} [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } @@ -148,3 +152,21 @@ rfd-tokio = ["dep:rfd", "rfd/tokio"] futures = ["dep:futures"] crossbeam = [ "dep:crossbeam", "floem_renderer/crossbeam" ] + +[patch.crates-io] +vello = { path = "../vello/vello" } +vello_svg = {path = "../vello_svg"} +dioxus-devtools = {path = "../dioxus/packages/devtools"} + + +[profile] + +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 + +[profile.server-dev] +inherits = "dev" + +[profile.android-dev] +inherits = "dev" diff --git a/examples/widget-gallery/src/checkbox.rs b/examples/widget-gallery/src/checkbox.rs index 9e457a1f9..d735af20f 100644 --- a/examples/widget-gallery/src/checkbox.rs +++ b/examples/widget-gallery/src/checkbox.rs @@ -23,8 +23,10 @@ pub fn checkbox_view() -> impl IntoView { form_item("Checkbox:", Checkbox::new_rw(is_checked)), form_item( "Custom Checkbox 1:", - Checkbox::new_rw_custom(is_checked, CUSTOM_CHECK_SVG) - .style(|s| s.color(palette::css::GREEN)), + Checkbox::new_rw_custom(is_checked, CUSTOM_CHECK_SVG).style(|s| { + dbg!("again"); + s.color(palette::css::GREEN) + }), ), form_item( "Disabled Checkbox:", diff --git a/reactive/Cargo.toml b/reactive/Cargo.toml index 913aeebf1..62826fb54 100644 --- a/reactive/Cargo.toml +++ b/reactive/Cargo.toml @@ -8,3 +8,4 @@ license.workspace = true [dependencies] smallvec = "1.10.0" +dioxus-devtools.workspace = true diff --git a/reactive/src/effect.rs b/reactive/src/effect.rs index a9dbd902e..5bce4540c 100644 --- a/reactive/src/effect.rs +++ b/reactive/src/effect.rs @@ -1,8 +1,11 @@ -use std::{any::Any, cell::RefCell, collections::HashSet, marker::PhantomData, mem, rc::Rc}; +use std::{ + any::Any, cell::RefCell, collections::HashSet, marker::PhantomData, mem, rc::Rc, + sync::atomic::AtomicU64, +}; use crate::{ id::Id, - runtime::RUNTIME, + runtime::{register_effect, RUNTIME}, scope::{with_scope, Scope}, signal::NotThreadSafe, }; @@ -12,6 +15,8 @@ pub(crate) trait EffectTrait { fn run(&self) -> bool; fn add_observer(&self, id: Id); fn clear_observers(&self) -> HashSet; + fn hot_fn_ptr(&self) -> u64; + fn name(&self) -> &'static str; } struct Effect @@ -55,6 +60,7 @@ where observers: RefCell::new(HashSet::default()), ts: PhantomData, }); + register_effect(effect.clone()); id.set_scope(); run_initial_effect(effect); @@ -109,6 +115,7 @@ where value: RefCell::new(None), observers: RefCell::new(HashSet::default()), }); + register_effect(effect.clone()); id.set_scope(); run_initial_updater_effect(effect) @@ -195,10 +202,13 @@ where let effect_scope = Scope(effect_id, PhantomData); let (result, new_value) = with_scope(effect_scope, || { effect_scope.track(); - (effect.compute)(None) + // Wrap the compute function in HotFn for hot-reloading + let compute_fn = &effect.compute; + let mut hot_fn = dioxus_devtools::subsecond::HotFn::current(|| compute_fn(None)); + hot_fn.call(()) }); - // set new value + // Set new value *effect.value.borrow_mut() = Some(new_value); *runtime.current_effect.borrow_mut() = None; @@ -227,18 +237,21 @@ where T: 'static, F: Fn(Option) -> T, { + fn name(&self) -> &'static str { + "Effect" + } + fn id(&self) -> Id { self.id } fn run(&self) -> bool { + // Wrap the effect's function in HotFn for hot-reloading + let compute_fn = &self.f; + let mut hot_fn = dioxus_devtools::subsecond::HotFn::current(compute_fn); let curr_value = self.value.borrow_mut().take(); - - // run the effect - let new_value = (self.f)(curr_value); - + let new_value = hot_fn.call((curr_value,)); // Execute the hot-reloadable function *self.value.borrow_mut() = Some(new_value); - true } @@ -249,6 +262,12 @@ where fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + + fn hot_fn_ptr(&self) -> u64 { + let compute_fn = &self.f; + let hot_fn = dioxus_devtools::subsecond::HotFn::current(compute_fn); + hot_fn.ptr_address() + } } impl EffectTrait for UpdaterEffect @@ -257,17 +276,21 @@ where C: Fn(Option) -> (I, T), U: Fn(I, T) -> T, { + fn name(&self) -> &'static str { + "UpdaterEffect" + } + fn id(&self) -> Id { self.id } fn run(&self) -> bool { + // Wrap the compute function in HotFn for hot-reloading + let compute_fn = &self.compute; + let mut hot_fn = dioxus_devtools::subsecond::HotFn::current(compute_fn); let curr_value = self.value.borrow_mut().take(); - - // run the effect - let (i, t) = (self.compute)(curr_value); - let new_value = (self.on_change)(i, t); - + let (i, t) = hot_fn.call((curr_value,)); // Execute the hot-reloadable function + let new_value = (self.on_change)(i, t); // Apply the update *self.value.borrow_mut() = Some(new_value); true } @@ -279,6 +302,14 @@ where fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + + fn hot_fn_ptr(&self) -> u64 { + let compute_fn = &self.compute; + let hot_fn = dioxus_devtools::subsecond::HotFn::current(compute_fn); + hot_fn.ptr_address() + // static COUNT: AtomicU64 = AtomicU64::new(0); + // COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } } pub struct SignalTracker { @@ -313,6 +344,7 @@ impl SignalTracker { observers: RefCell::new(HashSet::default()), on_change: self.on_change.clone(), }); + register_effect(tracking_effect.clone()); RUNTIME.with(|runtime| { *runtime.current_effect.borrow_mut() = Some(tracking_effect.clone()); @@ -339,12 +371,19 @@ struct TrackingEffect { } impl EffectTrait for TrackingEffect { + fn name(&self) -> &'static str { + "TrackingEffect" + } + fn id(&self) -> Id { self.id } fn run(&self) -> bool { - (self.on_change)(); + // Wrap the on_change function in HotFn for hot-reloading + let on_change_fn = &self.on_change; + let mut hot_fn = dioxus_devtools::subsecond::HotFn::current(|| on_change_fn()); + hot_fn.call(()); true } @@ -355,4 +394,9 @@ impl EffectTrait for TrackingEffect { fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + + fn hot_fn_ptr(&self) -> u64 { + let on_change_fn = &self.on_change; + dioxus_devtools::subsecond::HotFn::current(|| on_change_fn()).ptr_address() + } } diff --git a/reactive/src/lib.rs b/reactive/src/lib.rs index ff969d23d..457386355 100644 --- a/reactive/src/lib.rs +++ b/reactive/src/lib.rs @@ -27,6 +27,7 @@ pub use effect::{ }; pub use memo::{create_memo, Memo}; pub use read::{ReadSignalValue, SignalGet, SignalRead, SignalTrack, SignalWith}; +pub use runtime::hotpatch; pub use scope::{as_child_of_current_scope, with_scope, Scope}; pub use signal::{create_rw_signal, create_signal, ReadSignal, RwSignal, WriteSignal}; pub use trigger::{create_trigger, Trigger}; diff --git a/reactive/src/runtime.rs b/reactive/src/runtime.rs index e9cebe5e5..4c7b3dceb 100644 --- a/reactive/src/runtime.rs +++ b/reactive/src/runtime.rs @@ -27,6 +27,7 @@ pub(crate) struct Runtime { pub(crate) contexts: RefCell>>, pub(crate) batching: Cell, pub(crate) pending_effects: RefCell; 10]>>, + pub(crate) hot_patched_effects: RefCell>, } impl Default for Runtime { @@ -45,6 +46,7 @@ impl Runtime { contexts: Default::default(), batching: Cell::new(false), pending_effects: RefCell::new(SmallVec::new()), + hot_patched_effects: RefCell::new(HashMap::new()), } } @@ -66,3 +68,46 @@ impl Runtime { } } } + +pub(crate) fn register_effect(effect: Rc) { + RUNTIME.with(|runtime| { + let Ok(mut hot_patched_effects) = runtime.hot_patched_effects.try_borrow_mut() else { + return; + }; + let effect_id = effect.id(); + let current_ptr = effect.hot_fn_ptr(); + hot_patched_effects.insert(effect_id, current_ptr); + }); +} + +pub fn hotpatch() { + RUNTIME.with(|runtime| { + let Ok(mut hot_patched_effects) = runtime.hot_patched_effects.try_borrow_mut() else { + return; + }; + println!("Starting hotpatch"); + let mut effects = HashMap::new(); + for signal in runtime.signals.borrow().values() { + for effect in signal.subscribers.borrow().values() { + if Some(effect.id()) != runtime.current_effect.borrow().as_ref().map(|v| v.id()) { + effects.insert(effect.id(), effect.clone()); + } + } + } + for effect in effects.values() { + let effect_id = effect.id(); + let new_ptr = effect.hot_fn_ptr(); + if let Some(current_ptr) = hot_patched_effects.get(&effect_id) { + if new_ptr != *current_ptr { + dbg!("different pointer"); + hot_patched_effects.insert(effect_id, new_ptr); + effect.run(); + } else { + } + } else { + hot_patched_effects.insert(effect_id, new_ptr); + } + } + println!("Finished hotpatch"); + }); +} diff --git a/src/app.rs b/src/app.rs index 8913ce0b2..98b83f779 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use crossbeam::channel::{unbounded as channel, Receiver, Sender}; #[cfg(not(feature = "crossbeam"))] use std::sync::mpsc::{channel, Receiver, Sender}; -use floem_reactive::WriteSignal; +use floem_reactive::{hotpatch, SignalTrack, WriteSignal}; use parking_lot::Mutex; use raw_window_handle::HasDisplayHandle; use winit::{ @@ -19,6 +19,7 @@ use crate::{ action::{Timer, TimerToken}, app_handle::ApplicationHandle, clipboard::Clipboard, + ext_event::create_nonblocking_signal_from_channel, inspector::Capture, profiler::Profile, view::IntoView, @@ -160,6 +161,7 @@ impl ApplicationHandler for Application { impl Application { pub fn new() -> Self { let event_loop = EventLoop::new().expect("can't start the event loop"); + initialize_hot_reload(); #[cfg(target_os = "macos")] crate::app_delegate::set_app_delegate(); @@ -232,3 +234,37 @@ impl Application { pub fn quit_app() { Application::send_proxy_event(UserEvent::QuitApp); } + +static INIT: std::sync::Once = std::sync::Once::new(); +pub fn initialize_hot_reload() { + use crate::reactive::create_updater; + use dioxus_devtools::{connect, subsecond::apply_patch, DevserverMsg}; + INIT.call_once(|| { + let (tx, rx) = channel::<()>(); + let update = create_nonblocking_signal_from_channel(rx); + + connect(move |msg| { + if let DevserverMsg::HotReload(hot_reload_msg) = msg { + if let Some(jumptable) = hot_reload_msg.jump_table { + unsafe { + if let Err(e) = apply_patch(jumptable) { + eprintln!("Hot patch application failed: {:?}", e); + return; + } + } + println!("Hot patch applied successfully!"); + tx.send(()).unwrap(); + } + } else { + dbg!(msg); + } + }); + + create_updater( + move || update.track(), + move |_| { + hotpatch(); + }, + ); + }); +} diff --git a/src/ext_event.rs b/src/ext_event.rs index b5b548e58..dac7d4ef1 100644 --- a/src/ext_event.rs +++ b/src/ext_event.rs @@ -200,6 +200,46 @@ pub fn create_signal_from_channel(rx: Receiver) -> ReadSig read } +/// this might miss values +pub fn create_nonblocking_signal_from_channel( + rx: Receiver, +) -> ReadSignal> { + let cx = Scope::new(); + let trigger = with_scope(cx, ExtSendTrigger::new); + + let channel_closed = cx.create_rw_signal(false); + let (read, write) = cx.create_signal(None); + let data = Arc::new(Mutex::new(VecDeque::new())); + + { + let data = data.clone(); + cx.create_effect(move |_| { + trigger.track(); + while let Some(Some(value)) = data.try_lock().map(|mut i| i.pop_front()) { + write.set(value); + } + + if channel_closed.get() { + cx.dispose(); + } + }); + } + + let send = create_ext_action(cx, move |_| { + channel_closed.set(true); + }); + + std::thread::spawn(move || { + while let Ok(event) = rx.recv() { + data.lock().push_back(Some(event)); + EXT_EVENT_HANDLER.add_trigger(trigger); + } + send(()); + }); + + read +} + #[cfg(feature = "tokio")] pub fn create_signal_from_tokio_channel( mut rx: tokio::sync::mpsc::UnboundedReceiver, diff --git a/src/lib.rs b/src/lib.rs index 944f64017..338b1b539 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,6 +222,7 @@ mod window_tracking; pub use app::{launch, quit_app, AppEvent, Application}; pub use app_state::AppState; pub use clipboard::{Clipboard, ClipboardError}; +pub use dioxus_devtools::subsecond::call as subcall; pub use floem_reactive as reactive; pub use floem_renderer::text; pub use floem_renderer::Renderer;