From bc4ac9ad33dedf59125c3046f8fcf4c367a993ab Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Wed, 29 Oct 2025 09:01:18 -0600 Subject: [PATCH] add subsecond hotreloading --- Cargo.toml | 11 ++ examples/localization/Cargo.toml | 3 + examples/localization/src/main.rs | 34 ++++- examples/widget-gallery/Cargo.toml | 1 + examples/widget-gallery/src/buttons.rs | 36 +----- examples/widget-gallery/src/checkbox.rs | 1 - examples/widget-gallery/src/main.rs | 2 +- examples/widget-gallery/src/radio_buttons.rs | 13 +- reactive/Cargo.toml | 4 + reactive/src/effect.rs | 124 +++++++++++++++++++ reactive/src/id.rs | 17 ++- reactive/src/lib.rs | 6 + reactive/src/runtime.rs | 38 ++++++ src/app.rs | 37 ++++++ src/ext_event.rs | 40 ++++++ src/inspector/view.rs | 2 +- src/theme.rs | 4 +- src/views/decorator.rs | 11 +- src/views/radio_button.rs | 1 + src/views/toggle_button.rs | 4 +- 20 files changed, 338 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f5f104da..4b5d1a8e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,8 @@ ui-events = {version = "0.2", features = ["kurbo"]} ui-events-winit = {path = "./ui-events-winit", package = "ui-events-floem-winit"} dpi = { version = "0.1.2", default-features = false } +dioxus-devtools = "=0.7.0-rc.3" + [dependencies] slotmap = "1.0" sha2 = "0.10" @@ -94,6 +96,11 @@ fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = {version = "0.3.2", optional = true } +#hotpatching +tungstenite = { version = "0.26.2", optional = true } +serde_json = { version = "1.0.140", optional = true } +dioxus-devtools = { workspace = true, optional = true} + [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } @@ -121,6 +128,7 @@ objc2-app-kit = { version = "0.3", features = [ default = ["editor", "default-image-formats", "vger"] vello = ["dep:floem_vello_renderer"] vger = ["dep:floem_vger_renderer"] +hotpatch = ["dep:tungstenite", "dep:serde_json", "dep:dioxus-devtools", "floem_reactive/hotpatch"] serde = [ "dep:serde", "winit/serde", @@ -159,3 +167,6 @@ rfd-async-std = ["dep:rfd", "rfd/async-std"] rfd-tokio = ["dep:rfd", "rfd/tokio"] crossbeam = ["dep:crossbeam", "floem_renderer/crossbeam"] localization = ["dep:fluent-bundle", "dep:unic-langid", "dep:sys-locale"] + + + diff --git a/examples/localization/Cargo.toml b/examples/localization/Cargo.toml index 6a669d847..c44db63e4 100644 --- a/examples/localization/Cargo.toml +++ b/examples/localization/Cargo.toml @@ -6,3 +6,6 @@ version.workspace = true [dependencies] floem = { path = "../..", features = ["vello", "localization"] } + +[features] +hotpatch = ["floem/hotpatch"] diff --git a/examples/localization/src/main.rs b/examples/localization/src/main.rs index 524e74e14..b5c4e8bde 100644 --- a/examples/localization/src/main.rs +++ b/examples/localization/src/main.rs @@ -1,4 +1,4 @@ -use floem::{action::inspect, prelude::*}; +use floem::{action::{inspect, set_window_scale}, prelude::*}; use localization::*; fn main() { @@ -55,6 +55,8 @@ fn counter_view() -> impl IntoView { )) .style(|s| s.size_full().items_center().justify_center().gap(10.)); + let mut window_scale = RwSignal::new(1.); + (lang_tabs, value_controls) .v_stack() .style(move |s| { @@ -75,4 +77,34 @@ fn counter_view() -> impl IntoView { inspect(); }, ) + .on_key_down( + floem::keyboard::Key::Character("=".into()), + |m| m.meta(), + move |_| { + set_window_scale({ + window_scale.update(|s| *s *= 1.1); + window_scale.get_untracked() + }); + }, + ) + .on_key_down( + floem::keyboard::Key::Character("-".into()), + |m| m.meta(), + move |_| { + set_window_scale({ + window_scale.update(|s| *s /= 1.1); + window_scale.get_untracked() + }); + }, + ) + .on_key_down( + floem::keyboard::Key::Character("0".into()), + |m| m.meta(), + move |_| { + set_window_scale({ + window_scale.update(|s| *s = 1.); + window_scale.get_untracked() + }); + }, + ) } diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 3013b5693..064a57dee 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -13,3 +13,4 @@ stacks = { path = "../stacks/", optional = true } default = ["full"] vello = ["floem/vello"] full = ["dep:files", "dep:stacks"] +hotpatch = ["floem/hotpatch"] diff --git a/examples/widget-gallery/src/buttons.rs b/examples/widget-gallery/src/buttons.rs index 150c6e889..950658025 100644 --- a/examples/widget-gallery/src/buttons.rs +++ b/examples/widget-gallery/src/buttons.rs @@ -1,19 +1,13 @@ use floem::{ peniko::{color::palette, Color}, - prelude::{ - palette::css::{DARK_GRAY, WHITE_SMOKE}, - RwSignal, SignalGet, - }, style::CursorStyle, - theme::StyleThemeExt, - views::{button, toggle_button, Decorators, ToggleButton, ToggleHandleBehavior}, + views::{button, toggle_button, Decorators, ToggleHandleBehavior}, IntoView, }; use crate::form::{form, form_item}; pub fn button_view() -> impl IntoView { - let state = RwSignal::new(false); form(( form_item( "Basic Button:", @@ -27,8 +21,8 @@ pub fn button_view() -> impl IntoView { s.border(1.0) .border_radius(10.0) .padding(10.0) - .background(palette::css::YELLOW_GREEN) - .color(palette::css::DARK_GREEN) + .background(palette::css::RED) + .color(palette::css::BLACK.with_alpha(0.5)) .cursor(CursorStyle::Pointer) .active(|s| s.color(palette::css::WHITE).background(palette::css::RED)) .hover(|s| s.background(Color::from_rgb8(244, 67, 54))) @@ -48,34 +42,12 @@ pub fn button_view() -> impl IntoView { }), ), form_item( - "Toggle button - Snap:", - toggle_button(|| true) - .on_toggle(|_| { - println!("Button Toggled"); - }) - .toggle_style(|s| s.behavior(ToggleHandleBehavior::Snap)), - ), - form_item( - "Toggle button - Follow:", + "Toggle button", toggle_button(|| true) .on_toggle(|_| { println!("Button Toggled"); }) .toggle_style(|s| s.behavior(ToggleHandleBehavior::Follow)), ), - form_item( - "Toggle button - toggle background:", - ToggleButton::new_rw(state).toggle_style(move |s| { - s.apply_if(state.get(), |s| { - s.accent_color(DARK_GRAY).handle_color(WHITE_SMOKE) - }) - .behavior(ToggleHandleBehavior::Snap) - }), - ), )) - .style(move |s| { - s.apply_if(state.get(), |s| { - s.with_theme(|s, t| s.background(t.bg_elevated())) - }) - }) } diff --git a/examples/widget-gallery/src/checkbox.rs b/examples/widget-gallery/src/checkbox.rs index 2878a0062..c7a00c846 100644 --- a/examples/widget-gallery/src/checkbox.rs +++ b/examples/widget-gallery/src/checkbox.rs @@ -17,7 +17,6 @@ pub const CROSS_SVG: &str = r##" "##; pub fn checkbox_view() -> impl IntoView { - // let width = 160.0; let is_checked = RwSignal::new(true); form(( form_item("Checkbox:", Checkbox::new_rw(is_checked)), diff --git a/examples/widget-gallery/src/main.rs b/examples/widget-gallery/src/main.rs index 5596e278b..13629b7bf 100644 --- a/examples/widget-gallery/src/main.rs +++ b/examples/widget-gallery/src/main.rs @@ -19,7 +19,7 @@ pub mod tabs; pub mod texteditor; use floem::{ - action::{add_overlay, set_window_menu, toggle_theme}, + action::{add_overlay, set_window_menu, set_window_scale, toggle_theme}, event::{Event, EventListener}, kurbo::Size, menu::*, diff --git a/examples/widget-gallery/src/radio_buttons.rs b/examples/widget-gallery/src/radio_buttons.rs index 2d991bf54..45a5b7c56 100644 --- a/examples/widget-gallery/src/radio_buttons.rs +++ b/examples/widget-gallery/src/radio_buttons.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use floem::{prelude::*, style_class}; +use floem::prelude::*; use strum::IntoEnumIterator; use crate::form::{form, form_item}; @@ -22,10 +22,7 @@ impl Display for OperatingSystem { } } -style_class!(RadioButtonGroupClass); - pub fn radio_buttons_view() -> impl IntoView { - // let width = 160.0; let operating_system = RwSignal::new(OperatingSystem::Windows); form(( form_item( @@ -48,8 +45,7 @@ pub fn radio_buttons_view() -> impl IntoView { "Labelled Radio Buttons:", OperatingSystem::iter() .map(move |os| RadioButton::new_labeled_rw(os, operating_system, move || os)) - .v_stack() - .class(RadioButtonGroupClass), + .v_stack(), ), form_item( "Disabled Labelled Radio Buttons:", @@ -58,9 +54,8 @@ pub fn radio_buttons_view() -> impl IntoView { RadioButton::new_labeled_get(os, operating_system, move || os) .style(|s| s.set_disabled(true)) }) - .v_stack() - .class(RadioButtonGroupClass), + .v_stack(), ), )) - .style(|s| s.class(RadioButtonGroupClass, |s| s.gap(10.).margin_left(5.))) + .style(|s| s.class(RadioButtonGroupClass, |s| s.gap(10.))) } diff --git a/reactive/Cargo.toml b/reactive/Cargo.toml index 913aeebf1..544536211 100644 --- a/reactive/Cargo.toml +++ b/reactive/Cargo.toml @@ -8,3 +8,7 @@ license.workspace = true [dependencies] smallvec = "1.10.0" +dioxus-devtools = {workspace = true, optional = true} + +[features] +hotpatch = ["dep:dioxus-devtools"] diff --git a/reactive/src/effect.rs b/reactive/src/effect.rs index 96971a284..d149cc77e 100644 --- a/reactive/src/effect.rs +++ b/reactive/src/effect.rs @@ -12,6 +12,8 @@ pub(crate) trait EffectTrait { fn run(&self) -> bool; fn add_observer(&self, id: Id); fn clear_observers(&self) -> HashSet; + #[allow(dead_code)] + fn hot_fn_ptr(&self) -> u64; } struct Effect @@ -249,6 +251,11 @@ where fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + + fn hot_fn_ptr(&self) -> u64 { + std::ptr::addr_of!(self.f) as *const () as u64 + // HotFn::current(&self.f).ptr_address().0 + } } impl EffectTrait for UpdaterEffect @@ -279,6 +286,10 @@ where fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + fn hot_fn_ptr(&self) -> u64 { + std::ptr::addr_of!(self.compute) as *const () as u64 + // HotFn::current(&self.compute).ptr_address().0 + } } pub struct SignalTracker { @@ -357,4 +368,117 @@ impl EffectTrait for TrackingEffect { fn clear_observers(&self) -> HashSet { mem::take(&mut *self.observers.borrow_mut()) } + fn hot_fn_ptr(&self) -> u64 { + let rc_ptr = Rc::as_ptr(&self.on_change); + rc_ptr as *const () as u64 + // HotFn::current(&*self.on_change).ptr_address().0 + } +} +#[cfg(feature = "hotpatch")] +pub use hotpatch::*; + +#[cfg(feature = "hotpatch")] +mod hotpatch { + use std::{cell::RefCell, collections::HashSet, marker::PhantomData, mem, rc::Rc}; + + use dioxus_devtools::subsecond::{HotFn, HotFunction}; + + use super::*; + + struct HotUpdaterEffect + where + C: dioxus_devtools::subsecond::HotFunction, + U: Fn(R), + { + id: Id, + compute: RefCell>, + on_change: U, + observers: RefCell>, + phantom: PhantomData, + } + impl Drop for HotUpdaterEffect + where + C: dioxus_devtools::subsecond::HotFunction, + U: Fn(R), + { + fn drop(&mut self) { + self.id.dispose(); + } + } + /// Create an effect updater that runs `on_change` when any signals that subscribe during the + /// run of `compute` are updated. `compute` is immediately run only once, and its value is returned + /// from the call to `create_updater`. + pub fn create_hot_updater( + compute: HotFn, + on_change: impl Fn(R) + 'static, + ) -> R + where + R: 'static, + C: dioxus_devtools::subsecond::HotFunction + 'static, + T: std::default::Default + 'static, // C: HotFunction<(), M, Return = R> + 'static, + { + let id = Id::next(); + let effect = Rc::new(HotUpdaterEffect { + id, + compute: RefCell::new(compute), + on_change, + observers: RefCell::new(HashSet::default()), + phantom: PhantomData, + }); + crate::runtime::register_effect(effect.clone()); + id.set_scope(); + run_initial_hot_updater_effect(effect) + } + impl EffectTrait for HotUpdaterEffect + where + R: 'static, + C: HotFunction, + U: Fn(R), + { + fn id(&self) -> Id { + self.id + } + + fn run(&self) -> bool { + let compute_fn = &self.compute; + let result = compute_fn.borrow_mut().call(T::default()); + (self.on_change)(result); + true + } + + fn add_observer(&self, id: Id) { + self.observers.borrow_mut().insert(id); + } + + fn clear_observers(&self) -> HashSet { + mem::take(&mut *self.observers.borrow_mut()) + } + + fn hot_fn_ptr(&self) -> u64 { + let compute_fn = &self.compute; + compute_fn.borrow().ptr_address().0 + } + } + fn run_initial_hot_updater_effect( + effect: Rc>, + ) -> R + where + R: 'static, + C: HotFunction + 'static, + U: Fn(R) + 'static, + { + let effect_id = effect.id(); + let result = RUNTIME.with(|runtime| { + *runtime.current_effect.borrow_mut() = Some(effect.clone()); + let effect_scope = Scope(effect_id, PhantomData); + let result = with_scope(effect_scope, || { + effect_scope.track(); + let compute_fn = &effect.compute; + compute_fn.borrow_mut().call(T::default()) + }); + *runtime.current_effect.borrow_mut() = None; + result + }); + result + } } diff --git a/reactive/src/id.rs b/reactive/src/id.rs index 36043dea9..6ee0ea39c 100644 --- a/reactive/src/id.rs +++ b/reactive/src/id.rs @@ -8,7 +8,7 @@ pub struct Id(u64); impl Id { /// Create a new Id that's next in order - pub(crate) fn next() -> Id { + pub fn next() -> Id { static COUNTER: AtomicU64 = AtomicU64::new(0); Id(COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) } @@ -24,7 +24,7 @@ impl Id { } /// Make this Id a child of the current Scope - pub(crate) fn set_scope(&self) { + pub fn set_scope(&self) { RUNTIME.with(|runtime| { let scope = runtime.current_scope.borrow(); let mut children = runtime.children.borrow_mut(); @@ -51,6 +51,19 @@ impl Id { if let Some(signal) = signal { for (_, effect) in signal.subscribers() { observer_clean_up(&effect); + + #[cfg(feature = "hotpatch")] + { + // Remove from hotpatch registry when disposing + RUNTIME.with(|runtime| { + if let Ok(mut hot_patched_effects) = + runtime.hot_patched_effects.try_borrow_mut() + { + let fn_ptr = effect.hot_fn_ptr(); + hot_patched_effects.remove(&fn_ptr); + } + }); + } } } } diff --git a/reactive/src/lib.rs b/reactive/src/lib.rs index c15b6a935..1752fdab3 100644 --- a/reactive/src/lib.rs +++ b/reactive/src/lib.rs @@ -25,6 +25,7 @@ pub use effect::{ batch, create_effect, create_stateful_updater, create_tracker, create_updater, untrack, SignalTracker, }; + pub use id::Id as ReactiveId; pub use memo::{create_memo, Memo}; pub use read::{ReadSignalValue, SignalGet, SignalRead, SignalTrack, SignalWith}; @@ -32,3 +33,8 @@ 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}; pub use write::{SignalUpdate, SignalWrite, WriteSignalValue}; + +#[cfg(feature = "hotpatch")] +pub use effect::create_hot_updater; +#[cfg(feature = "hotpatch")] +pub use runtime::hotpatch; diff --git a/reactive/src/runtime.rs b/reactive/src/runtime.rs index e9cebe5e5..326c6f7ba 100644 --- a/reactive/src/runtime.rs +++ b/reactive/src/runtime.rs @@ -27,6 +27,8 @@ pub(crate) struct Runtime { pub(crate) contexts: RefCell>>, pub(crate) batching: Cell, pub(crate) pending_effects: RefCell; 10]>>, + #[cfg(feature = "hotpatch")] + pub(crate) hot_patched_effects: RefCell>>, } impl Default for Runtime { @@ -45,6 +47,8 @@ impl Runtime { contexts: Default::default(), batching: Cell::new(false), pending_effects: RefCell::new(SmallVec::new()), + #[cfg(feature = "hotpatch")] + hot_patched_effects: RefCell::new(HashMap::new()), } } @@ -66,3 +70,37 @@ impl Runtime { } } } + +#[cfg(feature = "hotpatch")] +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 current_ptr = effect.hot_fn_ptr(); + hot_patched_effects.insert(current_ptr, effect.clone()); + }); +} + +#[cfg(feature = "hotpatch")] +pub fn hotpatch() { + RUNTIME.with(|runtime| { + let Ok(mut hot_patched_effects) = runtime.hot_patched_effects.try_borrow_mut() else { + return; + }; + + let updates: Vec<_> = hot_patched_effects + .iter() + .filter_map(|(old_ptr, effect)| { + let new_ptr = effect.hot_fn_ptr(); + (*old_ptr != new_ptr).then_some((*old_ptr, new_ptr, effect.clone())) + }) + .collect(); + + for (old_ptr, new_ptr, effect) in updates { + effect.run(); + hot_patched_effects.remove(&old_ptr); + hot_patched_effects.insert(new_ptr, effect); + } + }); +} diff --git a/src/app.rs b/src/app.rs index d7d37b325..754da0def 100644 --- a/src/app.rs +++ b/src/app.rs @@ -195,6 +195,9 @@ impl Application { pub fn new_with_config(config: AppConfig) -> Self { let event_loop = EventLoop::new().expect("can't start the event loop"); + #[cfg(feature = "hotpatch")] + initialize_hot_reload(); + #[cfg(target_os = "macos")] crate::app_delegate::set_app_delegate(); @@ -267,6 +270,40 @@ pub fn quit_app() { Application::send_proxy_event(UserEvent::QuitApp); } +#[cfg(feature = "hotpatch")] +pub fn initialize_hot_reload() { + use crate::ext_event::create_nonblocking_signal_from_channel; + use crate::reactive::{SignalTrack, create_updater}; + use dioxus_devtools::{DevserverMsg, connect, subsecond::apply_patch}; + static INIT: std::sync::Once = std::sync::Once::new(); + 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.clone()) { + eprintln!("Hot patch application failed: {:?}", e); + return; + } + } + println!("Hot patch applied successfully!"); + tx.send(()).unwrap(); + } + } + }); + + create_updater( + move || update.track(), + move |_| { + floem_reactive::hotpatch(); + }, + ); + }); +} + /// Signals the application to reopen. /// /// This function sends a `Reopen` event to the application's event loop. diff --git a/src/ext_event.rs b/src/ext_event.rs index 20683e0a6..6b92c61b8 100644 --- a/src/ext_event.rs +++ b/src/ext_event.rs @@ -166,6 +166,46 @@ pub fn update_signal_from_channel( }); } +/// 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 +} + #[derive(Clone)] pub struct ArcRwSignal { inner: Arc>, diff --git a/src/inspector/view.rs b/src/inspector/view.rs index 122021883..b812afce2 100644 --- a/src/inspector/view.rs +++ b/src/inspector/view.rs @@ -352,7 +352,7 @@ fn capture_view( .into_any(), _ => panic!(), } - .style(|s| s.width_full()) + .style(|s| s.width_full().padding_bottom(50)) .scroll() .scroll_style(|s| s.handle_thickness(6.).shrink_to_fit()) .style(|s| { diff --git a/src/theme.rs b/src/theme.rs index b11420106..cc457b86e 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -493,8 +493,8 @@ pub(crate) fn default_theme(os_theme: winit::window::Theme) -> Style { .padding(t.padding()) .set(Foreground, Brush::Solid(t.text_muted())) .active(|s| { - s.background(t.primary()) - .color(t.bg_base()) + s.color(t.bg_base()) + .background(t.bg_overlay()) .set(Foreground, Brush::Solid(t.bg_base())) }) .hover(|s| s.background(t.bg_overlay())) diff --git a/src/views/decorator.rs b/src/views/decorator.rs index 25812b661..be9884abf 100644 --- a/src/views/decorator.rs +++ b/src/views/decorator.rs @@ -49,12 +49,21 @@ pub trait Decorators: IntoView + Sized { let state = view_id.state(); let offset = state.borrow_mut().style.next_offset(); - let style = create_updater( + #[cfg(feature = "hotpatch")] + let style = floem_reactive::create_hot_updater( + dioxus_devtools::subsecond::HotFn::current(style), + move |style| { + view_id.update_style(offset, style); + }, + ); + #[cfg(not(feature = "hotpatch"))] + let style = floem_reactive::create_updater( move || style(Style::new()), move |style| { view_id.update_style(offset, style); }, ); + state.borrow_mut().style.push(style); view diff --git a/src/views/radio_button.rs b/src/views/radio_button.rs index a90f2d0dd..54a2072db 100644 --- a/src/views/radio_button.rs +++ b/src/views/radio_button.rs @@ -13,6 +13,7 @@ style_class!(pub RadioButtonClass); style_class!(pub RadioButtonDotClass); style_class!(pub RadioButtonDotSelectedClass); style_class!(pub LabeledRadioButtonClass); +style_class!(pub RadioButtonGroupClass); fn radio_button_svg(represented_value: T, actual_value: impl SignalGet + 'static) -> impl View where diff --git a/src/views/toggle_button.rs b/src/views/toggle_button.rs index 93dd7831e..e067fe2c1 100644 --- a/src/views/toggle_button.rs +++ b/src/views/toggle_button.rs @@ -220,8 +220,10 @@ impl View for ToggleButton { .. }) => { if *key == Key::Named(NamedKey::Enter) { + self.state = !self.state; + self.id.request_layout(); if let Some(ontoggle) = &self.ontoggle { - ontoggle(!self.state); + ontoggle(self.state); } } }