menu.rs
raw
//! The menu family: a bar of titles, the popups they open, and the rows
//! inside them.
//!
//! Five widgets, one shape. A `menu-bar` holds `menu` titles; a `menu` is a
//! title (in a bar) or a submenu row (in a menu), and owns the *deferred
//! content* of the popup it opens; a `menu-panel` is that popup, built by the
//! tree when the menu opens; `menu-item` and `menu-separator` are the rows.
//! The compiler enforces that shape (a descriptor's `required_parents`), which
//! is what lets a menu know at construction whether it is a title or a
//! submenu row rather than sniffing its parent at runtime.
//!
//! **Nothing here opens an overlay.** A widget cannot reach the tree, and
//! menus need the tree's overlay stack, its focus, and its structural
//! knowledge. So these widgets *report* — a clicked title emits
//! [`EventData::MenuOpen`], a panel's arrow key emits
//! [`EventData::MenuNav`] — and the tree acts. It is the same division a
//! button draws when its Enter becomes `EventData::Activated`.
//!
//! Highlight is not focus. The panel holds focus while it is open and moves a
//! highlighted row within itself, so arrowing through a menu costs no focus
//! traffic and leaves the focus to be restored when the menu closes. A row's
//! highlight is its own state, so it selects between token variants
//! (`background` vs `background-highlighted`) exactly as a checkbox selects
//! between `box-fill` and `box-fill-checked` — there is no `:highlighted`
//! theme selector, for the same reason there is no `:checked` one.
use guiduck_scene::Fragment;
use guiduck_scene::geom::{Point, Rect, Size};
use guiduck_scene::paint::{Brush, Color};
use parley::{Alignment, AlignmentOptions, Layout, StyleProperty};
use taffy::AvailableSpace;
use std::cell::Cell;
use std::rc::Rc;
use super::overlay::{AnchorSide, OverlayOptions};
use super::{CursorShape, Widget, WidgetId, WidgetTree};
use crate::content::ContentBuilder;
use crate::dirty::Dirt;
use crate::event::{EventData, EventKind, Key, KeyInput, PointerEvent};
use crate::graphic::{Graphic, GraphicPaint};
use crate::signals::Scope;
use crate::style::{ComputedStyle, StyleReader};
use crate::text::TextContext;
/// Which way a keystroke moves the highlight, or steps through the stack.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum MenuNav {
Previous,
Next,
First,
Last,
/// Enter: activate the highlighted row.
Activate,
/// Right: open the highlighted row's submenu, or step to the next title.
Deeper,
/// Left: close this level, or step to the previous title.
Shallower,
}
/// Inner padding of a menu row and of a bar title.
const ROW_PAD_X: f64 = 12.0;
const ROW_PAD_Y: f64 = 5.0;
/// Gap between a row's label and its accelerator text.
const ACCEL_GAP: f64 = 24.0;
/// Side of the submenu mark's square, and its gap from the accel column.
const MARK_SIZE: f64 = 9.0;
const MARK_GAP: f64 = 8.0;
/// A separator's line and the air around it.
const SEPARATOR_PAD_Y: f64 = 3.0;
/// A shaped run of text, rebuilt when its content, metrics, or brush change.
struct TextRun {
text: String,
layout: Option<Layout<Brush>>,
}
impl TextRun {
fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
layout: None,
}
}
fn set(&mut self, text: impl Into<String>) -> bool {
let text = text.into();
if self.text == text {
return false;
}
self.text = text;
self.layout = None;
true
}
fn invalidate(&mut self) {
self.layout = None;
}
fn shape(
&mut self,
text_cx: &mut TextContext,
font_size: f32,
family: Option<&str>,
brush: &Brush,
) -> &Layout<Brush> {
if self.layout.is_none() {
let mut builder =
text_cx
.layout_cx
.ranged_builder(&mut text_cx.font_cx, &self.text, 1.0, true);
if let Some(family) = family {
builder.push_default(StyleProperty::FontFamily(crate::text::font_family(family)));
}
builder.push_default(StyleProperty::FontSize(font_size));
builder.push_default(StyleProperty::Brush(brush.clone()));
let mut layout = builder.build(&self.text);
layout.break_all_lines(None);
layout.align(Alignment::Start, AlignmentOptions::default());
self.layout = Some(layout);
}
self.layout.as_ref().expect("just shaped")
}
fn width(&self) -> f32 {
self.layout.as_ref().map_or(0.0, Layout::width)
}
fn height(&self) -> f32 {
self.layout.as_ref().map_or(0.0, Layout::height)
}
}
// --- menu-separator -------------------------------------------------------
/// The appearance tokens a separator reads from the theme.
struct SeparatorTokens {
color: Brush,
thickness: f64,
}
impl SeparatorTokens {
fn blank() -> Self {
Self {
color: Color::TRANSPARENT.into(),
thickness: 0.0,
}
}
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.color, "color");
reader.number(&mut self.thickness, "thickness");
reader.changed()
}
}
impl Default for SeparatorTokens {
fn default() -> Self {
let mut tokens = Self::blank();
tokens.read(&super::fallback_style("menu-separator"));
tokens
}
}
/// A divider between groups of rows.
pub struct MenuSeparator {
tokens: SeparatorTokens,
dirt: Dirt,
}
impl MenuSeparator {
pub fn new() -> Self {
Self {
tokens: SeparatorTokens::default(),
dirt: Dirt::CLEAN,
}
}
}
impl Default for MenuSeparator {
fn default() -> Self {
Self::new()
}
}
impl Widget for MenuSeparator {
fn measure(
&mut self,
_text: &mut TextContext,
known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
taffy::Size {
width: known.width.unwrap_or(0.0),
height: (self.tokens.thickness + SEPARATOR_PAD_Y * 2.0) as f32,
}
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
let y = (size.height - self.tokens.thickness) / 2.0;
fragment.fill(
Rect::new(0.0, y, size.width, y + self.tokens.thickness),
self.tokens.color.clone(),
);
}
fn role(&self) -> accesskit::Role {
accesskit::Role::Splitter
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn type_name(&self) -> &'static str {
"menu-separator"
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.dirt.mark_layout();
}
}
}
// --- shared row appearance ------------------------------------------------
/// The appearance tokens a row (a `menu-item`, or a `menu` acting as a title
/// or submenu row) reads from the theme.
struct RowTokens {
background: Brush,
background_highlighted: Brush,
color: Brush,
color_highlighted: Brush,
accel_color: Brush,
corner_radius: f64,
submenu_mark: Graphic,
}
impl RowTokens {
fn blank() -> Self {
Self {
background: Color::TRANSPARENT.into(),
background_highlighted: Color::TRANSPARENT.into(),
color: Color::TRANSPARENT.into(),
color_highlighted: Color::TRANSPARENT.into(),
accel_color: Color::TRANSPARENT.into(),
corner_radius: 0.0,
submenu_mark: Graphic::Path(Default::default()),
}
}
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.background, "background");
reader.brush(&mut self.background_highlighted, "background-highlighted");
reader.brush(&mut self.color, "color");
reader.brush(&mut self.color_highlighted, "color-highlighted");
reader.brush(&mut self.accel_color, "accel-color");
reader.number(&mut self.corner_radius, "corner-radius");
reader.graphic(&mut self.submenu_mark, "submenu-mark");
reader.changed()
}
fn for_kind(kind: &'static str) -> Self {
let mut tokens = Self::blank();
tokens.read(&super::fallback_style(kind));
tokens
}
/// The background and label brushes for the row's own highlight state —
/// widget-internal state selecting between token variants, the checkbox
/// pattern.
fn painted(&self, highlighted: bool) -> (&Brush, &Brush) {
if highlighted {
(&self.background_highlighted, &self.color_highlighted)
} else {
(&self.background, &self.color)
}
}
}
/// Paint a row's background, label, accelerator, and submenu mark.
struct RowPaint<'a> {
tokens: &'a RowTokens,
highlighted: bool,
label: &'a TextRun,
accel: Option<&'a TextRun>,
submenu: bool,
}
impl RowPaint<'_> {
fn paint(&self, fragment: &mut Fragment, size: Size) {
let (background, _) = self.tokens.painted(self.highlighted);
let bounds = Rect::from_origin_size((0.0, 0.0), size);
if !super::is_transparent(background) {
fragment.fill(
bounds.to_rounded_rect(self.tokens.corner_radius),
background.clone(),
);
}
if let Some(layout) = &self.label.layout {
let origin = Point::new(ROW_PAD_X, (size.height - f64::from(layout.height())) / 2.0);
crate::text::append_layout(fragment, layout, origin);
}
// The accelerator sits against the right edge — the convention that
// makes a column of them readable.
let mut right = size.width - ROW_PAD_X;
if self.submenu {
let top = (size.height - MARK_SIZE) / 2.0;
let mark =
Rect::from_origin_size((right - MARK_SIZE, top), Size::new(MARK_SIZE, MARK_SIZE));
let (_, label_color) = self.tokens.painted(self.highlighted);
self.tokens.submenu_mark.paint_into(
fragment,
mark,
&GraphicPaint::Fill(label_color.clone()),
);
right -= MARK_SIZE + MARK_GAP;
}
if let Some(accel) = self.accel
&& let Some(layout) = &accel.layout
{
let origin = Point::new(
right - f64::from(layout.width()),
(size.height - f64::from(layout.height())) / 2.0,
);
crate::text::append_layout(fragment, layout, origin);
}
}
/// The row's natural size: label, accelerator column, submenu mark.
fn measure(&self) -> taffy::Size<f32> {
let mut width = f64::from(self.label.width()) + ROW_PAD_X * 2.0;
if let Some(accel) = self.accel {
width += ACCEL_GAP + f64::from(accel.width());
}
if self.submenu {
width += MARK_GAP + MARK_SIZE;
}
let height = f64::from(self.label.height()).max(MARK_SIZE) + ROW_PAD_Y * 2.0;
taffy::Size {
width: width.ceil() as f32,
height: height.ceil() as f32,
}
}
}
// --- menu-item ------------------------------------------------------------
/// One activatable row of a menu.
pub struct MenuItem {
label: TextRun,
accel: Option<TextRun>,
highlighted: bool,
font_size: f32,
family: Option<String>,
tokens: RowTokens,
dirt: Dirt,
emitted: Vec<EventData>,
}
impl Default for MenuItem {
fn default() -> Self {
Self {
label: TextRun::new(""),
accel: None,
highlighted: false,
font_size: 14.0,
family: None,
tokens: RowTokens::for_kind("menu-item"),
dirt: Dirt::CLEAN,
emitted: Vec::new(),
}
}
}
impl MenuItem {
pub fn new(label: impl Into<String>) -> Self {
let mut item = Self::default();
item.set_label(label);
item
}
/// The accelerator's rendered text. The `Keystroke` it also stands for is
/// registered by the tree, not held here.
pub fn set_accel_text(&mut self, text: impl Into<String>) {
match &mut self.accel {
Some(accel) => {
if accel.set(text) {
self.dirt.mark_layout();
}
}
None => {
self.accel = Some(TextRun::new(text));
self.dirt.mark_layout();
}
}
}
pub fn accel_text(mut self, text: impl Into<String>) -> Self {
self.set_accel_text(text);
self
}
pub fn family(mut self, family: impl Into<String>) -> Self {
self.set_family(family);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.set_font_size(size);
self
}
pub fn set_label(&mut self, label: impl Into<String>) {
if self.label.set(label) {
self.dirt.mark_layout();
}
}
pub fn set_font_size(&mut self, size: f32) {
if self.font_size != size {
self.font_size = size;
self.label.invalidate();
if let Some(accel) = &mut self.accel {
accel.invalidate();
}
self.dirt.mark_layout();
}
}
pub fn set_family(&mut self, family: impl Into<String>) {
let family = Some(family.into());
if self.family != family {
self.family = family;
self.label.invalidate();
if let Some(accel) = &mut self.accel {
accel.invalidate();
}
self.dirt.mark_layout();
}
}
pub fn label(&self) -> &str {
&self.label.text
}
pub fn is_highlighted(&self) -> bool {
self.highlighted
}
/// Highlight or unhighlight the row. Driven by the tree, which owns which
/// row of an open menu is current.
pub fn set_highlighted(&mut self, highlighted: bool) {
if self.highlighted != highlighted {
self.highlighted = highlighted;
// The label's brush changes with the highlight, so its shaped run
// is stale — same width, so paint-only.
self.label.invalidate();
self.dirt.mark_paint();
}
}
/// Report that this row was chosen. The tree turns it into the
/// `:on-select` dispatch and closes the menu stack.
pub fn select(&mut self) {
self.emitted.push(EventData::Selected);
}
fn row(&self) -> RowPaint<'_> {
RowPaint {
tokens: &self.tokens,
highlighted: self.highlighted,
label: &self.label,
accel: self.accel.as_ref(),
submenu: false,
}
}
}
impl Widget for MenuItem {
fn measure(
&mut self,
text_cx: &mut TextContext,
_known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
let (_, label_color) = self.tokens.painted(self.highlighted);
let label_color = label_color.clone();
let accel_color = self.tokens.accel_color.clone();
let (size, family) = (self.font_size, self.family.clone());
self.label
.shape(text_cx, size, family.as_deref(), &label_color);
if let Some(accel) = &mut self.accel {
accel.shape(text_cx, size, family.as_deref(), &accel_color);
}
self.row().measure()
}
fn finalize_layout(&mut self, text_cx: &mut TextContext, _size: Size, _content: Size) {
self.measure(
text_cx,
taffy::Size::NONE,
taffy::Size {
width: AvailableSpace::MaxContent,
height: AvailableSpace::MaxContent,
},
);
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
self.row().paint(fragment, size);
}
fn role(&self) -> accesskit::Role {
accesskit::Role::MenuItem
}
fn accessibility(&self, node: &mut accesskit::Node) {
node.set_label(self.label.text.clone());
if let Some(accel) = &self.accel {
node.set_keyboard_shortcut(accel.text.clone());
}
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn take_emitted(&mut self) -> Vec<EventData> {
std::mem::take(&mut self.emitted)
}
fn cursor(&self, _local: Point) -> CursorShape {
CursorShape::Pointer
}
fn type_name(&self) -> &'static str {
"menu-item"
}
fn on_pointer(
&mut self,
kind: EventKind,
_event: &PointerEvent,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
// Hovering a row makes it current, but that is the tree's doing (see
// `set_hover_chain`): enter and leave never reach a widget's own
// hooks.
if kind == EventKind::Click {
self.select();
true
} else {
false
}
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.label.invalidate();
if let Some(accel) = &mut self.accel {
accel.invalidate();
}
self.dirt.mark_paint();
}
if let Some(size) = style.number("font-size") {
self.set_font_size(size as f32);
}
if let Some(family) = style.string("font-family") {
self.set_family(family.to_owned());
}
}
}
// --- menu -----------------------------------------------------------------
/// A menu title (in a bar) or a submenu row (in a menu), and the deferred
/// content of the popup it opens.
pub struct Menu {
label: TextRun,
submenu: bool,
highlighted: bool,
open: bool,
content: ContentBuilder,
font_size: f32,
family: Option<String>,
tokens: RowTokens,
dirt: Dirt,
emitted: Vec<EventData>,
}
impl Default for Menu {
fn default() -> Self {
Self {
label: TextRun::new(""),
submenu: false,
highlighted: false,
open: false,
content: ContentBuilder::default(),
font_size: 14.0,
family: None,
tokens: RowTokens::for_kind("menu"),
dirt: Dirt::CLEAN,
emitted: Vec::new(),
}
}
}
impl Menu {
pub fn new(label: impl Into<String>) -> Self {
let mut menu = Self::default();
menu.set_label(label);
menu
}
/// Whether this menu is a submenu row (inside another menu) rather than a
/// bar title. Both consumers know it from the parent IR node's kind, so
/// it arrives here as a fact rather than as something to sniff — which is
/// exactly what the compiler's structural rule guarantees.
pub fn set_submenu(&mut self, submenu: bool) {
if self.submenu != submenu {
self.submenu = submenu;
self.dirt.mark_layout();
}
}
pub fn submenu(mut self, submenu: bool) -> Self {
self.set_submenu(submenu);
self
}
/// Use a specific font family instead of the collection's default.
pub fn family(mut self, family: impl Into<String>) -> Self {
self.set_family(family);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.set_font_size(size);
self
}
pub fn content(mut self, content: ContentBuilder) -> Self {
self.set_content(content);
self
}
pub fn label(&self) -> &str {
&self.label.text
}
pub fn is_submenu(&self) -> bool {
self.submenu
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn content_builder(&self) -> ContentBuilder {
self.content.clone()
}
/// Told by the tree, which owns the menu stack.
pub fn set_open(&mut self, open: bool) {
if self.open != open {
self.open = open;
self.dirt.mark_paint();
}
}
pub fn is_highlighted(&self) -> bool {
self.highlighted
}
pub fn set_highlighted(&mut self, highlighted: bool) {
if self.highlighted != highlighted {
self.highlighted = highlighted;
self.label.invalidate();
self.dirt.mark_paint();
}
}
pub fn set_label(&mut self, label: impl Into<String>) {
if self.label.set(label) {
self.dirt.mark_layout();
}
}
pub fn set_font_size(&mut self, size: f32) {
if self.font_size != size {
self.font_size = size;
self.label.invalidate();
self.dirt.mark_layout();
}
}
pub fn set_family(&mut self, family: impl Into<String>) {
let family = Some(family.into());
if self.family != family {
self.family = family;
self.label.invalidate();
self.dirt.mark_layout();
}
}
/// Ask the tree to open this menu's popup.
pub fn request_open(&mut self) {
self.emitted.push(EventData::MenuOpen);
}
fn row(&self) -> RowPaint<'_> {
RowPaint {
tokens: &self.tokens,
// An open menu's title stays lit while its popup is up, so the
// bar shows where you are.
highlighted: self.highlighted || self.open,
label: &self.label,
accel: None,
submenu: self.submenu,
}
}
}
impl Widget for Menu {
fn popup(&self) -> Option<Popup> {
Some(Popup {
content: self.content.clone(),
beside: self.is_submenu(),
match_width: false,
})
}
fn defers_content(&self) -> bool {
true
}
/// The popup's content, built each time it opens.
fn set_content(&mut self, content: ContentBuilder) {
self.content = content;
}
fn measure(
&mut self,
text_cx: &mut TextContext,
_known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
let (_, label_color) = self.tokens.painted(self.highlighted || self.open);
let label_color = label_color.clone();
let (size, family) = (self.font_size, self.family.clone());
self.label
.shape(text_cx, size, family.as_deref(), &label_color);
self.row().measure()
}
fn finalize_layout(&mut self, text_cx: &mut TextContext, _size: Size, _content: Size) {
self.measure(
text_cx,
taffy::Size::NONE,
taffy::Size {
width: AvailableSpace::MaxContent,
height: AvailableSpace::MaxContent,
},
);
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
self.row().paint(fragment, size);
}
fn role(&self) -> accesskit::Role {
accesskit::Role::MenuItem
}
fn accessibility(&self, node: &mut accesskit::Node) {
node.set_label(self.label.text.clone());
node.set_expanded(self.open);
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn take_emitted(&mut self) -> Vec<EventData> {
std::mem::take(&mut self.emitted)
}
fn cursor(&self, _local: Point) -> CursorShape {
CursorShape::Pointer
}
fn type_name(&self) -> &'static str {
"menu"
}
fn on_pointer(
&mut self,
kind: EventKind,
_event: &PointerEvent,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
if kind == EventKind::Click {
self.request_open();
true
} else {
false
}
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.label.invalidate();
self.dirt.mark_paint();
}
if let Some(size) = style.number("font-size") {
self.set_font_size(size as f32);
}
if let Some(family) = style.string("font-family") {
self.set_family(family.to_owned());
}
}
}
// --- menu-bar -------------------------------------------------------------
/// The appearance tokens a bar reads from the theme.
struct BarTokens {
background: Brush,
}
impl BarTokens {
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.background, "background");
reader.changed()
}
}
impl Default for BarTokens {
fn default() -> Self {
let mut tokens = Self {
background: Color::TRANSPARENT.into(),
};
tokens.read(&super::fallback_style("menu-bar"));
tokens
}
}
/// A row of menu titles.
pub struct MenuBar {
tokens: BarTokens,
dirt: Dirt,
}
impl MenuBar {
pub fn new() -> Self {
Self {
tokens: BarTokens::default(),
dirt: Dirt::CLEAN,
}
}
}
impl Default for MenuBar {
fn default() -> Self {
Self::new()
}
}
impl Widget for MenuBar {
fn adjust_style(&self, style: &mut taffy::Style) {
// A bar is a row of titles; the caller cannot forget that.
style.display = taffy::Display::Flex;
style.flex_direction = taffy::FlexDirection::Row;
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
fragment.fill(
Rect::from_origin_size((0.0, 0.0), size),
self.tokens.background.clone(),
);
}
fn role(&self) -> accesskit::Role {
accesskit::Role::MenuBar
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn type_name(&self) -> &'static str {
"menu-bar"
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.dirt.mark_paint();
}
}
}
// --- menu-panel -----------------------------------------------------------
/// The appearance tokens a popup panel reads from the theme.
struct PanelTokens {
background: Brush,
border_color: Brush,
border_width: f64,
corner_radius: f64,
}
impl PanelTokens {
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.background, "background");
reader.brush(&mut self.border_color, "border-color");
reader.number(&mut self.border_width, "border-width");
reader.number(&mut self.corner_radius, "corner-radius");
reader.changed()
}
}
impl Default for PanelTokens {
fn default() -> Self {
let mut tokens = Self {
background: Color::TRANSPARENT.into(),
border_color: Color::TRANSPARENT.into(),
border_width: 0.0,
corner_radius: 0.0,
};
tokens.read(&super::fallback_style("menu-panel"));
tokens
}
}
/// The popup a menu opens: the rows' home, and the keyboard's.
///
/// It holds focus while it is up — so the highlight can rove without any
/// focus traffic, and the overlay restores the previous focus when it closes —
/// and reports navigation keys rather than acting on them, because moving a
/// highlight needs its rows, which only the tree can see.
pub struct MenuPanel {
tokens: PanelTokens,
dirt: Dirt,
emitted: Vec<EventData>,
}
impl MenuPanel {
pub fn new() -> Self {
Self {
tokens: PanelTokens::default(),
dirt: Dirt::CLEAN,
emitted: Vec::new(),
}
}
}
impl Default for MenuPanel {
fn default() -> Self {
Self::new()
}
}
impl Widget for MenuPanel {
fn adjust_style(&self, style: &mut taffy::Style) {
style.display = taffy::Display::Flex;
style.flex_direction = taffy::FlexDirection::Column;
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
let t = &self.tokens;
let bounds = Rect::from_origin_size((0.0, 0.0), size);
fragment.fill(
bounds.to_rounded_rect(t.corner_radius),
t.background.clone(),
);
if t.border_width > 0.0 {
let inset = bounds.inset(-t.border_width / 2.0);
fragment.stroke(
inset.to_rounded_rect((t.corner_radius - t.border_width / 2.0).max(0.0)),
t.border_color.clone(),
guiduck_scene::geom::Stroke::new(t.border_width),
);
}
}
fn role(&self) -> accesskit::Role {
accesskit::Role::Menu
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn take_emitted(&mut self) -> Vec<EventData> {
std::mem::take(&mut self.emitted)
}
fn type_name(&self) -> &'static str {
"menu-panel"
}
fn focusable(&self) -> bool {
true
}
fn on_key(
&mut self,
key: &KeyInput,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
if !key.pressed {
return false;
}
let nav = match &key.key {
Key::Up => MenuNav::Previous,
Key::Down => MenuNav::Next,
Key::Home => MenuNav::First,
Key::End => MenuNav::Last,
Key::Enter => MenuNav::Activate,
Key::Right => MenuNav::Deeper,
Key::Left => MenuNav::Shallower,
// Escape is the overlay's business: it closes the topmost
// dismissable layer, which is exactly "one level".
_ => return false,
};
self.emitted.push(EventData::MenuNav(nav));
true
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.dirt.mark_paint();
}
}
}
// --- the tree's half ------------------------------------------------------
/// One open level of the menu stack.
pub(crate) struct OpenMenu {
/// The `menu` whose popup this is.
menu: WidgetId,
/// The overlay layer holding it.
overlay: WidgetId,
/// The `menu-panel` inside that layer.
panel: WidgetId,
/// The row the keyboard is currently on, if any.
highlight: Option<WidgetId>,
/// The reactive scope the content was built in. Every effect a menu's
/// content creates lives here, so closing the menu disposes exactly what
/// opening it created: teardown is scope disposal.
scope: Option<Scope>,
}
/// How a widget's popup opens.
///
/// Returned by [`Widget::popup`](super::Widget::popup); the tree reads it
/// rather than asking what kind of widget this is.
pub struct Popup {
/// The content to build into the panel when it opens.
pub content: ContentBuilder,
/// Out to the side rather than below — a submenu row rather than a title.
pub beside: bool,
/// Make the panel as wide as the opener.
pub match_width: bool,
}
impl WidgetTree {
/// Open a menu's popup: the tree's half of [`EventData::MenuOpen`].
///
/// Clicking an open menu closes it, which is what a bar title should do.
/// Otherwise every level that is not an ancestor of this one closes
/// first, so opening a sibling replaces rather than stacks.
pub(crate) fn open_menu(&mut self, menu: WidgetId) {
if self.menu_stack.iter().any(|open| open.menu == menu) {
self.close_menus_from(menu);
return;
}
// A menu opened from inside another menu's panel keeps that panel up;
// anything else is a sibling and goes.
let depth = self
.menu_stack
.iter()
.position(|open| self.is_within(menu, open.panel))
.map_or(0, |index| index + 1);
self.close_menu_levels(depth);
// The widget says how its popup opens; the tree does not carry a list
// of which kinds have one.
let Some(spec) = self.with_widget(menu, |widget| widget.popup()).flatten() else {
return;
};
let (submenu, content) = (spec.beside, spec.content);
// A field's list matches the field, so the popup reads as the field
// opening rather than as a menu appearing near it.
let field_width = spec.match_width.then(|| self.layout(menu).width());
// A bar title drops its popup below itself; a submenu row sends it out
// to the side. Placement flips and clamps against the viewport.
let side = if submenu {
AnchorSide::RightOf
} else {
AnchorSide::Below
};
// However this layer closes — Escape, an outside click, a selection —
// it comes back through `menu_closed`, so there is one teardown path.
// The id it needs is the overlay's, which does not exist yet, so the
// callback reads it from a cell the open fills in.
let commands = self.commands();
let closing: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let mut options = OverlayOptions::popup(menu, side);
options.dismiss_on_outside_click = true;
options.dismiss_on_escape = true;
options.on_close = Some({
let closing = Rc::clone(&closing);
Box::new(move || {
if let Some(overlay) = closing.get() {
commands.push(move |tree| tree.menu_closed(overlay));
}
})
});
let overlay = self.open_overlay(taffy::Style::default(), options);
closing.set(Some(overlay));
let panel_style = match field_width {
Some(width) => taffy::Style {
size: taffy::Size {
width: taffy::prelude::length(width as f32),
height: taffy::prelude::auto(),
},
..Default::default()
},
None => taffy::Style::default(),
};
let panel = self.insert(MenuPanel::new(), panel_style, Some(overlay));
// Everything the content creates — widgets, effects, subscriptions —
// is created now and belongs to this scope. Nothing existed while the
// menu was closed, and nothing survives its closing.
let scope = Scope::new();
scope.run(|| content.build(self, panel));
self.menu_stack.push(OpenMenu {
menu,
overlay,
panel,
highlight: None,
scope: Some(scope),
});
self.set_menu_open(menu, true);
// The panel holds focus while it is up, so the highlight can rove
// without focus traffic; the overlay restores the previous focus.
self.set_focus(Some(panel));
}
/// Close every level from `depth` up, innermost first.
///
/// The stack is popped *here*, synchronously, rather than left to the
/// overlay's `on_close`: that callback cannot touch the tree, so it can
/// only queue a command, and a loop waiting for a queued pop would spin
/// forever. Disposing the scope here is what makes closing exactly undo
/// opening — every effect the content created goes with it.
fn close_menu_levels(&mut self, depth: usize) {
while self.menu_stack.len() > depth {
let open = self.menu_stack.pop().expect("checked non-empty");
self.set_menu_open(open.menu, false);
if let Some(scope) = open.scope {
scope.dispose();
}
// Already gone when the close came *from* the overlay (Escape, an
// outside click); still here when we are the ones closing it.
if self.is_overlay(open.overlay) {
self.close_overlay(open.overlay);
}
}
}
/// A menu's overlay closed on its own — dismissed by Escape or by an
/// outside click. Drop its level, and any above it.
pub(crate) fn menu_closed(&mut self, overlay: WidgetId) {
if let Some(index) = self
.menu_stack
.iter()
.position(|open| open.overlay == overlay)
{
self.close_menu_levels(index);
}
}
/// Close `menu`'s level and everything above it.
fn close_menus_from(&mut self, menu: WidgetId) {
if let Some(index) = self.menu_stack.iter().position(|open| open.menu == menu) {
self.close_menu_levels(index);
}
}
/// Close the whole stack — a row was chosen, and the command is done.
pub(crate) fn close_all_menus(&mut self) {
self.close_menu_levels(0);
}
}
/// The rows of an open panel, in order: what the highlight moves through.
///
/// Separators are not rows — you cannot land on one — which is why this asks
/// the widgets rather than taking every child.
fn panel_rows(tree: &WidgetTree, panel: WidgetId) -> Vec<WidgetId> {
let mut rows = Vec::new();
let mut stack: Vec<WidgetId> = tree.children(panel).to_vec();
stack.reverse();
while let Some(id) = stack.pop() {
// A row is one of the two kinds you can land on. Asked by type, not
// by name: a separator is not a row, and neither is the scaffolding a
// `for` region puts around its rows.
if tree.widget::<MenuItem>(id).is_some() || tree.widget::<Menu>(id).is_some() {
rows.push(id);
continue;
}
// A `for` region wraps its rows in markers, so the rows of a
// data-driven menu are found by looking through the structure rather
// than only at the panel's direct children.
let mut children = tree.children(id).to_vec();
children.reverse();
stack.extend(children);
}
rows
}
impl WidgetTree {
/// Move the highlight, or act on it: the tree's half of
/// [`EventData::MenuNav`].
pub(crate) fn menu_nav(&mut self, panel: WidgetId, nav: MenuNav) {
let Some(level) = self.menu_stack.iter().position(|open| open.panel == panel) else {
return;
};
let rows = panel_rows(self, panel);
if rows.is_empty() {
return;
}
let current = self.menu_stack[level]
.highlight
.and_then(|id| rows.iter().position(|row| *row == id));
let next = match nav {
MenuNav::Previous => Some(match current {
Some(0) | None => rows.len() - 1,
Some(index) => index - 1,
}),
MenuNav::Next => Some(match current {
Some(index) if index + 1 < rows.len() => index + 1,
_ => 0,
}),
MenuNav::First => Some(0),
MenuNav::Last => Some(rows.len() - 1),
MenuNav::Activate => {
if let Some(row) = self.menu_stack[level].highlight {
self.activate_menu_row(row);
}
return;
}
MenuNav::Deeper => {
// Right opens the highlighted submenu; on a plain row it does
// nothing, since there is nowhere deeper to go.
if let Some(row) = self.menu_stack[level].highlight
&& self.widget::<Menu>(row).is_some()
{
self.open_menu(row);
}
return;
}
MenuNav::Shallower => {
// Left closes this level and returns to the row that opened
// it, which is where the eye already is.
let menu = self.menu_stack[level].menu;
self.close_menus_from(menu);
return;
}
};
if let Some(index) = next {
self.set_menu_highlight(level, Some(rows[index]));
}
}
/// A row asked to become the current one (the pointer entered it): the
/// tree's half of [`EventData::MenuHighlight`].
pub(crate) fn highlight_menu_row(&mut self, row: WidgetId) {
let Some(level) = self
.menu_stack
.iter()
.position(|open| self.is_within(row, open.panel))
else {
return;
};
// Pointing at a row of an outer menu abandons any submenu it opened.
self.close_menu_levels(level + 1);
self.set_menu_highlight(level, Some(row));
}
/// Move one level's highlight, telling the rows that gained and lost it.
fn set_menu_highlight(&mut self, level: usize, row: Option<WidgetId>) {
let previous = self.menu_stack[level].highlight;
if previous == row {
return;
}
self.menu_stack[level].highlight = row;
for (id, highlighted) in [(previous, false), (row, true)] {
let Some(id) = id else { continue };
if let Some(mut widget) = self.widget_mut::<MenuItem>(id) {
widget.set_highlighted(highlighted);
} else if let Some(mut widget) = self.widget_mut::<Menu>(id) {
widget.set_highlighted(highlighted);
}
}
}
/// Choose a row, whichever way it was chosen. A submenu row opens rather
/// than selects — there is nothing to report about opening a menu.
pub(crate) fn activate_menu_row(&mut self, row: WidgetId) {
if self.widget::<Menu>(row).is_some() {
self.open_menu(row);
} else if self.widget::<MenuItem>(row).is_some() {
self.select_menu_row(row);
}
}
/// Fire a row's `:on-select` and close the stack.
///
/// The one selection path: a pointer click reports `EventData::Selected`
/// and lands here, and so do Enter and an accelerator.
///
/// The wire fires now; the close is queued. Both halves of that matter.
/// The wire must run before the rows are gone, and the close must not run
/// *during* a dispatch that is still walking those rows — a pointer Click
/// arrives mid-traversal, and pulling the widgets out from under it would
/// leave the walk holding dead ids. The command queue is the seam for
/// exactly this: it runs when the frame's dispatch has unwound.
pub(crate) fn select_menu_row(&mut self, row: WidgetId) {
self.dispatch_semantic(
row,
crate::event::EventKind::Select,
crate::event::EventData::Selected,
);
self.commands().push(|tree| tree.close_all_menus());
}
/// Tell whatever opened a popup that it is (or is no longer) open — a
/// `menu` title lights up, a `dropdown` field turns its arrow.
fn set_menu_open(&mut self, id: WidgetId, open: bool) {
if let Some(mut widget) = self.widget_mut::<Menu>(id) {
widget.set_open(open);
} else if let Some(mut widget) = self.widget_mut::<Dropdown>(id) {
widget.set_open(open);
} else if let Some(mut widget) = self.widget_mut::<super::ComboBox>(id) {
widget.set_menu_open(open);
}
}
/// Whether any menu is open — the shell's cue that a click outside is a
/// dismissal rather than ordinary input.
pub fn menus_open(&self) -> bool {
!self.menu_stack.is_empty()
}
}
#[cfg(test)]
mod tests;
/// The runtime keystroke an accelerator declaration stands for.
///
/// The grammar lives in the compiler, which is where a typo becomes a
/// diagnostic — so this parses the *same* string with the *same* parser
/// rather than keeping a second opinion about what `"Ctrl+S"` means. It
/// cannot fail in practice: validation already accepted it.
fn keystroke_for(accel: &str) -> Option<crate::event::Keystroke> {
use guiduck_component_core::accel::{AccelKey, parse_accel};
let parsed = parse_accel(accel, guiduck_component_core::sexpr::Span::new(0, 0)).ok()?;
Some(crate::event::Keystroke {
key: match parsed.key {
AccelKey::Character(c) => Key::Character(c),
AccelKey::Enter => Key::Enter,
AccelKey::Delete => Key::Delete,
AccelKey::Home => Key::Home,
AccelKey::End => Key::End,
AccelKey::Left => Key::Left,
AccelKey::Right => Key::Right,
AccelKey::Up => Key::Up,
AccelKey::Down => Key::Down,
},
modifiers: crate::event::Modifiers {
ctrl: parsed.ctrl,
shift: parsed.shift,
alt: parsed.alt,
logo: parsed.logo,
},
})
}
impl WidgetTree {
/// Register a menu item's accelerator.
///
/// `run` is the item's own `:on-select` work, not a dispatch at the row:
/// an accelerator's whole point is firing while its menu is *closed*, and
/// a closed menu's rows do not exist. So the two routes share the handler
/// rather than the widget — which is also why an accelerator may not sit
/// inside `if` or `for`, where the row is per-item and there is nothing to
/// register at mount.
///
/// The key-routing order already does the right thing: the focused widget
/// sees the key first, so a text input's Ctrl+C beats an accelerator.
/// `owner` is the widget the accelerator belongs to — the menu that
/// declared it. When that widget goes, so does the accelerator: a dev
/// reload rebuilds the subtree, and a registration outliving its menu
/// would shadow the one that replaced it.
pub fn on_menu_accel(&mut self, owner: WidgetId, accel: &str, mut run: impl FnMut() + 'static) {
let Some(keystroke) = keystroke_for(accel) else {
return;
};
let commands = self.commands();
self.shortcuts.push((
Some(owner),
keystroke,
Box::new(move || {
// An accelerator can fire with a menu open (you can reach for
// Ctrl+S mid-browse), so the stack goes, exactly as choosing the
// row would take it.
commands.push(|tree| tree.close_all_menus());
run();
}),
));
}
}
// --- context-menu ---------------------------------------------------------
/// Rows that open at the pointer when the enclosing widget is right-pressed.
///
/// It draws nothing and takes no space: it is an *attachment point* that
/// happens to live in the tree, which is what lets it be written where the
/// thing it belongs to is written. Its rows are deferred exactly as a menu's
/// are, so a context menu on every row of a long list costs nothing until one
/// is opened.
#[derive(Default)]
pub struct ContextMenu {
content: ContentBuilder,
}
impl ContextMenu {
pub fn new(content: ContentBuilder) -> Self {
Self { content }
}
pub fn content_builder(&self) -> ContentBuilder {
self.content.clone()
}
}
impl Widget for ContextMenu {
fn defers_content(&self) -> bool {
true
}
/// The popup's rows, built each time it opens.
fn set_content(&mut self, content: ContentBuilder) {
self.content = content;
}
fn adjust_style(&self, style: &mut taffy::Style) {
// Present in the tree, absent from the layout — the caller cannot
// forget, because there is nothing here to lay out.
style.display = taffy::Display::None;
}
fn type_name(&self) -> &'static str {
"context-menu"
}
}
impl WidgetTree {
/// The context menu attached at or above `target`, if any.
///
/// Innermost wins: a row's own menu beats the list's, which is what
/// nesting them is for.
fn context_menu_for(&self, target: WidgetId) -> Option<WidgetId> {
let mut current = Some(target);
while let Some(id) = current {
if let Some(found) = self
.children(id)
.iter()
.find(|child| self.widget::<ContextMenu>(**child).is_some())
{
return Some(*found);
}
current = self.parent(id);
}
None
}
/// Open the context menu attached at or above `target`, at `at`. Reports
/// whether one was there — an unclaimed right-press is not ours.
pub(crate) fn open_context_menu(&mut self, target: WidgetId, at: Point) -> bool {
let Some(menu) = self.context_menu_for(target) else {
return false;
};
// A right-press elsewhere abandons whatever was open, exactly as a
// left-press outside would.
self.close_all_menus();
let Some(content) = self
.widget::<ContextMenu>(menu)
.map(|widget| widget.content_builder())
else {
return false;
};
let commands = self.commands();
let closing: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let mut options = OverlayOptions::popup_at(at);
options.dismiss_on_outside_click = true;
options.dismiss_on_escape = true;
options.on_close = Some({
let closing = Rc::clone(&closing);
Box::new(move || {
if let Some(overlay) = closing.get() {
commands.push(move |tree| tree.menu_closed(overlay));
}
})
});
let overlay = self.open_overlay(taffy::Style::default(), options);
closing.set(Some(overlay));
let panel = self.insert(MenuPanel::new(), taffy::Style::default(), Some(overlay));
let scope = Scope::new();
scope.run(|| content.build(self, panel));
// It joins the same stack as a bar menu's popup: one set of rules for
// dismissal, navigation, and selection, whatever opened it.
self.menu_stack.push(OpenMenu {
menu,
overlay,
panel,
highlight: None,
scope: Some(scope),
});
self.set_focus(Some(panel));
true
}
}
// --- dropdown -------------------------------------------------------------
/// The appearance tokens a dropdown reads from the theme.
struct DropdownTokens {
background: Brush,
color: Brush,
border: Brush,
border_width: f64,
corner_radius: f64,
focus_ring_color: Brush,
focus_ring_width: f64,
arrow_mark: Graphic,
arrow_color: Brush,
}
impl DropdownTokens {
fn read(&mut self, style: &ComputedStyle) -> bool {
let mut reader = StyleReader::new(style);
reader.brush(&mut self.background, "background");
reader.brush(&mut self.color, "color");
reader.brush(&mut self.border, "border-color");
reader.number(&mut self.border_width, "border-width");
reader.number(&mut self.corner_radius, "corner-radius");
reader.brush(&mut self.focus_ring_color, "focus-ring-color");
reader.number(&mut self.focus_ring_width, "focus-ring-width");
reader.graphic(&mut self.arrow_mark, "arrow-mark");
reader.brush(&mut self.arrow_color, "arrow-color");
reader.changed()
}
}
impl Default for DropdownTokens {
fn default() -> Self {
let mut tokens = Self {
background: Color::TRANSPARENT.into(),
color: Color::TRANSPARENT.into(),
border: Color::TRANSPARENT.into(),
border_width: 0.0,
corner_radius: 0.0,
focus_ring_color: Color::TRANSPARENT.into(),
focus_ring_width: 0.0,
arrow_mark: Graphic::Path(Default::default()),
arrow_color: Color::TRANSPARENT.into(),
};
tokens.read(&super::fallback_style("dropdown"));
tokens
}
}
/// A field that opens a list.
///
/// Structurally this *is* a [`Menu`]: deferred rows, one popup, the same
/// stack, highlight, dismissal, and selection. What it adds is a control's
/// manners — it is focusable and opens from the keyboard, it draws like a
/// field, and its popup matches its own width instead of hugging its rows.
/// That short list is the whole difference, which is the point: the fourth
/// customer of deferred content needed a widget, not machinery.
///
/// It holds no list and no selection. `:label` is the chosen option's text,
/// bindable, computed by the app from its own data — `for` is the model.
pub struct Dropdown {
label: TextRun,
open: bool,
content: ContentBuilder,
font_size: f32,
family: Option<String>,
tokens: DropdownTokens,
dirt: Dirt,
emitted: Vec<EventData>,
}
impl Default for Dropdown {
fn default() -> Self {
Self {
label: TextRun::new(""),
open: false,
content: ContentBuilder::default(),
font_size: 14.0,
family: None,
tokens: DropdownTokens::default(),
dirt: Dirt::CLEAN,
emitted: Vec::new(),
}
}
}
impl Dropdown {
pub fn new(label: impl Into<String>) -> Self {
let mut dropdown = Self::default();
dropdown.set_label(label);
dropdown
}
pub fn content(mut self, content: ContentBuilder) -> Self {
self.set_content(content);
self
}
pub fn family(mut self, family: impl Into<String>) -> Self {
self.set_family(family);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.set_font_size(size);
self
}
pub fn label(&self) -> &str {
&self.label.text
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn content_builder(&self) -> ContentBuilder {
self.content.clone()
}
pub fn set_open(&mut self, open: bool) {
if self.open != open {
self.open = open;
self.dirt.mark_paint();
}
}
pub fn set_label(&mut self, label: impl Into<String>) {
if self.label.set(label) {
self.dirt.mark_layout();
}
}
pub fn set_font_size(&mut self, size: f32) {
if self.font_size != size {
self.font_size = size;
self.label.invalidate();
self.dirt.mark_layout();
}
}
pub fn set_family(&mut self, family: impl Into<String>) {
let family = Some(family.into());
if self.family != family {
self.family = family;
self.label.invalidate();
self.dirt.mark_layout();
}
}
}
impl Widget for Dropdown {
fn popup(&self) -> Option<Popup> {
Some(Popup {
content: self.content.clone(),
beside: false,
match_width: true,
})
}
fn defers_content(&self) -> bool {
true
}
/// The list's rows, built each time it opens.
fn set_content(&mut self, content: ContentBuilder) {
self.content = content;
}
fn measure(
&mut self,
text_cx: &mut TextContext,
_known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
let color = self.tokens.color.clone();
let (size, family) = (self.font_size, self.family.clone());
self.label.shape(text_cx, size, family.as_deref(), &color);
taffy::Size {
width: (f64::from(self.label.width()) + ROW_PAD_X * 2.0 + MARK_GAP + MARK_SIZE).ceil()
as f32,
height: (f64::from(self.label.height()).max(MARK_SIZE) + ROW_PAD_Y * 2.0).ceil() as f32,
}
}
fn finalize_layout(&mut self, text_cx: &mut TextContext, _size: Size, _content: Size) {
self.measure(
text_cx,
taffy::Size::NONE,
taffy::Size {
width: AvailableSpace::MaxContent,
height: AvailableSpace::MaxContent,
},
);
}
fn paint(&mut self, fragment: &mut Fragment, size: Size) {
let t = &self.tokens;
let bounds = Rect::from_origin_size((0.0, 0.0), size);
fragment.fill(
bounds.to_rounded_rect(t.corner_radius),
t.background.clone(),
);
if t.border_width > 0.0 {
let inset = bounds.inset(-t.border_width / 2.0);
fragment.stroke(
inset.to_rounded_rect((t.corner_radius - t.border_width / 2.0).max(0.0)),
t.border.clone(),
guiduck_scene::geom::Stroke::new(t.border_width),
);
}
// The `:focus` rule makes the ring opaque; the widget never asks
// whether it is focused.
if !super::is_transparent(&t.focus_ring_color) {
let ring = bounds.inset(-t.focus_ring_width / 2.0 - 1.0);
fragment.stroke(
ring.to_rounded_rect((t.corner_radius + 1.0).max(0.0)),
t.focus_ring_color.clone(),
guiduck_scene::geom::Stroke::new(t.focus_ring_width),
);
}
if let Some(layout) = &self.label.layout {
let origin = Point::new(ROW_PAD_X, (size.height - f64::from(layout.height())) / 2.0);
crate::text::append_layout(fragment, layout, origin);
}
let mark = Rect::from_origin_size(
(
size.width - ROW_PAD_X - MARK_SIZE,
(size.height - MARK_SIZE) / 2.0,
),
Size::new(MARK_SIZE, MARK_SIZE),
);
t.arrow_mark
.paint_into(fragment, mark, &GraphicPaint::Fill(t.arrow_color.clone()));
}
fn role(&self) -> accesskit::Role {
accesskit::Role::ComboBox
}
fn accessibility(&self, node: &mut accesskit::Node) {
node.set_value(self.label.text.clone());
node.set_expanded(self.open);
}
fn take_dirt(&mut self) -> Dirt {
std::mem::take(&mut self.dirt)
}
fn take_emitted(&mut self) -> Vec<EventData> {
std::mem::take(&mut self.emitted)
}
fn cursor(&self, _local: Point) -> CursorShape {
CursorShape::Pointer
}
fn type_name(&self) -> &'static str {
"dropdown"
}
fn focusable(&self) -> bool {
true
}
fn on_key(
&mut self,
key: &KeyInput,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
if !key.pressed {
return false;
}
// The three ways a closed field is opened from the keyboard.
match &key.key {
Key::Enter | Key::Down => {}
Key::Character(text) if text == " " => {}
_ => return false,
}
self.emitted.push(EventData::MenuOpen);
true
}
fn on_pointer(
&mut self,
kind: EventKind,
_event: &PointerEvent,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
if kind == EventKind::Click {
self.emitted.push(EventData::MenuOpen);
true
} else {
false
}
}
fn apply_style(&mut self, style: &ComputedStyle) {
if self.tokens.read(style) {
self.label.invalidate();
self.dirt.mark_paint();
}
if let Some(size) = style.number("font-size") {
self.set_font_size(size as f32);
}
if let Some(family) = style.string("font-family") {
self.set_family(family.to_owned());
}
}
}