Enable/disable is now handled for all widgets

Closes #66
This commit is contained in:
Jonathan Johnson 2023-11-15 10:23:42 -08:00
parent 5965f19d27
commit 534f676ef0
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
8 changed files with 105 additions and 30 deletions

View file

@ -55,12 +55,12 @@ fn main() -> gooey::Result {
.and( .and(
"Log In" "Log In"
.into_button() .into_button()
.enabled(valid)
.on_click(move |_| { .on_click(move |_| {
println!("Welcome, {}", username.get()); println!("Welcome, {}", username.get());
exit(0); exit(0);
}) })
.make_with_id(login_tag) .make_with_id(login_tag)
.with_enabled(valid)
.into_default() .into_default()
.with_next_focus(cancel_id), .with_next_focus(cancel_id),
) )

View file

@ -36,12 +36,12 @@ fn main() -> gooey::Result {
.and( .and(
"Log In" "Log In"
.into_button() .into_button()
.enabled(valid)
.on_click(move |_| { .on_click(move |_| {
println!("Welcome, {}", username.get()); println!("Welcome, {}", username.get());
exit(0); exit(0);
}) })
.into_default(), .into_default()
.with_enabled(valid),
) )
.into_columns(); .into_columns();

View file

@ -191,9 +191,9 @@ fn square(row: usize, column: usize, game: &Dynamic<GameState>) -> impl MakeWidg
label label
.clone() .clone()
.into_button() .into_button()
.enabled(enabled)
.kind(ButtonKind::Outline) .kind(ButtonKind::Outline)
.on_click(move |_| game.lock().play(row, column)) .on_click(move |_| game.lock().play(row, column))
.with_enabled(enabled)
.pad() .pad()
.expand() .expand()
} }

View file

@ -229,11 +229,12 @@ impl<'context, 'window> EventContext<'context, 'window> {
focus_changes += 1; focus_changes += 1;
self.pending_state.focus = focus.and_then(|mut focus| loop { self.pending_state.focus = focus.and_then(|mut focus| loop {
if focus let mut focus_context = self.for_other(&focus);
.lock() let accept_focus = focus_context.enabled()
.as_widget() && focus.lock().as_widget().accept_focus(&mut focus_context);
.accept_focus(&mut self.for_other(&focus)) drop(focus_context);
{
if accept_focus {
break Some(focus); break Some(focus);
} else if let Some(next_focus) = } else if let Some(next_focus) =
focus.explicit_focus_target(self.pending_state.focus_is_advancing) focus.explicit_focus_target(self.pending_state.focus_is_advancing)
@ -707,6 +708,7 @@ pub struct WidgetContext<'context, 'window> {
pending_state: PendingState<'context>, pending_state: PendingState<'context>,
theme_mode: ThemeMode, theme_mode: ThemeMode,
effective_styles: Styles, effective_styles: Styles,
enabled: bool,
} }
impl<'context, 'window> WidgetContext<'context, 'window> { impl<'context, 'window> WidgetContext<'context, 'window> {
@ -717,6 +719,10 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
window: &'context mut RunningWindow<'window>, window: &'context mut RunningWindow<'window>,
theme_mode: ThemeMode, theme_mode: ThemeMode,
) -> Self { ) -> Self {
let enabled = current_node.enabled(&WindowHandle {
kludgine: window.handle(),
redraw_status: redraw_status.clone(),
});
Self { Self {
pending_state: PendingState::Owned(PendingWidgetState { pending_state: PendingState::Owned(PendingWidgetState {
focus: current_node focus: current_node
@ -730,6 +736,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
focus_is_advancing: false, focus_is_advancing: false,
}), }),
effective_styles: current_node.effective_styles(), effective_styles: current_node.effective_styles(),
enabled,
current_node, current_node,
redraw_status, redraw_status,
theme: Cow::Borrowed(theme), theme: Cow::Borrowed(theme),
@ -748,6 +755,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
pending_state: self.pending_state.borrowed(), pending_state: self.pending_state.borrowed(),
theme_mode: self.theme_mode, theme_mode: self.theme_mode,
effective_styles: self.effective_styles.clone(), effective_styles: self.effective_styles.clone(),
enabled: self.enabled,
} }
} }
@ -774,6 +782,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}; };
WidgetContext { WidgetContext {
effective_styles, effective_styles,
enabled: current_node.enabled(&self.handle()),
current_node, current_node,
redraw_status: self.redraw_status, redraw_status: self.redraw_status,
window: &mut *self.window, 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<ManagedWidget> { pub(crate) fn parent(&self) -> Option<ManagedWidget> {
self.current_node.parent() self.current_node.parent()
} }
@ -1005,6 +1020,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
} }
} }
#[derive(Clone)]
pub(crate) struct WindowHandle { pub(crate) struct WindowHandle {
kludgine: kludgine::app::WindowHandle<WindowCommand>, kludgine: kludgine::app::WindowHandle<WindowCommand>,
redraw_status: InvalidationStatus, redraw_status: InvalidationStatus,

View file

@ -6,6 +6,7 @@ use alot::{LotId, Lots};
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{Point, Rect, Size}; use kludgine::figures::{Point, Rect, Size};
use crate::context::WindowHandle;
use crate::styles::{Styles, ThemePair, VisualOrder}; use crate::styles::{Styles, ThemePair, VisualOrder};
use crate::utils::IgnorePoison; use crate::utils::IgnorePoison;
use crate::value::Value; use crate::value::Value;
@ -44,6 +45,7 @@ impl Tree {
effective_styles, effective_styles,
theme: None, theme: None,
theme_mode: None, theme_mode: None,
invalidation: 0,
}); });
data.nodes_by_id.insert(id, node_id); data.nodes_by_id.insert(id, node_id);
if widget.is_default() { if widget.is_default() {
@ -275,6 +277,25 @@ impl Tree {
data.widget_from_node(id, self) 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<LotId> { pub(crate) fn active_widget(&self) -> Option<LotId> {
self.data.lock().ignore_poison().active self.data.lock().ignore_poison().active
} }
@ -481,6 +502,7 @@ impl TreeData {
let mut node = &mut self.nodes[id]; let mut node = &mut self.nodes[id];
while node.layout.is_some() { while node.layout.is_some() {
node.layout = None; node.layout = None;
node.invalidation += 1;
node.last_layout_query = None; node.last_layout_query = None;
let (true, Some(parent)) = (include_hierarchy, node.parent) else { let (true, Some(parent)) = (include_hierarchy, node.parent) else {
@ -552,6 +574,7 @@ struct Node {
children: Vec<LotId>, children: Vec<LotId>,
parent: Option<LotId>, parent: Option<LotId>,
layout: Option<Rect<Px>>, layout: Option<Rect<Px>>,
invalidation: u64,
last_layout_query: Option<CachedLayoutQuery>, last_layout_query: Option<CachedLayoutQuery>,
associated_styles: Option<Value<Styles>>, associated_styles: Option<Value<Styles>>,
effective_styles: Styles, effective_styles: Styles,

View file

@ -16,7 +16,9 @@ use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
use kludgine::Color; use kludgine::Color;
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::context::{
AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext, WindowHandle,
};
use crate::styles::{ use crate::styles::{
ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue, NamedComponent, Styles, ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue, NamedComponent, Styles,
ThemePair, VisualOrder, ThemePair, VisualOrder,
@ -580,6 +582,19 @@ pub trait MakeWidget: Sized {
self.make_widget().with_next_focus(next_focus) 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<bool>) -> WidgetInstance {
self.make_widget().with_enabled(enabled)
}
/// Sets this widget as a "default" widget. /// Sets this widget as a "default" widget.
/// ///
/// Default widgets are automatically activated when the user signals they /// Default widgets are automatically activated when the user signals they
@ -846,6 +861,7 @@ struct WidgetInstanceData {
default: bool, default: bool,
cancel: bool, cancel: bool,
next_focus: Value<Option<WidgetId>>, next_focus: Value<Option<WidgetId>>,
enabled: Value<bool>,
widget: Box<Mutex<dyn AnyWidget>>, widget: Box<Mutex<dyn AnyWidget>>,
} }
@ -863,6 +879,7 @@ impl WidgetInstance {
default: false, default: false,
cancel: false, cancel: false,
widget: Box::new(Mutex::new(widget)), widget: Box::new(Mutex::new(widget)),
enabled: Value::Constant(true),
}), }),
} }
} }
@ -901,6 +918,23 @@ impl WidgetInstance {
self 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<bool>) -> 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. /// Sets this widget as a "default" widget.
/// ///
/// Default widgets are automatically activated when the user signals they /// Default widgets are automatically activated when the user signals they
@ -982,6 +1016,13 @@ impl WidgetInstance {
pub fn is_escape(&self) -> bool { pub fn is_escape(&self) -> bool {
self.data.cancel 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<WidgetId> for WidgetInstance { impl AsRef<WidgetId> for WidgetInstance {
@ -1141,6 +1182,10 @@ impl ManagedWidget {
self.tree.active_widget() == Some(self.node_id) 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. /// Returns true if this widget is currently the hovered widget.
#[must_use] #[must_use]
pub fn hovered(&self) -> bool { pub fn hovered(&self) -> bool {

View file

@ -26,8 +26,6 @@ pub struct Button {
pub content: WidgetRef, pub content: WidgetRef,
/// The callback that is invoked when the button is clicked. /// The callback that is invoked when the button is clicked.
pub on_click: Option<Callback<()>>, pub on_click: Option<Callback<()>>,
/// The enabled state of the button.
pub enabled: Value<bool>,
/// The kind of button to draw. /// The kind of button to draw.
pub kind: Value<ButtonKind>, pub kind: Value<ButtonKind>,
buttons_pressed: usize, buttons_pressed: usize,
@ -130,7 +128,6 @@ impl Button {
Self { Self {
content: content.widget_ref(), content: content.widget_ref(),
on_click: None, on_click: None,
enabled: Value::Constant(true),
cached_state: CacheState { cached_state: CacheState {
enabled: true, enabled: true,
kind: ButtonKind::default(), kind: ButtonKind::default(),
@ -161,23 +158,16 @@ impl Button {
self self
} }
/// Sets the value to use for the button's enabled status. fn invoke_on_click(&mut self, context: &WidgetContext<'_, '_>) {
#[must_use] if context.enabled() {
pub fn enabled(mut self, enabled: impl IntoValue<bool>) -> Self {
self.enabled = enabled.into_value();
self
}
fn invoke_on_click(&mut self) {
if self.enabled.get() {
if let Some(on_click) = self.on_click.as_mut() { if let Some(on_click) = self.on_click.as_mut() {
on_click.invoke(()); on_click.invoke(());
} }
} }
} }
fn visual_style(&self, context: &WidgetContext<'_, '_>) -> VisualState { fn visual_style(context: &WidgetContext<'_, '_>) -> VisualState {
if !self.enabled.get_tracked(context) { if !context.enabled() {
VisualState::Disabled VisualState::Disabled
} else if context.active() { } else if context.active() {
VisualState::Active VisualState::Active
@ -220,7 +210,7 @@ impl Button {
fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors {
let kind = self.kind.get_tracked(context); 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 { self.cached_state = CacheState {
enabled: !matches!(visual_state, VisualState::Disabled), enabled: !matches!(visual_state, VisualState::Disabled),
@ -341,7 +331,7 @@ impl VisualState {
impl Widget for Button { impl Widget for Button {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
#![allow(clippy::similar_names)] #![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 // TODO This seems ugly. It needs context, so it can't be moved into the
// dynamic system. // dynamic system.
@ -403,7 +393,7 @@ impl Widget for Button {
} }
fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { 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( fn mouse_down(
@ -455,7 +445,7 @@ impl Widget for Button {
{ {
context.focus(); context.focus();
self.invoke_on_click(); self.invoke_on_click(context);
} }
} }
} }
@ -503,7 +493,7 @@ impl Widget for Button {
let changed = context.activate(); let changed = context.activate();
if !changed { if !changed {
// The widget was already active. This is now a repeated keypress // The widget was already active. This is now a repeated keypress
self.invoke_on_click(); self.invoke_on_click(context);
} }
changed changed
} }
@ -538,7 +528,7 @@ impl Widget for Button {
// If we have no buttons pressed, the event should fire on activate not // If we have no buttons pressed, the event should fire on activate not
// on deactivate. // on deactivate.
if self.buttons_pressed == 0 { if self.buttons_pressed == 0 {
self.invoke_on_click(); self.invoke_on_click(context);
} }
self.update_colors(context, true); self.update_colors(context, true);
} }

View file

@ -1087,6 +1087,7 @@ pub(crate) mod sealed {
pub transparent: bool, pub transparent: bool,
} }
#[derive(Clone)]
pub enum WindowCommand { pub enum WindowCommand {
Redraw, Redraw,
// RequestClose, // RequestClose,