mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 11:40:37 +00:00
Disabled menu item support
This commit is contained in:
parent
b8410cd4ba
commit
c5f1832b3e
2 changed files with 104 additions and 41 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue