Disabled menu item support

This commit is contained in:
Jonathan Johnson 2024-05-10 10:16:47 -07:00
parent b8410cd4ba
commit c5f1832b3e
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
2 changed files with 104 additions and 41 deletions

View file

@ -45,5 +45,11 @@ fn menu(top: bool) -> Menu<MenuOptions> {
.with(MenuItem::new(MenuOptions::First, "First")) .with(MenuItem::new(MenuOptions::First, "First"))
.with(MenuItem::new(MenuOptions::Second, "Second")) .with(MenuItem::new(MenuOptions::Second, "Second"))
.with_separator() .with_separator()
.with(
MenuItem::build(MenuOptions::Second)
.text("Disabled")
.disabled(),
)
.with_separator()
.with(third) .with(third)
} }

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use alot::LotId; 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 figures::{Angle, IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero};
use kludgine::shapes::{PathBuilder, Shape, StrokeOptions}; use kludgine::shapes::{PathBuilder, Shape, StrokeOptions};
use kludgine::DrawableExt; use kludgine::DrawableExt;
@ -18,12 +18,12 @@ use super::disclose::IndicatorSize;
use super::layers::{OverlayBuilder, OverlayHandle, OverlayLayer, Overlayable}; use super::layers::{OverlayBuilder, OverlayHandle, OverlayLayer, Overlayable};
use super::Button; use super::Button;
use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::{ use crate::styles::components::{
CornerRadius, Easing, IntrinsicPadding, OpaqueWidgetColor, TextColor, CornerRadius, Easing, IntrinsicPadding, OpaqueWidgetColor, TextColor,
}; };
use crate::styles::Styles; use crate::styles::Styles;
use crate::value::{Dynamic, Source}; use crate::value::{Dynamic, IntoValue, Source, Value};
use crate::widget::{ use crate::widget::{
Callback, EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetId, WidgetInstance, Callback, EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetId, WidgetInstance,
WidgetRef, WidgetTag, HANDLED, WidgetRef, WidgetTag, HANDLED,
@ -130,13 +130,17 @@ where
value, value,
widget, widget,
submenu, submenu,
enabled,
}) => ItemKind::Item(OpenItem { }) => ItemKind::Item(OpenItem {
value: value.clone(), value: value.clone(),
contents: WidgetRef::new(widget.clone().align_left()), contents: WidgetRef::new(
widget.clone().align_left().with_enabled(enabled.clone()),
),
submenu: submenu.clone(), submenu: submenu.clone(),
colors: None, colors: None,
color_animation: AnimationHandle::default(), color_animation: AnimationHandle::default(),
state: VisualState::Normal, state: VisualState::Normal,
enabled: enabled.clone(),
}), }),
ItemKind::Separator => ItemKind::Separator, ItemKind::Separator => ItemKind::Separator,
}, },
@ -154,6 +158,7 @@ where
open_id: root_menu, open_id: root_menu,
padding: UPx::ZERO, padding: UPx::ZERO,
selecting: None, selecting: None,
hover_location: None,
mouse_down: false, mouse_down: false,
layer: overlay.clone(), layer: overlay.clone(),
open_submenu: None, open_submenu: None,
@ -244,6 +249,7 @@ pub struct MenuItemBuilder<T, Contents = ()> {
value: T, value: T,
submenu: Option<Arc<dyn SubmenuFactory>>, submenu: Option<Arc<dyn SubmenuFactory>>,
contents: Contents, contents: Contents,
enabled: Value<bool>,
} }
impl<T> MenuItemBuilder<T, ()> { impl<T> MenuItemBuilder<T, ()> {
@ -252,12 +258,14 @@ impl<T> MenuItemBuilder<T, ()> {
let Self { let Self {
value, value,
submenu, submenu,
enabled,
contents: (), contents: (),
} = self; } = self;
MenuItemBuilder { MenuItemBuilder {
value, value,
submenu, submenu,
enabled,
contents: text.into(), contents: text.into(),
} }
} }
@ -267,12 +275,14 @@ impl<T> MenuItemBuilder<T, ()> {
let Self { let Self {
value, value,
submenu, submenu,
enabled,
contents: (), contents: (),
} = self; } = self;
MenuItemBuilder { MenuItemBuilder {
value, value,
submenu, submenu,
enabled,
contents: widget.make_widget(), contents: widget.make_widget(),
} }
} }
@ -289,7 +299,7 @@ mod sealed {
use kempt::Set; use kempt::Set;
use super::{MenuOverlay, OpenMenuHandle}; use super::{MenuOverlay, OpenMenuHandle};
use crate::value::Dynamic; use crate::value::{Dynamic, Value};
use crate::widget::WidgetId; use crate::widget::WidgetId;
use crate::widgets::layers::OverlayLayer; use crate::widgets::layers::OverlayLayer;
@ -306,6 +316,7 @@ mod sealed {
self, self,
value: T, value: T,
submenu: Option<Arc<dyn SubmenuFactory>>, submenu: Option<Arc<dyn SubmenuFactory>>,
enabled: Value<bool>,
) -> super::MenuItem<T>; ) -> super::MenuItem<T>;
} }
@ -319,21 +330,33 @@ mod sealed {
impl<T> MenuItemContents<T> for String {} impl<T> MenuItemContents<T> for String {}
impl<T> MenuItemContents<T> for WidgetInstance {} impl<T> MenuItemContents<T> for WidgetInstance {}
impl<T> sealed::MenuItemContentsSealed<T> for String { impl<T> sealed::MenuItemContentsSealed<T> for String {
fn make_item(self, value: T, submenu: Option<Arc<dyn SubmenuFactory>>) -> MenuItem<T> { fn make_item(
self,
value: T,
submenu: Option<Arc<dyn SubmenuFactory>>,
enabled: Value<bool>,
) -> MenuItem<T> {
MenuItem { MenuItem {
value, value,
widget: self.make_widget(), widget: self.make_widget(),
submenu, submenu,
enabled,
} }
} }
} }
impl<T> sealed::MenuItemContentsSealed<T> for WidgetInstance { impl<T> sealed::MenuItemContentsSealed<T> for WidgetInstance {
fn make_item(self, value: T, submenu: Option<Arc<dyn SubmenuFactory>>) -> MenuItem<T> { fn make_item(
self,
value: T,
submenu: Option<Arc<dyn SubmenuFactory>>,
enabled: Value<bool>,
) -> MenuItem<T> {
MenuItem { MenuItem {
value, value,
widget: self, widget: self,
submenu, submenu,
enabled,
} }
} }
} }
@ -365,9 +388,24 @@ where
self self
} }
/// Sets whether this menu item should be enabled, and returns self.
#[must_use]
pub fn enabled(mut self, enabled: impl IntoValue<bool>) -> 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. /// Returns the finished menu item.
pub fn finish(self) -> MenuItem<T> { pub fn finish(self) -> MenuItem<T> {
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<T> { pub struct MenuItem<T> {
value: T, value: T,
widget: WidgetInstance, widget: WidgetInstance,
enabled: Value<bool>,
submenu: Option<Arc<dyn SubmenuFactory>>, submenu: Option<Arc<dyn SubmenuFactory>>,
} }
@ -398,6 +437,7 @@ impl<T> MenuItem<T> {
pub fn build(value: T) -> MenuItemBuilder<T, ()> { pub fn build(value: T) -> MenuItemBuilder<T, ()> {
MenuItemBuilder { MenuItemBuilder {
value, value,
enabled: Value::Constant(true),
submenu: None, submenu: None,
contents: (), contents: (),
} }
@ -413,6 +453,7 @@ where
.field("value", &self.value) .field("value", &self.value)
.field("widget", &self.widget) .field("widget", &self.widget)
.field("submenu", &self.submenu.is_some()) .field("submenu", &self.submenu.is_some())
.field("enabled", &self.enabled)
.finish() .finish()
} }
} }
@ -428,6 +469,7 @@ struct OpenMenu<T> {
open_id: LotId, open_id: LotId,
padding: UPx, padding: UPx,
selecting: Option<usize>, selecting: Option<usize>,
hover_location: Option<Point<Px>>,
mouse_down: bool, mouse_down: bool,
layer: OverlayLayer, layer: OverlayLayer,
open_submenu: Option<(usize, OpenMenuHandle)>, open_submenu: Option<(usize, OpenMenuHandle)>,
@ -436,43 +478,49 @@ struct OpenMenu<T> {
shared: Dynamic<SharedMenuState>, shared: Dynamic<SharedMenuState>,
} }
impl<T> OpenMenu<T> { impl<T> OpenMenu<T> {
fn handle_mouse_movement(&mut self, location: Point<Px>, 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; self.selecting = None;
for (index, rendered) in self.items.iter_mut().enumerate() { for (index, rendered) in self.items.iter_mut().enumerate() {
let hovered = location.y >= rendered.y - self.padding let hovered = location.y >= rendered.y - self.padding
&& location.y < rendered.y + rendered.height + self.padding; && location.y < rendered.y + rendered.height + self.padding;
if let ItemKind::Item(item) = &mut rendered.item { if let ItemKind::Item(item) = &mut rendered.item {
let new_state = if hovered { let enabled = item.enabled.get_tracking_redraw(context);
self.selecting = Some(index); let new_state = if enabled {
if let Some((submenu_index, handle)) = &self.open_submenu { if hovered {
if *submenu_index != index { self.selecting = Some(index);
context.focus(); if let Some((submenu_index, handle)) = &self.open_submenu {
handle.dismiss(); if *submenu_index != index {
self.open_submenu = None; 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 { } else {
VisualState::Hovered VisualState::Normal
} }
} else { } else {
VisualState::Normal VisualState::Disabled
}; };
if item.state != new_state { if item.state != new_state {
item.state = new_state; item.state = new_state;
@ -502,6 +550,8 @@ where
T: Clone + Debug + Send + 'static, T: Clone + Debug + Send + 'static,
{ {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
self.update_visual_state(&mut context.as_event_context());
let radii = context.get(&CornerRadius); let radii = context.get(&CornerRadius);
let radii = radii.map(|r| r.into_px(context.gfx.scale())); let radii = radii.map(|r| r.into_px(context.gfx.scale()));
let bg = context.get(&OpaqueWidgetColor); let bg = context.get(&OpaqueWidgetColor);
@ -540,7 +590,7 @@ where
.line_to(Point::new(full_size.width - self.padding * 2, UPx::ZERO)) .line_to(Point::new(full_size.width - self.padding * 2, UPx::ZERO))
.close() .close()
.stroke( .stroke(
StrokeOptions::mm_wide(1) StrokeOptions::lp_wide(Lp::points(2))
.into_upx(context.gfx.scale()) .into_upx(context.gfx.scale())
.colored(context.theme().surface.color), .colored(context.theme().surface.color),
); );
@ -667,17 +717,19 @@ where
location: Point<Px>, location: Point<Px>,
context: &mut crate::context::EventContext<'_>, context: &mut crate::context::EventContext<'_>,
) -> Option<kludgine::app::winit::window::CursorIcon> { ) -> Option<kludgine::app::winit::window::CursorIcon> {
self.handle_mouse_movement(location, context); self.hover_location = Some(location);
self.update_visual_state(context);
self.shared.lock().hovering.insert(context.widget().id()); self.shared.lock().hovering.insert(context.widget().id());
None None
} }
fn unhover(&mut self, context: &mut EventContext<'_>) { fn unhover(&mut self, context: &mut EventContext<'_>) {
self.hover_location = None;
let mut shared = self.shared.lock(); let mut shared = self.shared.lock();
shared.hovering.remove(&context.widget().id()); shared.hovering.remove(&context.widget().id());
if shared.hovering.is_empty() { if shared.hovering.is_empty() {
drop(shared); 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<'_>, context: &mut crate::context::EventContext<'_>,
) -> EventHandling { ) -> EventHandling {
self.mouse_down = true; self.mouse_down = true;
self.handle_mouse_movement(location, context); self.hover_location = Some(location);
self.update_visual_state(context);
HANDLED HANDLED
} }
@ -701,7 +754,8 @@ where
_button: kludgine::app::winit::event::MouseButton, _button: kludgine::app::winit::event::MouseButton,
context: &mut crate::context::EventContext<'_>, context: &mut crate::context::EventContext<'_>,
) { ) {
self.handle_mouse_movement(location, context); self.hover_location = Some(location);
self.update_visual_state(context);
} }
fn mouse_up( fn mouse_up(
@ -724,6 +778,7 @@ where
handle.dismiss(); handle.dismiss();
} }
} }
self.hover_location = None;
self.mouse_down = false; self.mouse_down = false;
} }
@ -776,6 +831,7 @@ impl<T> RenderedItem<T> {
struct OpenItem<T> { struct OpenItem<T> {
value: T, value: T,
enabled: Value<bool>,
contents: WidgetRef, contents: WidgetRef,
submenu: Option<Arc<dyn SubmenuFactory>>, submenu: Option<Arc<dyn SubmenuFactory>>,
colors: Option<Dynamic<ButtonColors>>, colors: Option<Dynamic<ButtonColors>>,
@ -792,6 +848,7 @@ where
.field("value", &self.value) .field("value", &self.value)
.field("contents", &self.contents) .field("contents", &self.contents)
.field("submenu", &self.submenu.is_some()) .field("submenu", &self.submenu.is_some())
.field("enabled", &self.enabled)
.finish_non_exhaustive() .finish_non_exhaustive()
} }
} }