mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 15:01:11 +00:00
Submenu support in context menus
This commit is contained in:
parent
8b96966031
commit
38cdea9816
6 changed files with 490 additions and 98 deletions
|
|
@ -19,13 +19,7 @@ fn main() -> cushy::Result {
|
|||
let overlay = overlay.clone();
|
||||
move |click| {
|
||||
if let Some(click) = click {
|
||||
Menu::new()
|
||||
.with(MenuItem::new(MenuOptions::First, "First"))
|
||||
.with(MenuItem::new(MenuOptions::Second, "Second"))
|
||||
.with(MenuItem::new(MenuOptions::Third, "Third"))
|
||||
.on_selected(|selected| {
|
||||
println!("Selected item: {selected:?}");
|
||||
})
|
||||
menu(true)
|
||||
.overlay_in(&overlay)
|
||||
.at(click.window_location)
|
||||
.show();
|
||||
|
|
@ -38,3 +32,17 @@ fn main() -> cushy::Result {
|
|||
.into_layers()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn menu(top: bool) -> Menu<MenuOptions> {
|
||||
let mut third = MenuItem::build(MenuOptions::Third).text("Third");
|
||||
if top {
|
||||
third = third.submenu(menu(false));
|
||||
}
|
||||
Menu::new()
|
||||
.on_selected(|selected| {
|
||||
println!("Selected item: {selected:?}");
|
||||
})
|
||||
.with(MenuItem::new(MenuOptions::First, "First"))
|
||||
.with(MenuItem::new(MenuOptions::Second, "Second"))
|
||||
.with(third)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,10 +154,6 @@ impl<'context> EventContext<'context> {
|
|||
|
||||
pub(crate) fn hover(&mut self, location: Point<Px>) {
|
||||
let changes = self.tree.hover(Some(&self.current_node));
|
||||
for unhovered in changes.unhovered {
|
||||
let mut context = self.for_other(&unhovered);
|
||||
unhovered.lock().as_widget().unhover(&mut context);
|
||||
}
|
||||
|
||||
let mut cursor = None;
|
||||
for hover in changes.hovered.into_iter().rev() {
|
||||
|
|
@ -176,6 +172,11 @@ impl<'context> EventContext<'context> {
|
|||
}
|
||||
self.window_mut()
|
||||
.set_cursor(cursor.unwrap_or_default().into());
|
||||
|
||||
for unhovered in changes.unhovered {
|
||||
let mut context = self.for_other(&unhovered);
|
||||
unhovered.lock().as_widget().unhover(&mut context);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_hover(&mut self) {
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ impl Widget for Container {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn render_shadow(
|
||||
pub(crate) fn render_shadow(
|
||||
child_area: &Rect<Px>,
|
||||
mut corner_radii: CornerRadii<Px>,
|
||||
shadow: &ContainerShadow<Px>,
|
||||
|
|
|
|||
|
|
@ -584,6 +584,10 @@ pub trait Overlayable: Sized {
|
|||
#[must_use]
|
||||
fn hide_on_unhover(self) -> Self;
|
||||
|
||||
/// Show this overlay with a relationship to another widget.
|
||||
#[must_use]
|
||||
fn parent(self, id: WidgetId) -> Self;
|
||||
|
||||
/// Show this overlay to the left of the specified widget.
|
||||
#[must_use]
|
||||
fn left_of(self, id: WidgetId) -> Self;
|
||||
|
|
@ -631,6 +635,11 @@ impl Overlayable for OverlayBuilder<'_> {
|
|||
self
|
||||
}
|
||||
|
||||
fn parent(mut self, id: WidgetId) -> Self {
|
||||
self.layout.relative_to = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
fn left_of(self, id: WidgetId) -> Self {
|
||||
self.near(id, Direction::Left)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,32 @@
|
|||
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use figures::units::{Lp, Px, UPx};
|
||||
use figures::{IntoSigned, Point, Rect, Size, Zero};
|
||||
use alot::LotId;
|
||||
use figures::units::{Px, UPx};
|
||||
use figures::{Angle, IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero};
|
||||
use kludgine::shapes::{PathBuilder, Shape};
|
||||
use kludgine::DrawableExt;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use super::button::{ButtonClick, ButtonKind};
|
||||
use super::container::ContainerShadow;
|
||||
use self::sealed::{SharedMenuState, SubmenuFactory};
|
||||
use super::button::{ButtonClick, ButtonColors, ButtonKind, VisualState};
|
||||
use super::container::{self, ContainerShadow};
|
||||
use super::disclose::IndicatorSize;
|
||||
use super::layers::{OverlayBuilder, OverlayHandle, OverlayLayer, Overlayable};
|
||||
use crate::context::{GraphicsContext, LayoutContext};
|
||||
use crate::value::Dynamic;
|
||||
use crate::widget::{Callback, MakeWidget, Widget, WidgetInstance, WidgetRef};
|
||||
use super::Button;
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{
|
||||
CornerRadius, Easing, IntrinsicPadding, OpaqueWidgetColor, TextColor,
|
||||
};
|
||||
use crate::styles::Styles;
|
||||
use crate::value::{Dynamic, Source};
|
||||
use crate::widget::{
|
||||
Callback, EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetId, WidgetInstance,
|
||||
WidgetRef, WidgetTag, HANDLED,
|
||||
};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// An overlayable menu of selectable items.
|
||||
|
|
@ -21,12 +36,12 @@ use crate::ConstraintLimit;
|
|||
/// used with an [`OverlayLayer`], this widget can be shown above other widgets
|
||||
/// or at a specific location.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Menu<T> {
|
||||
pub struct Menu<T, Handler = MenuHandler<T>> {
|
||||
items: Vec<MenuItem<T>>,
|
||||
on_click: Option<Arc<Mutex<Callback<ChosenMenuItem<T>>>>>,
|
||||
on_click: Handler,
|
||||
}
|
||||
|
||||
impl<T> Default for Menu<T>
|
||||
impl<T> Default for Menu<T, ()>
|
||||
where
|
||||
T: Debug + Send + Clone + 'static,
|
||||
{
|
||||
|
|
@ -35,7 +50,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Menu<T>
|
||||
impl<T> Menu<T, ()>
|
||||
where
|
||||
T: Debug + Send + Clone + 'static,
|
||||
{
|
||||
|
|
@ -44,73 +59,95 @@ where
|
|||
pub const fn new() -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
on_click: None,
|
||||
on_click: (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the selected handler to `selected`, causing it to be invoked when
|
||||
/// an item is chosen.
|
||||
#[must_use]
|
||||
pub fn on_selected<F>(self, selected: F) -> Menu<T>
|
||||
where
|
||||
F: FnMut(ChosenMenuItem<T>) + Send + 'static,
|
||||
{
|
||||
Menu {
|
||||
items: self.items,
|
||||
on_click: MenuHandler(Arc::new(Mutex::new(Callback::new(selected)))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Handler> Menu<T, Handler>
|
||||
where
|
||||
T: Debug + Send + Clone + 'static,
|
||||
{
|
||||
/// Adds another menu `item` that is displayed using `widget`.
|
||||
#[must_use]
|
||||
pub fn with(mut self, item: impl Into<MenuItem<T>>) -> Self {
|
||||
self.items.push(item.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the selected handler to `selected`, causing it to be invoked when
|
||||
/// an item is chosen.
|
||||
#[must_use]
|
||||
pub fn on_selected<F>(mut self, selected: F) -> Self
|
||||
where
|
||||
F: FnMut(ChosenMenuItem<T>) + Send + 'static,
|
||||
{
|
||||
self.on_click = Some(Arc::new(Mutex::new(Callback::new(selected))));
|
||||
self
|
||||
}
|
||||
|
||||
impl<T> Menu<T>
|
||||
where
|
||||
T: Debug + Send + Clone + 'static,
|
||||
{
|
||||
/// Presents this menu in `overlay`, returning an [`Overlayable`] that can
|
||||
/// be positioned relative or absolutely within `overlay`.
|
||||
#[must_use]
|
||||
pub fn overlay_in<'overlay>(&self, overlay: &'overlay OverlayLayer) -> MenuOverlay<'overlay> {
|
||||
self.overlay_in_shared(overlay, Dynamic::default())
|
||||
}
|
||||
|
||||
fn overlay_in_shared<'overlay>(
|
||||
&self,
|
||||
overlay: &'overlay OverlayLayer,
|
||||
shared: Dynamic<SharedMenuState>,
|
||||
) -> MenuOverlay<'overlay> {
|
||||
let Self { items, on_click } = self;
|
||||
let handle = OpenMenuHandle(Dynamic::new(None));
|
||||
let items = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let MenuItem { value, widget } = item;
|
||||
let handle = handle.clone();
|
||||
let MenuItem {
|
||||
value,
|
||||
widget,
|
||||
submenu,
|
||||
} = item;
|
||||
OpenItem {
|
||||
contents: WidgetRef::new(
|
||||
widget
|
||||
.clone()
|
||||
.into_button()
|
||||
.kind(ButtonKind::Transparent)
|
||||
.on_click({
|
||||
let on_click = on_click.clone();
|
||||
let value = value.clone();
|
||||
move |click| {
|
||||
if let Some(on_click) = &on_click {
|
||||
let mut on_click = on_click.lock();
|
||||
on_click.invoke(ChosenMenuItem {
|
||||
item: value.clone(),
|
||||
click,
|
||||
});
|
||||
}
|
||||
handle.dismiss();
|
||||
}
|
||||
}),
|
||||
),
|
||||
value: value.clone(),
|
||||
contents: WidgetRef::new(widget.clone().align_left()),
|
||||
y: UPx::ZERO,
|
||||
height: UPx::ZERO,
|
||||
submenu: submenu.clone(),
|
||||
colors: None,
|
||||
color_animation: AnimationHandle::default(),
|
||||
state: VisualState::Normal,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let root_menu = shared.lock().open_menus.push(handle.clone());
|
||||
|
||||
let (menu_tag, menu_id) = WidgetTag::new();
|
||||
MenuOverlay(
|
||||
overlay.build_overlay(
|
||||
OpenMenu {
|
||||
on_click: on_click.clone(),
|
||||
items,
|
||||
handle: handle.clone(),
|
||||
open_id: root_menu,
|
||||
padding: UPx::ZERO,
|
||||
selecting: None,
|
||||
mouse_down: false,
|
||||
layer: overlay.clone(),
|
||||
open_submenu: None,
|
||||
menu_id,
|
||||
disclosure_size: UPx::ZERO,
|
||||
shared,
|
||||
}
|
||||
.contain()
|
||||
.shadow(ContainerShadow::drop(Lp::mm(1), Lp::mm(2)))
|
||||
.vertical_scroll(),
|
||||
.vertical_scroll()
|
||||
.make_with_tag(menu_tag),
|
||||
),
|
||||
handle,
|
||||
)
|
||||
|
|
@ -127,6 +164,10 @@ impl<'a> Overlayable for MenuOverlay<'a> {
|
|||
Self(self.0.hide_on_unhover(), self.1)
|
||||
}
|
||||
|
||||
fn parent(self, id: crate::widget::WidgetId) -> Self {
|
||||
Self(self.0.parent(id), self.1)
|
||||
}
|
||||
|
||||
fn left_of(self, id: crate::widget::WidgetId) -> Self {
|
||||
Self(self.0.left_of(id), self.1)
|
||||
}
|
||||
|
|
@ -186,6 +227,7 @@ pub struct ChosenMenuItem<T> {
|
|||
/// A builder of a [`MenuItem<T>`].
|
||||
pub struct MenuItemBuilder<T, Contents = ()> {
|
||||
value: T,
|
||||
submenu: Option<Arc<dyn SubmenuFactory>>,
|
||||
contents: Contents,
|
||||
}
|
||||
|
||||
|
|
@ -194,11 +236,13 @@ impl<T> MenuItemBuilder<T, ()> {
|
|||
pub fn text(self, text: impl Into<String>) -> MenuItemBuilder<T, String> {
|
||||
let Self {
|
||||
value,
|
||||
submenu,
|
||||
contents: (),
|
||||
} = self;
|
||||
|
||||
MenuItemBuilder {
|
||||
value,
|
||||
submenu,
|
||||
contents: text.into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -207,11 +251,13 @@ impl<T> MenuItemBuilder<T, ()> {
|
|||
pub fn widget(self, widget: impl MakeWidget) -> MenuItemBuilder<T, WidgetInstance> {
|
||||
let Self {
|
||||
value,
|
||||
submenu,
|
||||
contents: (),
|
||||
} = self;
|
||||
|
||||
MenuItemBuilder {
|
||||
value,
|
||||
submenu,
|
||||
contents: widget.make_widget(),
|
||||
}
|
||||
}
|
||||
|
|
@ -222,38 +268,91 @@ impl<T> MenuItemBuilder<T, ()> {
|
|||
pub trait MenuItemContents<T>: sealed::MenuItemContentsSealed<T> {}
|
||||
|
||||
mod sealed {
|
||||
use std::sync::Arc;
|
||||
|
||||
use alot::OrderedLots;
|
||||
use kempt::Set;
|
||||
|
||||
use super::{MenuOverlay, OpenMenuHandle};
|
||||
use crate::value::Dynamic;
|
||||
use crate::widget::WidgetId;
|
||||
use crate::widgets::layers::OverlayLayer;
|
||||
|
||||
pub trait SubmenuFactory: Send + Sync + 'static {
|
||||
fn overlay_submenu_in<'overlay>(
|
||||
&self,
|
||||
overlay: &'overlay OverlayLayer,
|
||||
shared_state: Dynamic<SharedMenuState>,
|
||||
) -> MenuOverlay<'overlay>;
|
||||
}
|
||||
|
||||
pub trait MenuItemContentsSealed<T> {
|
||||
fn make_item(self, value: T) -> super::MenuItem<T>;
|
||||
fn make_item(
|
||||
self,
|
||||
value: T,
|
||||
submenu: Option<Arc<dyn SubmenuFactory>>,
|
||||
) -> super::MenuItem<T>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SharedMenuState {
|
||||
pub open_menus: OrderedLots<OpenMenuHandle>,
|
||||
pub hovering: Set<WidgetId>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MenuItemContents<T> for String {}
|
||||
impl<T> MenuItemContents<T> for WidgetInstance {}
|
||||
impl<T> sealed::MenuItemContentsSealed<T> for String {
|
||||
fn make_item(self, value: T) -> MenuItem<T> {
|
||||
fn make_item(self, value: T, submenu: Option<Arc<dyn SubmenuFactory>>) -> MenuItem<T> {
|
||||
MenuItem {
|
||||
value,
|
||||
widget: self.make_widget(),
|
||||
submenu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> sealed::MenuItemContentsSealed<T> for WidgetInstance {
|
||||
fn make_item(self, value: T) -> MenuItem<T> {
|
||||
fn make_item(self, value: T, submenu: Option<Arc<dyn SubmenuFactory>>) -> MenuItem<T> {
|
||||
MenuItem {
|
||||
value,
|
||||
widget: self,
|
||||
submenu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> sealed::SubmenuFactory for Menu<T>
|
||||
where
|
||||
T: Clone + std::fmt::Debug + Send + Sync + 'static,
|
||||
{
|
||||
fn overlay_submenu_in<'overlay>(
|
||||
&self,
|
||||
overlay: &'overlay OverlayLayer,
|
||||
shared_state: Dynamic<SharedMenuState>,
|
||||
) -> MenuOverlay<'overlay> {
|
||||
self.overlay_in_shared(overlay, shared_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Contents> MenuItemBuilder<T, Contents>
|
||||
where
|
||||
Contents: MenuItemContents<T>,
|
||||
{
|
||||
/// Attaches a submenu to this item and returns self.
|
||||
#[must_use]
|
||||
pub fn submenu<U>(mut self, submenu: Menu<U>) -> Self
|
||||
where
|
||||
U: Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
self.submenu = Some(Arc::new(submenu));
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the finished menu item.
|
||||
pub fn finish(self) -> MenuItem<T> {
|
||||
self.contents.make_item(self.value)
|
||||
self.contents.make_item(self.value, self.submenu)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,11 +366,11 @@ where
|
|||
}
|
||||
|
||||
/// An item in a [`Menu<T>`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct MenuItem<T> {
|
||||
value: T,
|
||||
widget: WidgetInstance,
|
||||
// submenu: Option<Menu<T>>,
|
||||
submenu: Option<Arc<dyn SubmenuFactory>>,
|
||||
}
|
||||
|
||||
impl<T> MenuItem<T> {
|
||||
|
|
@ -284,28 +383,179 @@ impl<T> MenuItem<T> {
|
|||
pub fn build(value: T) -> MenuItemBuilder<T, ()> {
|
||||
MenuItemBuilder {
|
||||
value,
|
||||
submenu: None,
|
||||
contents: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OpenMenu {
|
||||
items: Vec<OpenItem>,
|
||||
handle: OpenMenuHandle,
|
||||
impl<T> Debug for MenuItem<T>
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MenuItem")
|
||||
.field("value", &self.value)
|
||||
.field("widget", &self.widget)
|
||||
.field("submenu", &self.submenu.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler for a [`ChosenMenuItem<T>`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MenuHandler<T>(Arc<Mutex<Callback<ChosenMenuItem<T>>>>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OpenItem {
|
||||
contents: WidgetRef,
|
||||
height: UPx,
|
||||
struct OpenMenu<T> {
|
||||
items: Vec<OpenItem<T>>,
|
||||
on_click: MenuHandler<T>,
|
||||
open_id: LotId,
|
||||
padding: UPx,
|
||||
selecting: Option<usize>,
|
||||
mouse_down: bool,
|
||||
layer: OverlayLayer,
|
||||
open_submenu: Option<(usize, OpenMenuHandle)>,
|
||||
menu_id: WidgetId,
|
||||
disclosure_size: UPx,
|
||||
shared: Dynamic<SharedMenuState>,
|
||||
}
|
||||
impl<T> OpenMenu<T> {
|
||||
fn handle_mouse_movement(&mut self, location: Point<Px>, context: &mut EventContext<'_>) {
|
||||
self.selecting = None;
|
||||
for (index, item) in self.items.iter_mut().enumerate() {
|
||||
let hovered = location.y >= item.y - self.padding
|
||||
&& location.y < item.y + item.height + self.padding;
|
||||
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;
|
||||
}
|
||||
} 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 + (item.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 {
|
||||
VisualState::Normal
|
||||
};
|
||||
if item.state != new_state {
|
||||
item.state = new_state;
|
||||
let new_colors = if hovered {
|
||||
ButtonKind::Solid.colors_for_default(new_state, context)
|
||||
} else {
|
||||
Button::colors_for_transparent(new_state, context)
|
||||
};
|
||||
if let Some(colors) = &item.colors {
|
||||
item.color_animation = colors
|
||||
.transition_to(new_colors)
|
||||
.over(Duration::from_millis(150))
|
||||
.with_easing(context.get(&Easing))
|
||||
.spawn();
|
||||
} else {
|
||||
item.colors = Some(Dynamic::new(new_colors));
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for OpenMenu {
|
||||
impl<T> Widget for OpenMenu<T>
|
||||
where
|
||||
T: Clone + Debug + Send + 'static,
|
||||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
||||
let radii = context.get(&CornerRadius);
|
||||
let radii = radii.map(|r| r.into_px(context.gfx.scale()));
|
||||
let bg = context.get(&OpaqueWidgetColor);
|
||||
let full_size = context.gfx.size();
|
||||
let content_rect = Rect::new(
|
||||
Point::new(self.padding, UPx::ZERO),
|
||||
Size::new(
|
||||
full_size.width - self.padding * 2,
|
||||
full_size.height - self.padding,
|
||||
),
|
||||
)
|
||||
.into_signed();
|
||||
container::render_shadow(
|
||||
&content_rect,
|
||||
radii,
|
||||
&ContainerShadow::new(Point::ZERO)
|
||||
.blur_radius(self.padding.into_signed())
|
||||
.spread(self.padding.into_signed()),
|
||||
bg,
|
||||
context,
|
||||
);
|
||||
let bg_shape = if radii.is_zero() {
|
||||
Shape::filled_rect(content_rect, bg)
|
||||
} else {
|
||||
Shape::filled_round_rect(content_rect, radii, bg)
|
||||
};
|
||||
context.gfx.draw_shape(&bg_shape);
|
||||
let disclosure_size = (self.disclosure_size.into_signed() / 2).round();
|
||||
let pt1 = Point::new(disclosure_size, Px::ZERO).rotate_by(Angle::degrees(0));
|
||||
let pt2 = Point::new(disclosure_size, Px::ZERO).rotate_by(Angle::degrees(120));
|
||||
let pt3 = Point::new(disclosure_size, Px::ZERO).rotate_by(Angle::degrees(240));
|
||||
|
||||
let submenu = PathBuilder::new(pt1).line_to(pt2).line_to(pt3).close();
|
||||
for item in &mut self.items {
|
||||
let mounted = item.contents.mounted(context);
|
||||
context.for_other(&mounted).redraw();
|
||||
|
||||
if let Some(colors) = &item.colors {
|
||||
let colors = colors.get_tracking_redraw(context);
|
||||
let child_rect = Rect::new(
|
||||
Point::new(self.padding, item.y - self.padding),
|
||||
Size::new(
|
||||
full_size.width - self.padding * 2,
|
||||
item.height + self.padding * 2,
|
||||
),
|
||||
)
|
||||
.into_signed();
|
||||
|
||||
let bg_shape = if radii.is_zero() {
|
||||
Shape::filled_rect(child_rect, colors.background)
|
||||
} else {
|
||||
Shape::filled_round_rect(child_rect, radii, colors.background)
|
||||
};
|
||||
context.gfx.draw_shape(&bg_shape);
|
||||
|
||||
if item.submenu.is_some() {
|
||||
let disclosure_offset = Point::new(
|
||||
full_size.width - self.disclosure_size / 2 - self.padding * 2,
|
||||
item.y + item.height / 2,
|
||||
)
|
||||
.into_signed();
|
||||
context.gfx.draw_shape(
|
||||
submenu
|
||||
.fill(colors.foreground)
|
||||
.translate_by(disclosure_offset),
|
||||
);
|
||||
}
|
||||
|
||||
let mut context = context.for_other(&mounted);
|
||||
context.attach_styles(Styles::new().with(&TextColor, colors.foreground));
|
||||
context.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,33 +566,117 @@ impl Widget for OpenMenu {
|
|||
) -> Size<UPx> {
|
||||
let mut maximum_item_width = UPx::ZERO;
|
||||
let mut remaining_height = available_space.height.max();
|
||||
self.padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
self.disclosure_size =
|
||||
(context.get(&IndicatorSize).into_upx(context.gfx.scale()) / 2).round();
|
||||
let double_padding = self.padding * 2;
|
||||
let submenu_space = if self.items.iter().any(|i| i.submenu.is_some()) {
|
||||
self.padding + self.disclosure_size
|
||||
} else {
|
||||
UPx::ZERO
|
||||
};
|
||||
let available_width = available_space.width.max() - double_padding;
|
||||
|
||||
let mut y = self.padding;
|
||||
for item in &mut self.items {
|
||||
let mounted = item.contents.mounted(context);
|
||||
let available_width = available_width - submenu_space;
|
||||
let size = context.for_other(&mounted).layout(Size::new(
|
||||
ConstraintLimit::SizeToFit(available_space.width.max()),
|
||||
ConstraintLimit::SizeToFit(available_width),
|
||||
ConstraintLimit::SizeToFit(remaining_height),
|
||||
));
|
||||
item.y = y;
|
||||
item.height = size.height;
|
||||
let full_height = size.height + double_padding;
|
||||
y += full_height;
|
||||
|
||||
remaining_height = remaining_height.saturating_sub(item.height);
|
||||
remaining_height = remaining_height.saturating_sub(full_height);
|
||||
maximum_item_width = maximum_item_width.max(size.width);
|
||||
}
|
||||
|
||||
let mut y = UPx::ZERO;
|
||||
for item in &mut self.items {
|
||||
let mounted = item.contents.mounted(context);
|
||||
context.set_child_layout(
|
||||
&mounted,
|
||||
Rect::new(
|
||||
Point::new(Px::ZERO, y.into_signed()),
|
||||
Size::new(maximum_item_width, item.height).into_signed(),
|
||||
),
|
||||
Point::new(double_padding, item.y),
|
||||
Size::new(maximum_item_width, item.height),
|
||||
)
|
||||
.into_signed(),
|
||||
);
|
||||
y += item.height;
|
||||
}
|
||||
|
||||
Size::new(maximum_item_width, y)
|
||||
Size::new(maximum_item_width + double_padding * 2 + submenu_space, y)
|
||||
}
|
||||
|
||||
fn hit_test(
|
||||
&mut self,
|
||||
_location: Point<Px>,
|
||||
_context: &mut crate::context::EventContext<'_>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hover(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
context: &mut crate::context::EventContext<'_>,
|
||||
) -> Option<kludgine::app::winit::window::CursorIcon> {
|
||||
self.handle_mouse_movement(location, context);
|
||||
self.shared.lock().hovering.insert(context.widget().id());
|
||||
None
|
||||
}
|
||||
|
||||
fn unhover(&mut self, context: &mut EventContext<'_>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: crate::window::DeviceId,
|
||||
_button: kludgine::app::winit::event::MouseButton,
|
||||
context: &mut crate::context::EventContext<'_>,
|
||||
) -> EventHandling {
|
||||
self.mouse_down = true;
|
||||
self.handle_mouse_movement(location, context);
|
||||
|
||||
HANDLED
|
||||
}
|
||||
|
||||
fn mouse_drag(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: crate::window::DeviceId,
|
||||
_button: kludgine::app::winit::event::MouseButton,
|
||||
context: &mut crate::context::EventContext<'_>,
|
||||
) {
|
||||
self.handle_mouse_movement(location, context);
|
||||
}
|
||||
|
||||
fn mouse_up(
|
||||
&mut self,
|
||||
_location: Option<Point<Px>>,
|
||||
_device_id: crate::window::DeviceId,
|
||||
_button: kludgine::app::winit::event::MouseButton,
|
||||
_context: &mut crate::context::EventContext<'_>,
|
||||
) {
|
||||
if let Some(index) = self.selecting {
|
||||
self.on_click.0.lock().invoke(ChosenMenuItem {
|
||||
item: self.items[index].value.clone(),
|
||||
click: None,
|
||||
});
|
||||
let mut shared = self.shared.lock();
|
||||
for handle in shared.open_menus.drain() {
|
||||
handle.dismiss();
|
||||
}
|
||||
}
|
||||
self.mouse_down = false;
|
||||
}
|
||||
|
||||
fn accept_focus(&mut self, _context: &mut crate::context::EventContext<'_>) -> bool {
|
||||
|
|
@ -351,9 +685,49 @@ impl Widget for OpenMenu {
|
|||
|
||||
fn mounted(&mut self, context: &mut crate::context::EventContext<'_>) {
|
||||
context.focus();
|
||||
|
||||
let colors = Button::colors_for_transparent(VisualState::Normal, context);
|
||||
for item in &mut self.items {
|
||||
item.colors = Some(Dynamic::new(colors));
|
||||
}
|
||||
}
|
||||
|
||||
fn blur(&mut self, _context: &mut crate::context::EventContext<'_>) {
|
||||
self.handle.dismiss();
|
||||
if self.open_submenu.is_none() {
|
||||
let mut shared = self.shared.lock();
|
||||
if let Some(index) = shared.open_menus.index_of_id(self.open_id) {
|
||||
while shared.open_menus.len() > index {
|
||||
let Some(handle) = shared.open_menus.pop() else {
|
||||
unreachable!()
|
||||
};
|
||||
handle.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenItem<T> {
|
||||
value: T,
|
||||
contents: WidgetRef,
|
||||
submenu: Option<Arc<dyn SubmenuFactory>>,
|
||||
y: UPx,
|
||||
height: UPx,
|
||||
colors: Option<Dynamic<ButtonColors>>,
|
||||
color_animation: AnimationHandle,
|
||||
state: VisualState,
|
||||
}
|
||||
|
||||
impl<T> Debug for OpenItem<T>
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OpenItem")
|
||||
.field("value", &self.value)
|
||||
.field("contents", &self.contents)
|
||||
.field("submenu", &self.submenu.is_some())
|
||||
.field("height", &self.height)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1596,19 +1596,6 @@ where
|
|||
);
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
EventContext::new(
|
||||
WidgetContext::new(
|
||||
self.root.clone(),
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
&mut self.fonts,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
.clear_focus();
|
||||
|
||||
if let (ElementState::Pressed, Some(location), Some(hovered)) = (
|
||||
state,
|
||||
self.cursor.location,
|
||||
|
|
@ -1640,6 +1627,19 @@ where
|
|||
.insert(button, handler.id());
|
||||
return HANDLED;
|
||||
}
|
||||
} else {
|
||||
EventContext::new(
|
||||
WidgetContext::new(
|
||||
self.root.clone(),
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
&mut self.fonts,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
.clear_focus();
|
||||
}
|
||||
IGNORED
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue