mirror of
https://github.com/danbulant/cushy
synced 2026-07-08 12:40:36 +00:00
Basic context menus
Missing a ton of functionality (separators, keyboard accessibility, submenus), but the basic concept is working.
This commit is contained in:
parent
1c8d4e0176
commit
8b96966031
6 changed files with 459 additions and 31 deletions
40
examples/menu.rs
Normal file
40
examples/menu.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use cushy::widget::MakeWidget;
|
||||||
|
use cushy::widgets::layers::{OverlayLayer, Overlayable};
|
||||||
|
use cushy::widgets::menu::{Menu, MenuItem};
|
||||||
|
use cushy::Run;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum MenuOptions {
|
||||||
|
First,
|
||||||
|
Second,
|
||||||
|
Third,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> cushy::Result {
|
||||||
|
let overlay = OverlayLayer::default();
|
||||||
|
|
||||||
|
"Click Me"
|
||||||
|
.into_button()
|
||||||
|
.on_click({
|
||||||
|
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:?}");
|
||||||
|
})
|
||||||
|
.overlay_in(&overlay)
|
||||||
|
.at(click.window_location)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.centered()
|
||||||
|
.expand()
|
||||||
|
.and(overlay)
|
||||||
|
.into_layers()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use cushy::widget::{MakeWidget, MakeWidgetWithTag, WidgetTag};
|
use cushy::widget::{MakeWidget, MakeWidgetWithTag, WidgetTag};
|
||||||
use cushy::widgets::container::ContainerShadow;
|
use cushy::widgets::container::ContainerShadow;
|
||||||
use cushy::widgets::layers::{OverlayBuilder, OverlayLayer};
|
use cushy::widgets::layers::{OverlayBuilder, OverlayLayer, Overlayable};
|
||||||
use cushy::Run;
|
use cushy::Run;
|
||||||
use figures::units::Lp;
|
use figures::units::Lp;
|
||||||
use figures::{Point, Zero};
|
use figures::{Point, Zero};
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ define_components! {
|
||||||
OpaqueWidgetColor(Color, "opaque_color", .surface.opaque_widget)
|
OpaqueWidgetColor(Color, "opaque_color", .surface.opaque_widget)
|
||||||
/// A set of radius descriptions for how much roundness to apply to the
|
/// A set of radius descriptions for how much roundness to apply to the
|
||||||
/// shapes of widgets.
|
/// shapes of widgets.
|
||||||
CornerRadius(CornerRadii<Dimension>, "corner_radius", CornerRadii::from(Dimension::Lp(Lp::points(10))))
|
CornerRadius(CornerRadii<Dimension>, "corner_radius", CornerRadii::from(Dimension::Lp(Lp::points(6))))
|
||||||
/// The font family to render text using.
|
/// The font family to render text using.
|
||||||
FontFamily(FontFamilyList, "font_family", FontFamilyList::from(FamilyOwned::SansSerif))
|
FontFamily(FontFamilyList, "font_family", FontFamilyList::from(FamilyOwned::SansSerif))
|
||||||
/// The font (boldness) weight to apply to text rendering.
|
/// The font (boldness) weight to apply to text rendering.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod layers;
|
pub mod layers;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
pub mod menu;
|
||||||
mod mode_switch;
|
mod mode_switch;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod radio;
|
pub mod radio;
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,48 @@ impl OverlayState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A type that is being prepared to be shown in an [`OverlayLayer`].
|
||||||
|
pub trait Overlayable: Sized {
|
||||||
|
/// The resulting handle type when this overlay is shown.
|
||||||
|
type Handle;
|
||||||
|
|
||||||
|
/// Sets this overlay to hide automatically when it or its relative widget
|
||||||
|
/// are no longer hovered by the mouse cursor.
|
||||||
|
#[must_use]
|
||||||
|
fn hide_on_unhover(self) -> Self;
|
||||||
|
|
||||||
|
/// Show this overlay to the left of the specified widget.
|
||||||
|
#[must_use]
|
||||||
|
fn left_of(self, id: WidgetId) -> Self;
|
||||||
|
|
||||||
|
/// Show this overlay to the right of the specified widget.
|
||||||
|
#[must_use]
|
||||||
|
fn right_of(self, id: WidgetId) -> Self;
|
||||||
|
|
||||||
|
/// Show this overlay to show below the specified widget.
|
||||||
|
#[must_use]
|
||||||
|
fn below(self, id: WidgetId) -> Self;
|
||||||
|
|
||||||
|
/// Show this overlay to show above the specified widget.
|
||||||
|
#[must_use]
|
||||||
|
fn above(self, id: WidgetId) -> Self;
|
||||||
|
|
||||||
|
/// Shows this overlay near `id` off to the `direction` side.
|
||||||
|
#[must_use]
|
||||||
|
fn near(self, id: WidgetId, direction: Direction) -> Self;
|
||||||
|
|
||||||
|
/// Shows this overlay at a specified window `location`.
|
||||||
|
#[must_use]
|
||||||
|
fn at(self, location: Point<Px>) -> Self;
|
||||||
|
|
||||||
|
/// Sets `callback` to be invoked once this overlay is dismissed.
|
||||||
|
#[must_use]
|
||||||
|
fn on_dismiss(self, callback: Callback) -> Self;
|
||||||
|
|
||||||
|
/// Shows this overlay, returning a handle that to the displayed overlay.
|
||||||
|
fn show(self) -> Self::Handle;
|
||||||
|
}
|
||||||
|
|
||||||
/// A builder for overlaying a widget on an [`OverlayLayer`].
|
/// A builder for overlaying a widget on an [`OverlayLayer`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OverlayBuilder<'a> {
|
pub struct OverlayBuilder<'a> {
|
||||||
|
|
@ -581,64 +623,47 @@ pub struct OverlayBuilder<'a> {
|
||||||
layout: OverlayLayout,
|
layout: OverlayLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OverlayBuilder<'_> {
|
impl Overlayable for OverlayBuilder<'_> {
|
||||||
/// Sets this overlay to hide automatically when it or its relative widget
|
type Handle = OverlayHandle;
|
||||||
/// are no longer hovered by the mouse cursor.
|
|
||||||
#[must_use]
|
fn hide_on_unhover(mut self) -> Self {
|
||||||
pub fn hide_on_unhover(mut self) -> Self {
|
|
||||||
self.layout.requires_hover = true;
|
self.layout.requires_hover = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show this overlay to the left of the specified widget.
|
fn left_of(self, id: WidgetId) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn left_of(self, id: WidgetId) -> Self {
|
|
||||||
self.near(id, Direction::Left)
|
self.near(id, Direction::Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show this overlay to the right of the specified widget.
|
fn right_of(self, id: WidgetId) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn right_of(self, id: WidgetId) -> Self {
|
|
||||||
self.near(id, Direction::Right)
|
self.near(id, Direction::Right)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show this overlay to show below the specified widget.
|
fn below(self, id: WidgetId) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn below(self, id: WidgetId) -> Self {
|
|
||||||
self.near(id, Direction::Down)
|
self.near(id, Direction::Down)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show this overlay to show above the specified widget.
|
fn above(self, id: WidgetId) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn above(self, id: WidgetId) -> Self {
|
|
||||||
self.near(id, Direction::Up)
|
self.near(id, Direction::Up)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows this overlay near `id` off to the `direction` side.
|
fn near(mut self, id: WidgetId, direction: Direction) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn near(mut self, id: WidgetId, direction: Direction) -> Self {
|
|
||||||
self.layout.relative_to = Some(id);
|
self.layout.relative_to = Some(id);
|
||||||
self.layout.positioning = Position::Relative(direction);
|
self.layout.positioning = Position::Relative(direction);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows this overlay at a specified window `location`.
|
fn at(mut self, location: Point<Px>) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn at(mut self, location: Point<Px>) -> Self {
|
|
||||||
self.layout.positioning = Position::At(location);
|
self.layout.positioning = Position::At(location);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets `callback` to be invoked once this overlay is dismissed.
|
fn on_dismiss(mut self, callback: Callback) -> Self {
|
||||||
#[must_use]
|
|
||||||
pub fn on_dismiss(mut self, callback: Callback) -> Self {
|
|
||||||
self.layout.on_dismiss = Some(Arc::new(Mutex::new(callback)));
|
self.layout.on_dismiss = Some(Arc::new(Mutex::new(callback)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows this overlay, returning a handle that to the displayed overlay.
|
fn show(self) -> Self::Handle {
|
||||||
#[must_use]
|
|
||||||
pub fn show(self) -> OverlayHandle {
|
|
||||||
self.fade_in();
|
self.fade_in();
|
||||||
self.overlay.state.map_mut(|mut state| {
|
self.overlay.state.map_mut(|mut state| {
|
||||||
state.new_overlays += 1;
|
state.new_overlays += 1;
|
||||||
|
|
@ -649,7 +674,9 @@ impl OverlayBuilder<'_> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverlayBuilder<'_> {
|
||||||
fn fade_in(&self) {
|
fn fade_in(&self) {
|
||||||
self.layout
|
self.layout
|
||||||
.opacity
|
.opacity
|
||||||
|
|
@ -746,6 +773,7 @@ impl Direction {
|
||||||
|
|
||||||
/// A handle to an overlay that was shown in an [`OverlayLayer`].
|
/// A handle to an overlay that was shown in an [`OverlayLayer`].
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
|
#[must_use = "Overlay handles will dismiss the shown overlay when dropped."]
|
||||||
pub struct OverlayHandle {
|
pub struct OverlayHandle {
|
||||||
state: Dynamic<OverlayState>,
|
state: Dynamic<OverlayState>,
|
||||||
id: LotId,
|
id: LotId,
|
||||||
|
|
|
||||||
359
src/widgets/menu.rs
Normal file
359
src/widgets/menu.rs
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
//! Overlay menu widgets.
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use figures::units::{Lp, Px, UPx};
|
||||||
|
use figures::{IntoSigned, Point, Rect, Size, Zero};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
use super::button::{ButtonClick, ButtonKind};
|
||||||
|
use super::container::ContainerShadow;
|
||||||
|
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 crate::ConstraintLimit;
|
||||||
|
|
||||||
|
/// An overlayable menu of selectable items.
|
||||||
|
///
|
||||||
|
/// This widget is designed to implement Cushy's contextual menu system. When
|
||||||
|
/// used with an [`OverlayLayer`], this widget can be shown above other widgets
|
||||||
|
/// or at a specific location.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Menu<T> {
|
||||||
|
items: Vec<MenuItem<T>>,
|
||||||
|
on_click: Option<Arc<Mutex<Callback<ChosenMenuItem<T>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Menu<T>
|
||||||
|
where
|
||||||
|
T: Debug + Send + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Menu<T>
|
||||||
|
where
|
||||||
|
T: Debug + Send + Clone + 'static,
|
||||||
|
{
|
||||||
|
/// Returns a new, empty menu.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Vec::new(),
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
height: UPx::ZERO,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
MenuOverlay(
|
||||||
|
overlay.build_overlay(
|
||||||
|
OpenMenu {
|
||||||
|
items,
|
||||||
|
handle: handle.clone(),
|
||||||
|
}
|
||||||
|
.contain()
|
||||||
|
.shadow(ContainerShadow::drop(Lp::mm(1), Lp::mm(2)))
|
||||||
|
.vertical_scroll(),
|
||||||
|
),
|
||||||
|
handle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Menu`] that is preparing to be shown in an [`OverlayLayer`].
|
||||||
|
pub struct MenuOverlay<'a>(OverlayBuilder<'a>, OpenMenuHandle);
|
||||||
|
|
||||||
|
impl<'a> Overlayable for MenuOverlay<'a> {
|
||||||
|
type Handle = OpenMenuHandle;
|
||||||
|
|
||||||
|
fn hide_on_unhover(self) -> Self {
|
||||||
|
Self(self.0.hide_on_unhover(), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left_of(self, id: crate::widget::WidgetId) -> Self {
|
||||||
|
Self(self.0.left_of(id), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right_of(self, id: crate::widget::WidgetId) -> Self {
|
||||||
|
Self(self.0.right_of(id), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn below(self, id: crate::widget::WidgetId) -> Self {
|
||||||
|
Self(self.0.below(id), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn above(self, id: crate::widget::WidgetId) -> Self {
|
||||||
|
Self(self.0.above(id), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn near(self, id: crate::widget::WidgetId, direction: super::layers::Direction) -> Self {
|
||||||
|
Self(self.0.near(id, direction), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at(self, location: Point<Px>) -> Self {
|
||||||
|
Self(self.0.at(location), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_dismiss(self, callback: Callback) -> Self {
|
||||||
|
Self(self.0.on_dismiss(callback), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show(self) -> Self::Handle {
|
||||||
|
let handle = self.0.show();
|
||||||
|
*self.1 .0.lock() = Some(handle);
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a [`Menu`] that was shown.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OpenMenuHandle(Dynamic<Option<OverlayHandle>>);
|
||||||
|
|
||||||
|
impl OpenMenuHandle {
|
||||||
|
/// Closes the menu, if it is still shown.
|
||||||
|
pub fn dismiss(&self) {
|
||||||
|
*self.0.lock() = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selected item of a shown [`Menu<T>`].
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub struct ChosenMenuItem<T> {
|
||||||
|
/// The item that was chosen.
|
||||||
|
pub item: T,
|
||||||
|
/// Information about the button click that caused this item to be chosen,
|
||||||
|
/// if present.
|
||||||
|
pub click: Option<ButtonClick>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder of a [`MenuItem<T>`].
|
||||||
|
pub struct MenuItemBuilder<T, Contents = ()> {
|
||||||
|
value: T,
|
||||||
|
contents: Contents,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MenuItemBuilder<T, ()> {
|
||||||
|
/// Sets the text of this menu item to `text` and returns self.
|
||||||
|
pub fn text(self, text: impl Into<String>) -> MenuItemBuilder<T, String> {
|
||||||
|
let Self {
|
||||||
|
value,
|
||||||
|
contents: (),
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
MenuItemBuilder {
|
||||||
|
value,
|
||||||
|
contents: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the contents of this menu item to `widget` and returns self.
|
||||||
|
pub fn widget(self, widget: impl MakeWidget) -> MenuItemBuilder<T, WidgetInstance> {
|
||||||
|
let Self {
|
||||||
|
value,
|
||||||
|
contents: (),
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
MenuItemBuilder {
|
||||||
|
value,
|
||||||
|
contents: widget.make_widget(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type that can be used inside of a [`MenuItemBuilder`] as a menu item's
|
||||||
|
/// contents.
|
||||||
|
pub trait MenuItemContents<T>: sealed::MenuItemContentsSealed<T> {}
|
||||||
|
|
||||||
|
mod sealed {
|
||||||
|
pub trait MenuItemContentsSealed<T> {
|
||||||
|
fn make_item(self, value: T) -> super::MenuItem<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
MenuItem {
|
||||||
|
value,
|
||||||
|
widget: self.make_widget(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> sealed::MenuItemContentsSealed<T> for WidgetInstance {
|
||||||
|
fn make_item(self, value: T) -> MenuItem<T> {
|
||||||
|
MenuItem {
|
||||||
|
value,
|
||||||
|
widget: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Contents> MenuItemBuilder<T, Contents>
|
||||||
|
where
|
||||||
|
Contents: MenuItemContents<T>,
|
||||||
|
{
|
||||||
|
/// Returns the finished menu item.
|
||||||
|
pub fn finish(self) -> MenuItem<T> {
|
||||||
|
self.contents.make_item(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Contents> From<MenuItemBuilder<T, Contents>> for MenuItem<T>
|
||||||
|
where
|
||||||
|
Contents: MenuItemContents<T>,
|
||||||
|
{
|
||||||
|
fn from(builder: MenuItemBuilder<T, Contents>) -> Self {
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An item in a [`Menu<T>`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MenuItem<T> {
|
||||||
|
value: T,
|
||||||
|
widget: WidgetInstance,
|
||||||
|
// submenu: Option<Menu<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MenuItem<T> {
|
||||||
|
/// Returns a new menu item with the given value and contents.
|
||||||
|
pub fn new(value: T, contents: impl MakeWidget) -> Self {
|
||||||
|
Self::build(value).widget(contents).finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a builder for a menu item with the given value.
|
||||||
|
pub fn build(value: T) -> MenuItemBuilder<T, ()> {
|
||||||
|
MenuItemBuilder {
|
||||||
|
value,
|
||||||
|
contents: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OpenMenu {
|
||||||
|
items: Vec<OpenItem>,
|
||||||
|
handle: OpenMenuHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OpenItem {
|
||||||
|
contents: WidgetRef,
|
||||||
|
height: UPx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for OpenMenu {
|
||||||
|
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
|
||||||
|
for item in &mut self.items {
|
||||||
|
let mounted = item.contents.mounted(context);
|
||||||
|
context.for_other(&mounted).redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<ConstraintLimit>,
|
||||||
|
context: &mut LayoutContext<'_, '_, '_, '_>,
|
||||||
|
) -> Size<UPx> {
|
||||||
|
let mut maximum_item_width = UPx::ZERO;
|
||||||
|
let mut remaining_height = available_space.height.max();
|
||||||
|
|
||||||
|
for item in &mut self.items {
|
||||||
|
let mounted = item.contents.mounted(context);
|
||||||
|
let size = context.for_other(&mounted).layout(Size::new(
|
||||||
|
ConstraintLimit::SizeToFit(available_space.width.max()),
|
||||||
|
ConstraintLimit::SizeToFit(remaining_height),
|
||||||
|
));
|
||||||
|
item.height = size.height;
|
||||||
|
|
||||||
|
remaining_height = remaining_height.saturating_sub(item.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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
y += item.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
Size::new(maximum_item_width, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_focus(&mut self, _context: &mut crate::context::EventContext<'_>) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mounted(&mut self, context: &mut crate::context::EventContext<'_>) {
|
||||||
|
context.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blur(&mut self, _context: &mut crate::context::EventContext<'_>) {
|
||||||
|
self.handle.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue