From c5f1832b3e1bc02d8e41e427a25d2655fcab69a1 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Fri, 10 May 2024 10:16:47 -0700 Subject: [PATCH] Disabled menu item support --- examples/menu.rs | 6 ++ src/widgets/menu.rs | 139 +++++++++++++++++++++++++++++++------------- 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/examples/menu.rs b/examples/menu.rs index 19c416c..6b3e572 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -45,5 +45,11 @@ fn menu(top: bool) -> Menu { .with(MenuItem::new(MenuOptions::First, "First")) .with(MenuItem::new(MenuOptions::Second, "Second")) .with_separator() + .with( + MenuItem::build(MenuOptions::Second) + .text("Disabled") + .disabled(), + ) + .with_separator() .with(third) } diff --git a/src/widgets/menu.rs b/src/widgets/menu.rs index a61feeb..4114498 100644 --- a/src/widgets/menu.rs +++ b/src/widgets/menu.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use alot::LotId; -use figures::units::{Px, UPx}; +use figures::units::{Lp, Px, UPx}; use figures::{Angle, IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero}; use kludgine::shapes::{PathBuilder, Shape, StrokeOptions}; use kludgine::DrawableExt; @@ -18,12 +18,12 @@ use super::disclose::IndicatorSize; use super::layers::{OverlayBuilder, OverlayHandle, OverlayLayer, Overlayable}; use super::Button; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; -use crate::context::{EventContext, GraphicsContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::{ CornerRadius, Easing, IntrinsicPadding, OpaqueWidgetColor, TextColor, }; use crate::styles::Styles; -use crate::value::{Dynamic, Source}; +use crate::value::{Dynamic, IntoValue, Source, Value}; use crate::widget::{ Callback, EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetId, WidgetInstance, WidgetRef, WidgetTag, HANDLED, @@ -130,13 +130,17 @@ where value, widget, submenu, + enabled, }) => ItemKind::Item(OpenItem { value: value.clone(), - contents: WidgetRef::new(widget.clone().align_left()), + contents: WidgetRef::new( + widget.clone().align_left().with_enabled(enabled.clone()), + ), submenu: submenu.clone(), colors: None, color_animation: AnimationHandle::default(), state: VisualState::Normal, + enabled: enabled.clone(), }), ItemKind::Separator => ItemKind::Separator, }, @@ -154,6 +158,7 @@ where open_id: root_menu, padding: UPx::ZERO, selecting: None, + hover_location: None, mouse_down: false, layer: overlay.clone(), open_submenu: None, @@ -244,6 +249,7 @@ pub struct MenuItemBuilder { value: T, submenu: Option>, contents: Contents, + enabled: Value, } impl MenuItemBuilder { @@ -252,12 +258,14 @@ impl MenuItemBuilder { let Self { value, submenu, + enabled, contents: (), } = self; MenuItemBuilder { value, submenu, + enabled, contents: text.into(), } } @@ -267,12 +275,14 @@ impl MenuItemBuilder { let Self { value, submenu, + enabled, contents: (), } = self; MenuItemBuilder { value, submenu, + enabled, contents: widget.make_widget(), } } @@ -289,7 +299,7 @@ mod sealed { use kempt::Set; use super::{MenuOverlay, OpenMenuHandle}; - use crate::value::Dynamic; + use crate::value::{Dynamic, Value}; use crate::widget::WidgetId; use crate::widgets::layers::OverlayLayer; @@ -306,6 +316,7 @@ mod sealed { self, value: T, submenu: Option>, + enabled: Value, ) -> super::MenuItem; } @@ -319,21 +330,33 @@ mod sealed { impl MenuItemContents for String {} impl MenuItemContents for WidgetInstance {} impl sealed::MenuItemContentsSealed for String { - fn make_item(self, value: T, submenu: Option>) -> MenuItem { + fn make_item( + self, + value: T, + submenu: Option>, + enabled: Value, + ) -> MenuItem { MenuItem { value, widget: self.make_widget(), submenu, + enabled, } } } impl sealed::MenuItemContentsSealed for WidgetInstance { - fn make_item(self, value: T, submenu: Option>) -> MenuItem { + fn make_item( + self, + value: T, + submenu: Option>, + enabled: Value, + ) -> MenuItem { MenuItem { value, widget: self, submenu, + enabled, } } } @@ -365,9 +388,24 @@ where self } + /// Sets whether this menu item should be enabled, and returns self. + #[must_use] + pub fn enabled(mut self, enabled: impl IntoValue) -> Self { + self.enabled = enabled.into_value(); + self + } + + /// Disables this menu item, and returns self. + #[must_use] + pub fn disabled(mut self) -> Self { + self.enabled = Value::Constant(false); + self + } + /// Returns the finished menu item. pub fn finish(self) -> MenuItem { - self.contents.make_item(self.value, self.submenu) + self.contents + .make_item(self.value, self.submenu, self.enabled) } } @@ -385,6 +423,7 @@ where pub struct MenuItem { value: T, widget: WidgetInstance, + enabled: Value, submenu: Option>, } @@ -398,6 +437,7 @@ impl MenuItem { pub fn build(value: T) -> MenuItemBuilder { MenuItemBuilder { value, + enabled: Value::Constant(true), submenu: None, contents: (), } @@ -413,6 +453,7 @@ where .field("value", &self.value) .field("widget", &self.widget) .field("submenu", &self.submenu.is_some()) + .field("enabled", &self.enabled) .finish() } } @@ -428,6 +469,7 @@ struct OpenMenu { open_id: LotId, padding: UPx, selecting: Option, + hover_location: Option>, mouse_down: bool, layer: OverlayLayer, open_submenu: Option<(usize, OpenMenuHandle)>, @@ -436,43 +478,49 @@ struct OpenMenu { shared: Dynamic, } impl OpenMenu { - fn handle_mouse_movement(&mut self, location: Point, context: &mut EventContext<'_>) { + fn update_visual_state(&mut self, context: &mut EventContext<'_>) { + let location = self.hover_location.unwrap_or(Point::squared(Px::new(-1))); self.selecting = None; for (index, rendered) in self.items.iter_mut().enumerate() { let hovered = location.y >= rendered.y - self.padding && location.y < rendered.y + rendered.height + self.padding; if let ItemKind::Item(item) = &mut rendered.item { - let new_state = if hovered { - self.selecting = Some(index); - if let Some((submenu_index, handle)) = &self.open_submenu { - if *submenu_index != index { - context.focus(); - handle.dismiss(); - self.open_submenu = None; + let enabled = item.enabled.get_tracking_redraw(context); + let new_state = if enabled { + if hovered { + self.selecting = Some(index); + if let Some((submenu_index, handle)) = &self.open_submenu { + if *submenu_index != index { + context.focus(); + handle.dismiss(); + self.open_submenu = None; + } + } else if let Some(factory) = &item.submenu { + let last_layout = context.last_layout().expect("must have rendered"); + let menu_location = Point::new( + last_layout.origin.x + last_layout.size.width + - self.padding.into_signed() * 2, + last_layout.origin.y + (rendered.y - self.padding).into_signed(), + ); + self.open_submenu = Some(( + index, + factory + .overlay_submenu_in(&self.layer, self.shared.clone()) + .parent(self.menu_id) + .at(menu_location) + .show(), + )); + } + if self.mouse_down { + VisualState::Active + } else { + VisualState::Hovered } - } else if let Some(factory) = &item.submenu { - let last_layout = context.last_layout().expect("must have rendered"); - let menu_location = Point::new( - last_layout.origin.x + last_layout.size.width - - self.padding.into_signed() * 2, - last_layout.origin.y + (rendered.y - self.padding).into_signed(), - ); - self.open_submenu = Some(( - index, - factory - .overlay_submenu_in(&self.layer, self.shared.clone()) - .parent(self.menu_id) - .at(menu_location) - .show(), - )); - } - if self.mouse_down { - VisualState::Active } else { - VisualState::Hovered + VisualState::Normal } } else { - VisualState::Normal + VisualState::Disabled }; if item.state != new_state { item.state = new_state; @@ -502,6 +550,8 @@ where T: Clone + Debug + Send + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { + self.update_visual_state(&mut context.as_event_context()); + let radii = context.get(&CornerRadius); let radii = radii.map(|r| r.into_px(context.gfx.scale())); let bg = context.get(&OpaqueWidgetColor); @@ -540,7 +590,7 @@ where .line_to(Point::new(full_size.width - self.padding * 2, UPx::ZERO)) .close() .stroke( - StrokeOptions::mm_wide(1) + StrokeOptions::lp_wide(Lp::points(2)) .into_upx(context.gfx.scale()) .colored(context.theme().surface.color), ); @@ -667,17 +717,19 @@ where location: Point, context: &mut crate::context::EventContext<'_>, ) -> Option { - self.handle_mouse_movement(location, context); + self.hover_location = Some(location); + self.update_visual_state(context); self.shared.lock().hovering.insert(context.widget().id()); None } fn unhover(&mut self, context: &mut EventContext<'_>) { + self.hover_location = None; let mut shared = self.shared.lock(); shared.hovering.remove(&context.widget().id()); if shared.hovering.is_empty() { drop(shared); - self.handle_mouse_movement(Point::squared(Px::new(-1)), context); + self.update_visual_state(context); } } @@ -689,7 +741,8 @@ where context: &mut crate::context::EventContext<'_>, ) -> EventHandling { self.mouse_down = true; - self.handle_mouse_movement(location, context); + self.hover_location = Some(location); + self.update_visual_state(context); HANDLED } @@ -701,7 +754,8 @@ where _button: kludgine::app::winit::event::MouseButton, context: &mut crate::context::EventContext<'_>, ) { - self.handle_mouse_movement(location, context); + self.hover_location = Some(location); + self.update_visual_state(context); } fn mouse_up( @@ -724,6 +778,7 @@ where handle.dismiss(); } } + self.hover_location = None; self.mouse_down = false; } @@ -776,6 +831,7 @@ impl RenderedItem { struct OpenItem { value: T, + enabled: Value, contents: WidgetRef, submenu: Option>, colors: Option>, @@ -792,6 +848,7 @@ where .field("value", &self.value) .field("contents", &self.contents) .field("submenu", &self.submenu.is_some()) + .field("enabled", &self.enabled) .finish_non_exhaustive() } }