diff --git a/examples/focus-order.rs b/examples/focus-order.rs index 94776ac..24f573f 100644 --- a/examples/focus-order.rs +++ b/examples/focus-order.rs @@ -55,12 +55,12 @@ fn main() -> gooey::Result { .and( "Log In" .into_button() - .enabled(valid) .on_click(move |_| { println!("Welcome, {}", username.get()); exit(0); }) .make_with_id(login_tag) + .with_enabled(valid) .into_default() .with_next_focus(cancel_id), ) diff --git a/examples/login.rs b/examples/login.rs index f81dfe5..22a762e 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -36,12 +36,12 @@ fn main() -> gooey::Result { .and( "Log In" .into_button() - .enabled(valid) .on_click(move |_| { println!("Welcome, {}", username.get()); exit(0); }) - .into_default(), + .into_default() + .with_enabled(valid), ) .into_columns(); diff --git a/examples/tic-tac-toe.rs b/examples/tic-tac-toe.rs index d7ded68..8a31892 100644 --- a/examples/tic-tac-toe.rs +++ b/examples/tic-tac-toe.rs @@ -191,9 +191,9 @@ fn square(row: usize, column: usize, game: &Dynamic) -> impl MakeWidg label .clone() .into_button() - .enabled(enabled) .kind(ButtonKind::Outline) .on_click(move |_| game.lock().play(row, column)) + .with_enabled(enabled) .pad() .expand() } diff --git a/src/context.rs b/src/context.rs index 3782beb..b2c2037 100644 --- a/src/context.rs +++ b/src/context.rs @@ -229,11 +229,12 @@ impl<'context, 'window> EventContext<'context, 'window> { focus_changes += 1; self.pending_state.focus = focus.and_then(|mut focus| loop { - if focus - .lock() - .as_widget() - .accept_focus(&mut self.for_other(&focus)) - { + let mut focus_context = self.for_other(&focus); + let accept_focus = focus_context.enabled() + && focus.lock().as_widget().accept_focus(&mut focus_context); + drop(focus_context); + + if accept_focus { break Some(focus); } else if let Some(next_focus) = focus.explicit_focus_target(self.pending_state.focus_is_advancing) @@ -707,6 +708,7 @@ pub struct WidgetContext<'context, 'window> { pending_state: PendingState<'context>, theme_mode: ThemeMode, effective_styles: Styles, + enabled: bool, } impl<'context, 'window> WidgetContext<'context, 'window> { @@ -717,6 +719,10 @@ impl<'context, 'window> WidgetContext<'context, 'window> { window: &'context mut RunningWindow<'window>, theme_mode: ThemeMode, ) -> Self { + let enabled = current_node.enabled(&WindowHandle { + kludgine: window.handle(), + redraw_status: redraw_status.clone(), + }); Self { pending_state: PendingState::Owned(PendingWidgetState { focus: current_node @@ -730,6 +736,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { focus_is_advancing: false, }), effective_styles: current_node.effective_styles(), + enabled, current_node, redraw_status, theme: Cow::Borrowed(theme), @@ -748,6 +755,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pending_state: self.pending_state.borrowed(), theme_mode: self.theme_mode, effective_styles: self.effective_styles.clone(), + enabled: self.enabled, } } @@ -774,6 +782,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { }; WidgetContext { effective_styles, + enabled: current_node.enabled(&self.handle()), current_node, redraw_status: self.redraw_status, window: &mut *self.window, @@ -784,6 +793,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> { }) } + /// Returns true if this widget is enabled. + #[must_use] + pub const fn enabled(&self) -> bool { + self.enabled + } + pub(crate) fn parent(&self) -> Option { self.current_node.parent() } @@ -1005,6 +1020,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } +#[derive(Clone)] pub(crate) struct WindowHandle { kludgine: kludgine::app::WindowHandle, redraw_status: InvalidationStatus, diff --git a/src/tree.rs b/src/tree.rs index 5a25dd5..68f9996 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -6,6 +6,7 @@ use alot::{LotId, Lots}; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{Point, Rect, Size}; +use crate::context::WindowHandle; use crate::styles::{Styles, ThemePair, VisualOrder}; use crate::utils::IgnorePoison; use crate::value::Value; @@ -44,6 +45,7 @@ impl Tree { effective_styles, theme: None, theme_mode: None, + invalidation: 0, }); data.nodes_by_id.insert(id, node_id); if widget.is_default() { @@ -275,6 +277,25 @@ impl Tree { data.widget_from_node(id, self) } + pub(crate) fn is_enabled(&self, mut id: LotId, context: &WindowHandle) -> bool { + let data = self.data.lock().ignore_poison(); + loop { + let Some(node) = data.nodes.get(id) else { + return false; + }; + + if !node.widget.enabled(context) { + return false; + } + + let Some(parent) = node.parent else { break }; + + id = parent; + } + + true + } + pub(crate) fn active_widget(&self) -> Option { self.data.lock().ignore_poison().active } @@ -481,6 +502,7 @@ impl TreeData { let mut node = &mut self.nodes[id]; while node.layout.is_some() { node.layout = None; + node.invalidation += 1; node.last_layout_query = None; let (true, Some(parent)) = (include_hierarchy, node.parent) else { @@ -552,6 +574,7 @@ struct Node { children: Vec, parent: Option, layout: Option>, + invalidation: u64, last_layout_query: Option, associated_styles: Option>, effective_styles: Styles, diff --git a/src/widget.rs b/src/widget.rs index 1cb5c29..c063c23 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -16,7 +16,9 @@ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::Color; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; +use crate::context::{ + AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext, WindowHandle, +}; use crate::styles::{ ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue, NamedComponent, Styles, ThemePair, VisualOrder, @@ -580,6 +582,19 @@ pub trait MakeWidget: Sized { self.make_widget().with_next_focus(next_focus) } + /// Sets this widget to be enabled/disabled based on `enabled` and returns + /// self. + /// + /// If this widget is disabled, all children widgets will also be disabled. + /// + /// # Panics + /// + /// This function can only be called when one instance of the widget exists. + /// If any clones exist, a panic will occur. + fn with_enabled(self, enabled: impl IntoValue) -> WidgetInstance { + self.make_widget().with_enabled(enabled) + } + /// Sets this widget as a "default" widget. /// /// Default widgets are automatically activated when the user signals they @@ -846,6 +861,7 @@ struct WidgetInstanceData { default: bool, cancel: bool, next_focus: Value>, + enabled: Value, widget: Box>, } @@ -863,6 +879,7 @@ impl WidgetInstance { default: false, cancel: false, widget: Box::new(Mutex::new(widget)), + enabled: Value::Constant(true), }), } } @@ -901,6 +918,23 @@ impl WidgetInstance { self } + /// Sets this widget to be enabled/disabled based on `enabled` and returns + /// self. + /// + /// If this widget is disabled, all children widgets will also be disabled. + /// + /// # Panics + /// + /// This function can only be called when one instance of the widget exists. + /// If any clones exist, a panic will occur. + #[must_use] + pub fn with_enabled(mut self, enabled: impl IntoValue) -> WidgetInstance { + let data = Arc::get_mut(&mut self.data) + .expect("with_enabled can only be called on newly created widget instances"); + data.enabled = enabled.into_value(); + self + } + /// Sets this widget as a "default" widget. /// /// Default widgets are automatically activated when the user signals they @@ -982,6 +1016,13 @@ impl WidgetInstance { pub fn is_escape(&self) -> bool { self.data.cancel } + + pub(crate) fn enabled(&self, context: &WindowHandle) -> bool { + if let Value::Dynamic(dynamic) = &self.data.enabled { + dynamic.redraw_when_changed(context.clone()); + } + self.data.enabled.get() + } } impl AsRef for WidgetInstance { @@ -1141,6 +1182,10 @@ impl ManagedWidget { self.tree.active_widget() == Some(self.node_id) } + pub(crate) fn enabled(&self, handle: &WindowHandle) -> bool { + self.tree.is_enabled(self.node_id, handle) + } + /// Returns true if this widget is currently the hovered widget. #[must_use] pub fn hovered(&self) -> bool { diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 6295b81..5737e07 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -26,8 +26,6 @@ pub struct Button { pub content: WidgetRef, /// The callback that is invoked when the button is clicked. pub on_click: Option>, - /// The enabled state of the button. - pub enabled: Value, /// The kind of button to draw. pub kind: Value, buttons_pressed: usize, @@ -130,7 +128,6 @@ impl Button { Self { content: content.widget_ref(), on_click: None, - enabled: Value::Constant(true), cached_state: CacheState { enabled: true, kind: ButtonKind::default(), @@ -161,23 +158,16 @@ impl Button { self } - /// Sets the value to use for the button's enabled status. - #[must_use] - pub fn enabled(mut self, enabled: impl IntoValue) -> Self { - self.enabled = enabled.into_value(); - self - } - - fn invoke_on_click(&mut self) { - if self.enabled.get() { + fn invoke_on_click(&mut self, context: &WidgetContext<'_, '_>) { + if context.enabled() { if let Some(on_click) = self.on_click.as_mut() { on_click.invoke(()); } } } - fn visual_style(&self, context: &WidgetContext<'_, '_>) -> VisualState { - if !self.enabled.get_tracked(context) { + fn visual_style(context: &WidgetContext<'_, '_>) -> VisualState { + if !context.enabled() { VisualState::Disabled } else if context.active() { VisualState::Active @@ -220,7 +210,7 @@ impl Button { fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { let kind = self.kind.get_tracked(context); - let visual_state = self.visual_style(context); + let visual_state = Self::visual_style(context); self.cached_state = CacheState { enabled: !matches!(visual_state, VisualState::Disabled), @@ -341,7 +331,7 @@ impl VisualState { impl Widget for Button { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { #![allow(clippy::similar_names)] - let enabled = self.enabled.get_tracked(context); + let enabled = context.enabled(); // TODO This seems ugly. It needs context, so it can't be moved into the // dynamic system. @@ -403,7 +393,7 @@ impl Widget for Button { } fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { - self.enabled.get() && context.get(&AutoFocusableControls).is_all() + context.get(&AutoFocusableControls).is_all() } fn mouse_down( @@ -455,7 +445,7 @@ impl Widget for Button { { context.focus(); - self.invoke_on_click(); + self.invoke_on_click(context); } } } @@ -503,7 +493,7 @@ impl Widget for Button { let changed = context.activate(); if !changed { // The widget was already active. This is now a repeated keypress - self.invoke_on_click(); + self.invoke_on_click(context); } changed } @@ -538,7 +528,7 @@ impl Widget for Button { // If we have no buttons pressed, the event should fire on activate not // on deactivate. if self.buttons_pressed == 0 { - self.invoke_on_click(); + self.invoke_on_click(context); } self.update_colors(context, true); } diff --git a/src/window.rs b/src/window.rs index 0d7b8bc..073ce33 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1087,6 +1087,7 @@ pub(crate) mod sealed { pub transparent: bool, } + #[derive(Clone)] pub enum WindowCommand { Redraw, // RequestClose,