use alloc::string::String;
use alloc::vec::Vec;
use denise::Pen;
use denise::{
Color, ElementState, InputEvent, KeyCode, Point, PointerButton, Radius, Rect, Role, Size, Theme,
};
use denise_text::{TextEngine, TextStyle};
use crate::motion::Wake;
use crate::overlay::{Side, anchored};
use crate::widget::{
Animation, Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, Widget,
};
use super::style::hovered_row;
use crate::widgets::describe::{
Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, ROLES, Value,
};
const PAD: i32 = 10;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MenuItem {
pub label: String,
pub shortcut: String,
pub checked: bool,
pub enabled: bool,
pub separator: bool,
pub items: Vec<MenuItem>,
}
impl MenuItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
shortcut: String::new(),
checked: false,
enabled: true,
separator: false,
items: Vec::new(),
}
}
pub fn heading(label: impl Into<String>) -> Self {
Self {
enabled: false,
..Self::new(label)
}
}
pub fn separator() -> Self {
Self {
separator: true,
enabled: false,
..Self::new("")
}
}
pub fn submenu(label: impl Into<String>, items: impl IntoIterator<Item = MenuItem>) -> Self {
Self {
items: items.into_iter().collect(),
..Self::new(label)
}
}
fn selectable(&self) -> bool {
self.enabled && !self.separator
}
#[must_use]
pub fn with_shortcut(mut self, keys: &str) -> Self {
self.shortcut = shortcut(keys);
self
}
#[must_use]
pub fn checked(mut self, on: bool) -> Self {
self.checked = on;
self
}
#[must_use]
pub fn enabled(mut self, on: bool) -> Self {
self.enabled = on;
self
}
#[must_use]
pub fn disabled(self) -> Self {
self.enabled(false)
}
}
pub fn shortcut(keys: &str) -> String {
if cfg!(target_os = "macos") {
keys.split('+')
.map(|part| match part {
"Cmd" | "Ctrl" | "Super" => "\u{2318}",
"Shift" => "\u{21e7}",
"Alt" | "Option" => "\u{2325}",
other => other,
})
.collect()
} else {
keys.replace("Cmd", "Ctrl").replace("Option", "Alt")
}
}
pub struct MenuBar<M> {
titles: Vec<String>,
message: Option<fn(usize) -> M>,
open: Option<usize>,
hovered: Option<usize>,
role: Role,
style: TextStyle,
}
impl<M: 'static> MenuBar<M> {
pub fn new(
titles: impl IntoIterator<Item = impl Into<String>>,
message: fn(usize) -> M,
) -> Self {
Self {
titles: titles.into_iter().map(Into::into).collect(),
message: Some(message),
open: None,
hovered: None,
role: Role::Primary,
style: TextStyle::built_in(16),
}
}
pub fn inert(titles: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
message: None,
..Self::new(titles, |_| unreachable!("inert bars have no message"))
}
}
#[must_use]
pub fn with_role(mut self, role: Role) -> Self {
self.role = role;
self
}
#[must_use]
pub fn with_style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_size(mut self, size_px: u16) -> Self {
self.style.size_px = size_px;
self
}
pub fn titles(&self) -> &[String] {
&self.titles
}
pub fn set_titles(&mut self, titles: impl IntoIterator<Item = impl Into<String>>) {
self.titles = titles.into_iter().map(Into::into).collect();
if self.open.is_some_and(|i| i >= self.titles.len()) {
self.open = None;
}
}
pub fn style(&self) -> TextStyle {
self.style
}
pub fn set_open(&mut self, index: Option<usize>) {
self.open = index;
}
pub fn open(&self) -> Option<usize> {
self.open
}
pub fn preferred_height(&self, theme: &Theme, text: &mut TextEngine) -> i32 {
text.metrics(self.style).line_height() + theme.metrics.border * 2 + PAD
}
}
pub fn title_layout(
titles: &[String],
bounds: Rect,
style: TextStyle,
text: &mut TextEngine,
) -> Vec<Rect> {
let mut out = Vec::with_capacity(titles.len());
let mut x = bounds.x;
for title in titles {
let width = text.measure_line(style, title) + PAD * 2;
out.push(Rect::new(x, bounds.y, width, bounds.height));
x += width;
}
out
}
impl<M: 'static> Widget<M> for MenuBar<M> {
fn accepts_pointer(&self) -> bool {
self.message.is_some()
}
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
let theme = ctx.theme;
let bounds = ctx.bounds;
canvas.fill_rect(bounds, theme.color(Role::Base200));
let rects = title_layout(&self.titles, bounds, self.style, ctx.text);
let metrics = ctx.text.metrics(self.style);
let baseline = bounds.y + (bounds.height - metrics.line_height()) / 2 + metrics.ascent;
let radius = theme.radius(denise::Radius::Selector);
for (i, rect) in rects.iter().enumerate() {
let open = self.open == Some(i);
let hovered = self.hovered == Some(i) && !open;
let fg = if open {
canvas.fill_rounded_rect(*rect, radius, theme.color(self.role));
theme.content_of(self.role)
} else {
if hovered {
canvas.fill_rounded_rect(*rect, radius, theme.color(Role::Base300));
}
theme.color(Role::BaseContent)
};
ctx.text.draw_line(
canvas,
self.style,
Point::new(rect.x + PAD, baseline),
&self.titles[i],
fg,
);
}
}
fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
let width: i32 = self
.titles
.iter()
.map(|t| ctx.text.measure_line(self.style, t) + PAD * 2)
.sum();
Measured {
width: Some(width),
height: Some(self.preferred_height(ctx.theme, ctx.text)),
}
}
fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
let Some(message) = self.message else {
return Handled::No;
};
let Event::Input(input) = event else {
return Handled::No;
};
match input {
InputEvent::PointerMoved { position } => {
let hovered = self.hit(ctx.bounds, ctx.text, *position);
if hovered != self.hovered {
self.hovered = hovered;
return Handled::Yes;
}
Handled::No
}
InputEvent::PointerLeft => {
if self.hovered.take().is_some() {
return Handled::Yes;
}
Handled::No
}
InputEvent::PointerButton {
button: PointerButton::Left,
state: ElementState::Down,
position,
..
} => match self.hit(ctx.bounds, ctx.text, *position) {
Some(index) => {
ctx.emit(message(index));
Handled::Yes
}
None => Handled::No,
},
_ => Handled::No,
}
}
fn describe(&self) -> Option<&dyn DynDescribe> {
Some(self)
}
fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
Some(self)
}
}
impl<M: 'static> MenuBar<M> {
fn hit(&self, bounds: Rect, text: &mut TextEngine, p: Point) -> Option<usize> {
title_layout(&self.titles, bounds, self.style, text)
.into_iter()
.position(|r| r.contains(p))
}
}
impl<M> Describe for MenuBar<M> {
const KIND: &'static str = "menubar";
const DOC: &'static str = "A row of menu titles along the top of a window.";
const GROUP: Group = Group::Container;
const ICON: &'static denise::icon::Icon = &super::icons::MENU_BAR;
const PROPERTIES: &'static [Property] = &[
Property::new(
"title",
PropertyKind::List,
"The menu names, as `title` child nodes.",
),
Property::new(
"on-open",
PropertyKind::Message(Payload::Index),
"Emitted with the index of the title that was pressed. The application decides what opens.",
),
Property::new(
"role",
PropertyKind::Enum(ROLES),
"Colour of the open title's highlight.",
),
Property::new(
"size",
PropertyKind::Int { min: 6, max: 96 },
"Text size in logical pixels.",
)
.in_pixels(),
];
fn get(&self, name: &str) -> Option<Value> {
Some(match name {
"role" => Value::role(self.role),
"size" => Value::Int(i32::from(self.style.size_px)),
_ => return None,
})
}
fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
match name {
"role" => self.role = value.as_role()?,
"size" => self.style.size_px = value.as_size()?,
"title" | "on-open" => return Err(Mismatch::Supplied),
_ => return Err(Mismatch::Unknown),
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MenuEvent {
Picked(usize),
Title(usize),
Dismissed,
}
pub fn open_menu<M: Clone + 'static>(
ui: &mut crate::Ui<M>,
bar: crate::NodeId,
index: usize,
items: &[MenuItem],
message: fn(MenuEvent) -> M,
) -> Option<crate::NodeId> {
let widget = ui.widget::<MenuBar<M>>(bar)?;
let titles = widget.titles().to_vec();
let style = widget.style();
let bounds = ui.bounds(bar)?;
let rects = title_layout(&titles, bounds, style, ui.text_mut());
let at = *rects.get(index)?;
open(
ui,
bar,
at,
Side::Below,
items,
style,
message,
rects,
Some(index),
)
}
pub fn open_menu_at<M: Clone + 'static>(
ui: &mut crate::Ui<M>,
anchor: crate::NodeId,
at: Rect,
items: &[MenuItem],
style: TextStyle,
message: fn(MenuEvent) -> M,
) -> Option<crate::NodeId> {
open(
ui,
anchor,
at,
Side::Below,
items,
style,
message,
Vec::new(),
None,
)
}
#[allow(clippy::too_many_arguments)]
fn open<M: Clone + 'static>(
ui: &mut crate::Ui<M>,
anchor: crate::NodeId,
at: Rect,
side: Side,
items: &[MenuItem],
style: TextStyle,
message: fn(MenuEvent) -> M,
titles: Vec<Rect>,
open_title: Option<usize>,
) -> Option<crate::NodeId> {
if items.is_empty() {
return None;
}
let surface = ui.size();
let theme = *ui.theme();
let geometry = Geometry::of(style, &theme, ui.text_mut());
let size = geometry.panel_size(items, style, ui.text_mut());
let rect = anchored(surface, at, size, side, 0);
let container = ui.push_popup_at(anchor, Rect::ZERO, surface, Side::Below)?;
let menu = ui.add(
container,
Menu {
items: items.to_vec(),
style,
message,
titles,
open_title,
announced: None,
panels: alloc::vec![Panel {
path: Vec::new(),
rect,
hovered: None,
selected: None,
}],
pending: None,
geometry,
},
Rect::from_size(surface),
)?;
ui.focus(Some(menu));
Some(menu)
}
const CHECK: &str = "\u{2713}";
const FOLLOW_MS: u64 = 140;
#[derive(Clone, Copy, Debug)]
struct Geometry {
row_h: i32,
sep_h: i32,
pad_y: i32,
inset: i32,
pad_x: i32,
tick_w: i32,
keys_gap: i32,
chevron_w: i32,
radius_panel: i32,
radius_row: i32,
border: i32,
ascent: i32,
line_h: i32,
}
impl Geometry {
fn of(style: TextStyle, theme: &Theme, text: &mut TextEngine) -> Self {
let metrics = text.metrics(style);
let s = i32::from(style.size_px).max(8);
let pad_y = (s * 2 / 5).max(3);
Self {
row_h: metrics.line_height() + (s * 11 / 20).max(4),
sep_h: (s * 3 / 5).max(5),
pad_y,
inset: pad_y,
pad_x: (s * 4 / 5).max(6),
tick_w: text.measure_line(style, CHECK) + (s / 2).max(3),
keys_gap: s * 2,
chevron_w: (s * 3 / 5).max(4) + (s / 2).max(3),
radius_panel: theme.radius(Radius::Box).max(s / 3),
radius_row: (s * 2 / 5).max(2),
border: theme.metrics.border.max(1),
ascent: metrics.ascent,
line_h: metrics.line_height(),
}
}
fn row_height(&self, item: &MenuItem) -> i32 {
if item.separator {
self.sep_h
} else {
self.row_h
}
}
fn panel_size(&self, items: &[MenuItem], style: TextStyle, text: &mut TextEngine) -> Size {
let s = i32::from(style.size_px).max(8);
let content = items
.iter()
.map(|item| {
let label = text.measure_line(style, &item.label);
let keys = if item.shortcut.is_empty() {
0
} else {
self.keys_gap + text.measure_line(style, &item.shortcut)
};
let chevron = if item.items.is_empty() {
0
} else {
self.chevron_w
};
label + keys.max(chevron)
})
.max()
.unwrap_or(0)
.max(s * 8);
let width = self.inset * 2 + self.pad_x * 2 + self.tick_w + content;
let height = self.pad_y * 2 + items.iter().map(|i| self.row_height(i)).sum::<i32>();
Size::new(width as u32, height as u32)
}
fn row_rect(&self, rect: Rect, items: &[MenuItem], index: usize) -> Rect {
let mut y = rect.y + self.pad_y;
for item in &items[..index] {
y += self.row_height(item);
}
Rect::new(
rect.x + self.inset,
y,
rect.width - self.inset * 2,
self.row_height(&items[index]),
)
}
fn row_at(&self, rect: Rect, items: &[MenuItem], p: Point) -> Option<usize> {
if !rect.contains(p) {
return None;
}
let mut y = rect.y + self.pad_y;
for (i, item) in items.iter().enumerate() {
let h = self.row_height(item);
if p.y >= y && p.y < y + h {
return Some(i);
}
y += h;
}
None
}
}
#[derive(Clone, Debug)]
struct Panel {
path: Vec<usize>,
rect: Rect,
hovered: Option<usize>,
selected: Option<usize>,
}
pub struct Menu<M> {
items: Vec<MenuItem>,
style: TextStyle,
message: fn(MenuEvent) -> M,
titles: Vec<Rect>,
open_title: Option<usize>,
announced: Option<usize>,
panels: Vec<Panel>,
pending: Option<Pending>,
geometry: Geometry,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Pending {
panel: usize,
row: usize,
since: u64,
}
fn items_at<'a>(root: &'a [MenuItem], path: &[usize]) -> &'a [MenuItem] {
let mut items = root;
for &i in path {
items = &items[i].items;
}
items
}
fn count(items: &[MenuItem]) -> usize {
items.iter().map(|item| 1 + count(&item.items)).sum()
}
fn flat_index(items: &[MenuItem], path: &[usize], row: usize) -> usize {
match path.split_first() {
None => count(&items[..row]),
Some((&first, rest)) => {
count(&items[..first]) + 1 + flat_index(&items[first].items, rest, row)
}
}
}
impl<M: 'static> Menu<M> {
pub fn panels(&self) -> impl Iterator<Item = Rect> + '_ {
self.panels.iter().map(|p| p.rect)
}
pub fn row_rect(&self, panel: usize, row: usize) -> Option<Rect> {
let p = self.panels.get(panel)?;
let items = items_at(&self.items, &p.path);
(row < items.len()).then(|| self.geometry.row_rect(p.rect, items, row))
}
fn panel_items(&self, panel: usize) -> &[MenuItem] {
items_at(&self.items, &self.panels[panel].path)
}
fn hit(&self, p: Point) -> Option<(usize, Option<usize>)> {
(0..self.panels.len()).rev().find_map(|k| {
let panel = &self.panels[k];
panel
.rect
.contains(p)
.then(|| (k, self.geometry.row_at(panel.rect, self.panel_items(k), p)))
})
}
fn open_submenu(&mut self, k: usize, row: usize, text: &mut TextEngine, surface: Size) {
self.panels.truncate(k + 1);
let parent = self.panels[k].rect;
let items = self.panel_items(k);
let item = &items[row];
if item.items.is_empty() || !item.selectable() {
return;
}
let g = self.geometry;
let row_rect = g.row_rect(parent, items, row);
let size = g.panel_size(&item.items, self.style, text);
let (w, h) = (size.width as i32, size.height as i32);
let mut x = parent.right() - g.inset;
if x + w > surface.width as i32 && parent.x - w + g.inset >= 0 {
x = parent.x - w + g.inset;
}
let x = x.clamp(0, (surface.width as i32 - w).max(0));
let y = (row_rect.y - g.pad_y).clamp(0, (surface.height as i32 - h).max(0));
let mut path = self.panels[k].path.clone();
path.push(row);
self.panels[k].hovered = Some(row);
self.panels.push(Panel {
path,
rect: Rect::new(x, y, w, h),
hovered: None,
selected: None,
});
}
fn submenu_of(&self, k: usize, row: usize) -> bool {
self.panels
.get(k + 1)
.is_some_and(|child| child.path.last() == Some(&row))
}
fn title_at(&self, p: Point) -> Option<usize> {
self.titles.iter().position(|r| r.contains(p))
}
fn step(items: &[MenuItem], from: Option<usize>, step: isize) -> Option<usize> {
let n = items.len() as isize;
if n == 0 {
return None;
}
let mut at = from.map_or(if step > 0 { -1 } else { n }, |i| i as isize);
for _ in 0..n {
at = (at + step).rem_euclid(n);
if items[at as usize].selectable() {
return Some(at as usize);
}
}
None
}
fn unhover_except(&mut self, keep: Option<usize>) -> bool {
let mut changed = false;
for (i, panel) in self.panels.iter_mut().enumerate() {
if Some(i) != keep {
changed |= panel.hovered.take().is_some();
}
}
changed
}
fn pointer_moved(&mut self, p: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
if let Some(title) = self.title_at(p)
&& Some(title) != self.open_title
{
if self.announced != Some(title) {
self.announced = Some(title);
ctx.emit((self.message)(MenuEvent::Title(title)));
}
return Handled::Yes;
}
self.announced = None;
let Some((k, row)) = self.hit(p) else {
let changed = self.unhover_except(None);
return if changed { Handled::Yes } else { Handled::No };
};
let (row, opens) = {
let items = self.panel_items(k);
let row = row.filter(|&r| items[r].selectable());
(row, row.is_some_and(|r| !items[r].items.is_empty()))
};
let mut changed = self.unhover_except(Some(k));
if self.panels[k].hovered != row {
self.panels[k].hovered = row;
changed = true;
}
match row {
Some(r) if self.submenu_of(k, r) => {
self.pending = None;
}
Some(r) if opens => {
let surface = Size::new(ctx.bounds.width as u32, ctx.bounds.height as u32);
self.open_submenu(k, r, ctx.text, surface);
self.pending = None;
changed = true;
}
Some(r) if self.panels.len() > k + 1 => {
if self.pending.map(|p| (p.panel, p.row)) != Some((k, r)) {
self.pending = Some(Pending {
panel: k,
row: r,
since: ctx.now_ms,
});
}
ctx.request_animation();
}
_ => self.pending = None,
}
if changed { Handled::Yes } else { Handled::No }
}
fn released(&mut self, p: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
let Some((k, Some(row))) = self.hit(p) else {
return Handled::No;
};
let items = self.panel_items(k);
let item = &items[row];
if !item.selectable() {
return Handled::No;
}
if !item.items.is_empty() {
if !self.submenu_of(k, row) {
let surface = Size::new(ctx.bounds.width as u32, ctx.bounds.height as u32);
self.open_submenu(k, row, ctx.text, surface);
}
self.pending = None;
return Handled::Yes;
}
let index = flat_index(&self.items, &self.panels[k].path, row);
ctx.emit((self.message)(MenuEvent::Picked(index)));
Handled::Yes
}
fn pressed(&mut self, p: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
if self.hit(p).is_some() {
return Handled::Yes;
}
match self.title_at(p) {
Some(title) if Some(title) != self.open_title => {
ctx.emit((self.message)(MenuEvent::Title(title)));
}
_ => ctx.emit((self.message)(MenuEvent::Dismissed)),
}
Handled::Yes
}
fn key(&mut self, code: KeyCode, ctx: &mut EventCtx<'_, M>) -> Handled {
let deepest = self.panels.len() - 1;
let items = self.panel_items(deepest);
let selected = self.panels[deepest].selected;
match code {
KeyCode::ArrowDown => {
self.panels[deepest].selected = Self::step(items, selected, 1);
self.panels[deepest].hovered = None;
Handled::Yes
}
KeyCode::ArrowUp => {
self.panels[deepest].selected = Self::step(items, selected, -1);
self.panels[deepest].hovered = None;
Handled::Yes
}
KeyCode::ArrowRight => {
match selected {
Some(row) if !items[row].items.is_empty() => {
let surface = Size::new(ctx.bounds.width as u32, ctx.bounds.height as u32);
self.open_submenu(deepest, row, ctx.text, surface);
let last = self.panels.len() - 1;
let child = self.panel_items(last);
self.panels[last].selected = Self::step(child, None, 1);
}
_ => {
if let Some(open) = self.open_title
&& !self.titles.is_empty()
{
let next = (open + 1) % self.titles.len();
ctx.emit((self.message)(MenuEvent::Title(next)));
}
}
}
Handled::Yes
}
KeyCode::ArrowLeft => {
if deepest > 0 {
self.panels.truncate(deepest);
} else if let Some(open) = self.open_title
&& !self.titles.is_empty()
{
let prev = (open + self.titles.len() - 1) % self.titles.len();
ctx.emit((self.message)(MenuEvent::Title(prev)));
}
Handled::Yes
}
KeyCode::Enter | KeyCode::NumpadEnter => {
if let Some(row) = selected {
if !items[row].items.is_empty() {
let surface = Size::new(ctx.bounds.width as u32, ctx.bounds.height as u32);
self.open_submenu(deepest, row, ctx.text, surface);
} else {
let index = flat_index(&self.items, &self.panels[deepest].path, row);
ctx.emit((self.message)(MenuEvent::Picked(index)));
}
}
Handled::Yes
}
_ => Handled::No,
}
}
fn paint_panel(&self, k: usize, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
let theme = ctx.theme;
let g = self.geometry;
let panel = &self.panels[k];
let rect = panel.rect;
let items = self.panel_items(k);
let s = i32::from(self.style.size_px).max(8);
let drop = (s / 6).max(2);
canvas.fill_rounded_rect(
rect.translate(0, drop).inflate(1),
g.radius_panel + 1,
Color::rgba(0, 0, 0, 28),
);
canvas.fill_rounded_rect(
rect.translate(0, drop / 2),
g.radius_panel,
Color::rgba(0, 0, 0, 40),
);
canvas.fill_rounded_rect(rect, g.radius_panel, theme.color(Role::Base200));
canvas.stroke_rounded_rect(rect, g.radius_panel, g.border, theme.color(Role::Base300));
let fg_normal = theme.color(Role::BaseContent);
let dim = fg_normal.mix(theme.color(Role::Base200), 140);
let rule = theme.color(Role::Base300).mix(fg_normal, 40);
let tick_gap = g.tick_w - ctx.text.measure_line(self.style, CHECK);
for (i, item) in items.iter().enumerate() {
let row = g.row_rect(rect, items, i);
if item.separator {
let y = row.y + row.height / 2;
canvas.fill_rect(
Rect::new(row.x + g.pad_x / 2, y, row.width - g.pad_x, g.border),
rule,
);
continue;
}
let hovered = hovered_row(ctx.state, panel.hovered);
let highlighted = item.selectable()
&& (panel.selected == Some(i) || hovered == Some(i) || self.submenu_of(k, i));
let fg = if highlighted {
canvas.fill_rounded_rect(row, g.radius_row, theme.color(Role::Primary));
theme.content_of(Role::Primary)
} else if item.enabled {
fg_normal
} else {
dim
};
let baseline = row.y + (row.height - g.line_h) / 2 + g.ascent;
let x = row.x + g.pad_x;
if item.checked {
ctx.text
.draw_line(canvas, self.style, Point::new(x, baseline), CHECK, fg);
}
ctx.text.draw_line(
canvas,
self.style,
Point::new(x + g.tick_w, baseline),
&item.label,
fg,
);
let right = row.right() - g.pad_x;
if !item.items.is_empty() {
let w = (g.chevron_w - tick_gap).max(3);
let h = (w * 5 / 4).max(4);
let cy = row.y + row.height / 2;
let x0 = right - w;
let fx = |x: i32, y: i32| (x * 256, y * 256);
canvas.fill_polygon_fx(
&[fx(x0, cy - h / 2), fx(x0 + w, cy), fx(x0, cy + h / 2)],
if highlighted { fg } else { dim },
);
} else if !item.shortcut.is_empty() {
let keys = ctx.text.measure_line(self.style, &item.shortcut);
ctx.text.draw_line(
canvas,
self.style,
Point::new(right - keys, baseline),
&item.shortcut,
if highlighted { fg } else { dim },
);
}
}
}
}
impl<M: 'static> Widget<M> for Menu<M> {
fn accepts_pointer(&self) -> bool {
true
}
fn focusable(&self) -> bool {
true
}
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
for k in 0..self.panels.len() {
self.paint_panel(k, ctx, canvas);
}
}
fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
let Event::Input(input) = event else {
return Handled::No;
};
match input {
InputEvent::PointerMoved { position } => self.pointer_moved(*position, ctx),
InputEvent::PointerButton {
button: PointerButton::Left,
state: ElementState::Down,
position,
..
} => self.pressed(*position, ctx),
InputEvent::PointerButton {
button: PointerButton::Left,
state: ElementState::Up,
position,
..
} => self.released(*position, ctx),
InputEvent::PointerButton {
state: ElementState::Down,
position,
..
} => {
if self.hit(*position).is_none() {
ctx.emit((self.message)(MenuEvent::Dismissed));
}
Handled::Yes
}
InputEvent::Key {
code,
state: ElementState::Down,
..
} => self.key(*code, ctx),
_ => Handled::No,
}
}
fn animate(&mut self, now_ms: u64) -> Animation {
let Some(pending) = self.pending else {
return Animation::NONE;
};
if now_ms.saturating_sub(pending.since) < FOLLOW_MS {
return Animation {
repaint: false,
next: Wake::At(pending.since + FOLLOW_MS),
};
}
self.pending = None;
if pending.panel < self.panels.len() {
self.panels.truncate(pending.panel + 1);
}
Animation {
repaint: true,
next: Wake::Never,
}
}
}