#![allow(unsafe_code)]
use std::cell::RefCell;
use std::ffi::c_void;
use std::rc::Rc;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::menu::{
Align, Flex, Item as MuriItem, Menu as MuriMenu, Row as MuriRow, Segment as MuriSegment,
};
pub use crate::event::MenuEventReceiver;
pub use crate::menu::{Icon as MuriIcon, MenuEvent, MenuId};
pub mod about_metadata;
pub mod accelerator;
use about_metadata::AboutMetadata;
use accelerator::Accelerator;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
NotAChildOfThisMenu,
AlreadyInitialized,
AcceleratorParse(String),
BadIcon(String),
Unsupported(crate::Unsupported),
Platform(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotAChildOfThisMenu => f.write_str("item is not a child of this menu"),
Error::AlreadyInitialized => f.write_str("menu has already been initialized"),
Error::AcceleratorParse(s) => write!(f, "failed to parse accelerator: {s}"),
Error::BadIcon(s) => write!(f, "bad icon: {s}"),
Error::Unsupported(u) => write!(f, "unsupported on this platform: {u}"),
Error::Platform(s) => write!(f, "platform error: {s}"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub struct BadIcon(pub String);
impl std::fmt::Display for BadIcon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "bad icon: {}", self.0)
}
}
impl std::error::Error for BadIcon {}
fn next_auto_id() -> MenuId {
static COUNTER: AtomicU32 = AtomicU32::new(1);
MenuId(COUNTER.fetch_add(1, Ordering::Relaxed).to_string())
}
fn resolve_id(id: Option<MenuId>) -> MenuId {
id.unwrap_or_else(next_auto_id)
}
#[derive(Clone, Debug)]
pub struct Icon {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
}
impl Icon {
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> std::result::Result<Self, BadIcon> {
if width == 0 || height == 0 {
return Err(BadIcon(format!(
"icon dimensions must be non-zero, got {width}x{height}"
)));
}
let expected = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(4));
match expected {
Some(expected) if rgba.len() == expected => Ok(Icon {
rgba,
width,
height,
}),
Some(expected) => Err(BadIcon(format!(
"expected {expected} bytes for {width}x{height} RGBA, got {}",
rgba.len()
))),
None => Err(BadIcon(format!(
"icon dimensions {width}x{height} overflow the addressable buffer size"
))),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NativeIcon {
User,
Caution,
Info,
Computer,
Folder,
StatusAvailable,
StatusUnavailable,
}
impl NativeIcon {
fn symbol_name(&self) -> &'static str {
match self {
NativeIcon::User => "person",
NativeIcon::Caution => "exclamationmark.triangle",
NativeIcon::Info => "info.circle",
NativeIcon::Computer => "desktopcomputer",
NativeIcon::Folder => "folder",
NativeIcon::StatusAvailable => "circle.fill",
NativeIcon::StatusUnavailable => "circle",
}
}
}
pub trait IsMenuItem {
fn kind(&self) -> MenuItemKind;
fn id(&self) -> MenuId;
}
#[derive(Clone)]
#[non_exhaustive]
pub enum MenuItemKind {
MenuItem(MenuItem),
Submenu(Submenu),
Predefined(PredefinedMenuItem),
Check(CheckMenuItem),
Icon(IconMenuItem),
}
impl MenuItemKind {
pub fn id(&self) -> MenuId {
match self {
MenuItemKind::MenuItem(i) => i.id(),
MenuItemKind::Submenu(i) => i.id(),
MenuItemKind::Predefined(i) => i.id(),
MenuItemKind::Check(i) => i.id(),
MenuItemKind::Icon(i) => i.id(),
}
}
fn to_muri(&self) -> MuriItem {
match self {
MenuItemKind::MenuItem(i) => {
let s = i.inner.borrow();
MuriItem::Row(apply_label(
MuriRow::new(s.id.clone()).enabled(s.enabled),
&s.text,
))
}
MenuItemKind::Check(i) => {
let s = i.inner.borrow();
let mut row = MuriRow::new(s.id.clone())
.enabled(s.enabled)
.checked(s.checked);
if s.checked {
row = row.leading(MuriIcon::Checkmark);
}
MuriItem::Row(apply_label(row, &s.text))
}
MenuItemKind::Icon(i) => {
let s = i.inner.borrow();
let mut row = apply_label(MuriRow::new(s.id.clone()).enabled(s.enabled), &s.text);
match &s.icon {
Some(IconSource::Rgba(icon)) => {
if let Some(png) =
super::encode_rgba_cached(&icon.rgba, icon.width, icon.height)
{
row = row.leading(MuriIcon::Png(png));
}
}
Some(IconSource::Native(native)) => {
row = row.leading(MuriIcon::Symbol(native.symbol_name()));
}
None => {}
}
MuriItem::Row(row)
}
MenuItemKind::Submenu(i) => {
let s = i.inner.borrow();
let label = apply_label(MuriRow::new(s.id.clone()).enabled(s.enabled), &s.text);
MuriItem::Submenu {
label,
menu: kinds_to_muri_menu(&s.items),
}
}
MenuItemKind::Predefined(i) => {
let s = i.inner.borrow();
match &s.predefined {
Predefined::Separator => MuriItem::Separator,
_ => MuriItem::Row(
MuriRow::new(s.id.clone())
.label(s.text.clone())
.enabled(false),
),
}
}
}
}
}
fn kinds_to_muri_menu(items: &[MenuItemKind]) -> MuriMenu {
let mut menu = MuriMenu::new();
for kind in items {
menu.items.push(kind.to_muri());
}
menu
}
fn apply_label(row: MuriRow, text: &str) -> MuriRow {
match text.split_once('\t') {
Some((lead, tail)) => row.segments(vec![
MuriSegment::new(lead.trim_end()).flex(Flex::Grow),
MuriSegment::new(tail.trim_start()).align(Align::Right),
]),
None => row.label(text.to_owned()),
}
}
struct MenuItemState {
id: MenuId,
text: String,
enabled: bool,
#[allow(dead_code)] accelerator: Option<Accelerator>,
}
#[derive(Clone)]
pub struct MenuItem {
inner: Rc<RefCell<MenuItemState>>,
}
impl MenuItem {
pub fn new(text: impl AsRef<str>, enabled: bool, accelerator: Option<Accelerator>) -> Self {
Self::build(None, text, enabled, accelerator)
}
pub fn with_id(
id: impl Into<MenuId>,
text: impl AsRef<str>,
enabled: bool,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(Some(id.into()), text, enabled, accelerator)
}
fn build(
id: Option<MenuId>,
text: impl AsRef<str>,
enabled: bool,
accelerator: Option<Accelerator>,
) -> Self {
MenuItem {
inner: Rc::new(RefCell::new(MenuItemState {
id: resolve_id(id),
text: text.as_ref().to_owned(),
enabled,
accelerator,
})),
}
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn text(&self) -> String {
self.inner.borrow().text.clone()
}
pub fn set_text(&self, text: impl AsRef<str>) {
self.inner.borrow_mut().text = text.as_ref().to_owned();
}
pub fn is_enabled(&self) -> bool {
self.inner.borrow().enabled
}
pub fn set_enabled(&self, enabled: bool) {
self.inner.borrow_mut().enabled = enabled;
}
pub fn set_accelerator(&self, accelerator: Option<Accelerator>) {
self.inner.borrow_mut().accelerator = accelerator;
}
}
impl IsMenuItem for MenuItem {
fn kind(&self) -> MenuItemKind {
MenuItemKind::MenuItem(self.clone())
}
fn id(&self) -> MenuId {
MenuItem::id(self)
}
}
struct CheckMenuItemState {
id: MenuId,
text: String,
enabled: bool,
checked: bool,
#[allow(dead_code)]
accelerator: Option<Accelerator>,
}
#[derive(Clone)]
pub struct CheckMenuItem {
inner: Rc<RefCell<CheckMenuItemState>>,
}
impl CheckMenuItem {
pub fn new(
text: impl AsRef<str>,
enabled: bool,
checked: bool,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(None, text, enabled, checked, accelerator)
}
pub fn with_id(
id: impl Into<MenuId>,
text: impl AsRef<str>,
enabled: bool,
checked: bool,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(Some(id.into()), text, enabled, checked, accelerator)
}
fn build(
id: Option<MenuId>,
text: impl AsRef<str>,
enabled: bool,
checked: bool,
accelerator: Option<Accelerator>,
) -> Self {
CheckMenuItem {
inner: Rc::new(RefCell::new(CheckMenuItemState {
id: resolve_id(id),
text: text.as_ref().to_owned(),
enabled,
checked,
accelerator,
})),
}
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn text(&self) -> String {
self.inner.borrow().text.clone()
}
pub fn set_text(&self, text: impl AsRef<str>) {
self.inner.borrow_mut().text = text.as_ref().to_owned();
}
pub fn is_enabled(&self) -> bool {
self.inner.borrow().enabled
}
pub fn set_enabled(&self, enabled: bool) {
self.inner.borrow_mut().enabled = enabled;
}
pub fn is_checked(&self) -> bool {
self.inner.borrow().checked
}
pub fn set_checked(&self, checked: bool) {
self.inner.borrow_mut().checked = checked;
}
pub fn set_accelerator(&self, accelerator: Option<Accelerator>) {
self.inner.borrow_mut().accelerator = accelerator;
}
}
impl IsMenuItem for CheckMenuItem {
fn kind(&self) -> MenuItemKind {
MenuItemKind::Check(self.clone())
}
fn id(&self) -> MenuId {
CheckMenuItem::id(self)
}
}
enum IconSource {
Rgba(Icon),
Native(NativeIcon),
}
struct IconMenuItemState {
id: MenuId,
text: String,
enabled: bool,
icon: Option<IconSource>,
#[allow(dead_code)]
accelerator: Option<Accelerator>,
}
#[derive(Clone)]
pub struct IconMenuItem {
inner: Rc<RefCell<IconMenuItemState>>,
}
impl IconMenuItem {
pub fn new(
text: impl AsRef<str>,
enabled: bool,
icon: Option<Icon>,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(None, text, enabled, icon.map(IconSource::Rgba), accelerator)
}
pub fn with_id(
id: impl Into<MenuId>,
text: impl AsRef<str>,
enabled: bool,
icon: Option<Icon>,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(
Some(id.into()),
text,
enabled,
icon.map(IconSource::Rgba),
accelerator,
)
}
pub fn with_native_icon(
text: impl AsRef<str>,
enabled: bool,
native_icon: Option<NativeIcon>,
accelerator: Option<Accelerator>,
) -> Self {
Self::build(
None,
text,
enabled,
native_icon.map(IconSource::Native),
accelerator,
)
}
fn build(
id: Option<MenuId>,
text: impl AsRef<str>,
enabled: bool,
icon: Option<IconSource>,
accelerator: Option<Accelerator>,
) -> Self {
IconMenuItem {
inner: Rc::new(RefCell::new(IconMenuItemState {
id: resolve_id(id),
text: text.as_ref().to_owned(),
enabled,
icon,
accelerator,
})),
}
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn text(&self) -> String {
self.inner.borrow().text.clone()
}
pub fn set_text(&self, text: impl AsRef<str>) {
self.inner.borrow_mut().text = text.as_ref().to_owned();
}
pub fn is_enabled(&self) -> bool {
self.inner.borrow().enabled
}
pub fn set_enabled(&self, enabled: bool) {
self.inner.borrow_mut().enabled = enabled;
}
pub fn set_icon(&self, icon: Option<Icon>) {
self.inner.borrow_mut().icon = icon.map(IconSource::Rgba);
}
pub fn set_native_icon(&self, native_icon: Option<NativeIcon>) {
self.inner.borrow_mut().icon = native_icon.map(IconSource::Native);
}
pub fn set_accelerator(&self, accelerator: Option<Accelerator>) {
self.inner.borrow_mut().accelerator = accelerator;
}
}
impl IsMenuItem for IconMenuItem {
fn kind(&self) -> MenuItemKind {
MenuItemKind::Icon(self.clone())
}
fn id(&self) -> MenuId {
IconMenuItem::id(self)
}
}
#[derive(Clone)]
enum Predefined {
Separator,
Copy,
Cut,
Paste,
SelectAll,
Undo,
Redo,
Minimize,
Maximize,
Fullscreen,
CloseWindow,
Hide,
HideOthers,
ShowAll,
Quit,
About(#[allow(dead_code)] Option<AboutMetadata>),
Services,
BringAllToFront,
}
impl Predefined {
fn default_text(&self) -> &'static str {
match self {
Predefined::Separator => "",
Predefined::Copy => "Copy",
Predefined::Cut => "Cut",
Predefined::Paste => "Paste",
Predefined::SelectAll => "Select All",
Predefined::Undo => "Undo",
Predefined::Redo => "Redo",
Predefined::Minimize => "Minimize",
Predefined::Maximize => "Zoom",
Predefined::Fullscreen => "Enter Full Screen",
Predefined::CloseWindow => "Close Window",
Predefined::Hide => "Hide",
Predefined::HideOthers => "Hide Others",
Predefined::ShowAll => "Show All",
Predefined::Quit => "Quit",
Predefined::About(_) => "About",
Predefined::Services => "Services",
Predefined::BringAllToFront => "Bring All to Front",
}
}
}
struct PredefinedState {
id: MenuId,
text: String,
predefined: Predefined,
}
#[derive(Clone)]
pub struct PredefinedMenuItem {
inner: Rc<RefCell<PredefinedState>>,
}
impl PredefinedMenuItem {
fn build(predefined: Predefined, text: Option<&str>) -> Self {
let text = text
.map(str::to_owned)
.unwrap_or_else(|| predefined.default_text().to_owned());
PredefinedMenuItem {
inner: Rc::new(RefCell::new(PredefinedState {
id: next_auto_id(),
text,
predefined,
})),
}
}
pub fn separator() -> Self {
Self::build(Predefined::Separator, None)
}
pub fn copy(text: Option<&str>) -> Self {
Self::build(Predefined::Copy, text)
}
pub fn cut(text: Option<&str>) -> Self {
Self::build(Predefined::Cut, text)
}
pub fn paste(text: Option<&str>) -> Self {
Self::build(Predefined::Paste, text)
}
pub fn select_all(text: Option<&str>) -> Self {
Self::build(Predefined::SelectAll, text)
}
pub fn undo(text: Option<&str>) -> Self {
Self::build(Predefined::Undo, text)
}
pub fn redo(text: Option<&str>) -> Self {
Self::build(Predefined::Redo, text)
}
pub fn minimize(text: Option<&str>) -> Self {
Self::build(Predefined::Minimize, text)
}
pub fn maximize(text: Option<&str>) -> Self {
Self::build(Predefined::Maximize, text)
}
pub fn fullscreen(text: Option<&str>) -> Self {
Self::build(Predefined::Fullscreen, text)
}
pub fn close_window(text: Option<&str>) -> Self {
Self::build(Predefined::CloseWindow, text)
}
pub fn hide(text: Option<&str>) -> Self {
Self::build(Predefined::Hide, text)
}
pub fn hide_others(text: Option<&str>) -> Self {
Self::build(Predefined::HideOthers, text)
}
pub fn show_all(text: Option<&str>) -> Self {
Self::build(Predefined::ShowAll, text)
}
pub fn quit(text: Option<&str>) -> Self {
Self::build(Predefined::Quit, text)
}
pub fn about(text: Option<&str>, metadata: Option<AboutMetadata>) -> Self {
Self::build(Predefined::About(metadata), text)
}
pub fn services(text: Option<&str>) -> Self {
Self::build(Predefined::Services, text)
}
pub fn bring_all_to_front(text: Option<&str>) -> Self {
Self::build(Predefined::BringAllToFront, text)
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn text(&self) -> String {
self.inner.borrow().text.clone()
}
#[allow(dead_code)] fn is_separator(&self) -> bool {
matches!(self.inner.borrow().predefined, Predefined::Separator)
}
}
impl IsMenuItem for PredefinedMenuItem {
fn kind(&self) -> MenuItemKind {
MenuItemKind::Predefined(self.clone())
}
fn id(&self) -> MenuId {
PredefinedMenuItem::id(self)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SurfaceMode {
Undetermined,
MenuBar,
Custom,
}
#[derive(Clone, Copy, Debug)]
pub struct Position {
pub x: f64,
pub y: f64,
}
struct MenuState {
id: MenuId,
items: Vec<MenuItemKind>,
mode: SurfaceMode,
}
#[derive(Clone)]
pub struct Menu {
inner: Rc<RefCell<MenuState>>,
}
impl Default for Menu {
fn default() -> Self {
Menu::new()
}
}
impl Menu {
pub fn new() -> Self {
Menu {
inner: Rc::new(RefCell::new(MenuState {
id: next_auto_id(),
items: Vec::new(),
mode: SurfaceMode::Undetermined,
})),
}
}
pub fn with_id(id: impl Into<MenuId>) -> Self {
Menu {
inner: Rc::new(RefCell::new(MenuState {
id: id.into(),
items: Vec::new(),
mode: SurfaceMode::Undetermined,
})),
}
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn append(&self, item: &dyn IsMenuItem) -> Result<()> {
self.inner.borrow_mut().items.push(item.kind());
Ok(())
}
pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> Result<()> {
for item in items {
self.append(*item)?;
}
Ok(())
}
pub fn prepend(&self, item: &dyn IsMenuItem) -> Result<()> {
self.inner.borrow_mut().items.insert(0, item.kind());
Ok(())
}
pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> Result<()> {
let mut state = self.inner.borrow_mut();
let position = position.min(state.items.len());
state.items.insert(position, item.kind());
Ok(())
}
pub fn remove(&self, item: &dyn IsMenuItem) -> Result<()> {
let mut state = self.inner.borrow_mut();
let target = item.id();
if let Some(pos) = state.items.iter().position(|k| k.id() == target) {
state.items.remove(pos);
Ok(())
} else {
Err(Error::NotAChildOfThisMenu)
}
}
pub fn items(&self) -> Vec<MenuItemKind> {
self.inner.borrow().items.clone()
}
pub(crate) fn to_muri_menu(&self) -> MuriMenu {
kinds_to_muri_menu(&self.inner.borrow().items)
}
#[allow(dead_code)] pub(crate) fn mode(&self) -> SurfaceMode {
self.inner.borrow().mode
}
fn set_mode(&self, mode: SurfaceMode) {
self.inner.borrow_mut().mode = mode;
}
fn route_menu_bar(&self) -> Result<()> {
self.set_mode(SurfaceMode::MenuBar);
let _ = self.to_muri_menu();
Ok(())
}
pub(crate) fn build_custom_surface(&self) -> crate::ContextMenu {
self.set_mode(SurfaceMode::Custom);
crate::ContextMenu::new(self.to_muri_menu())
}
}
pub trait ContextMenu {
fn init_for_nsapp(&self) -> Result<()>;
unsafe fn init_for_hwnd(&self, hwnd: isize) -> Result<()>;
fn init_for_gtk_window(&self) -> Result<()>;
unsafe fn show_context_menu_for_nsview(
&self,
nsview: *mut c_void,
position: Option<Position>,
) -> Result<()>;
unsafe fn show_context_menu_for_hwnd(
&self,
hwnd: isize,
position: Option<Position>,
) -> Result<()>;
fn show_context_menu_for_gtk_window(&self, position: Option<Position>) -> Result<()>;
}
fn open_custom(menu: &Menu, _position: Option<Position>) -> Result<()> {
let _surface = menu.build_custom_surface();
Ok(())
}
impl ContextMenu for Menu {
fn init_for_nsapp(&self) -> Result<()> {
self.route_menu_bar()
}
unsafe fn init_for_hwnd(&self, _hwnd: isize) -> Result<()> {
self.route_menu_bar()
}
fn init_for_gtk_window(&self) -> Result<()> {
self.route_menu_bar()
}
unsafe fn show_context_menu_for_nsview(
&self,
_nsview: *mut c_void,
position: Option<Position>,
) -> Result<()> {
open_custom(self, position)
}
unsafe fn show_context_menu_for_hwnd(
&self,
_hwnd: isize,
position: Option<Position>,
) -> Result<()> {
open_custom(self, position)
}
fn show_context_menu_for_gtk_window(&self, position: Option<Position>) -> Result<()> {
open_custom(self, position)
}
}
struct SubmenuState {
id: MenuId,
text: String,
enabled: bool,
items: Vec<MenuItemKind>,
}
#[derive(Clone)]
pub struct Submenu {
inner: Rc<RefCell<SubmenuState>>,
}
impl Submenu {
pub fn new(text: impl AsRef<str>, enabled: bool) -> Self {
Self::build(None, text, enabled)
}
pub fn with_id(id: impl Into<MenuId>, text: impl AsRef<str>, enabled: bool) -> Self {
Self::build(Some(id.into()), text, enabled)
}
fn build(id: Option<MenuId>, text: impl AsRef<str>, enabled: bool) -> Self {
Submenu {
inner: Rc::new(RefCell::new(SubmenuState {
id: resolve_id(id),
text: text.as_ref().to_owned(),
enabled,
items: Vec::new(),
})),
}
}
pub fn id(&self) -> MenuId {
self.inner.borrow().id.clone()
}
pub fn text(&self) -> String {
self.inner.borrow().text.clone()
}
pub fn set_text(&self, text: impl AsRef<str>) {
self.inner.borrow_mut().text = text.as_ref().to_owned();
}
pub fn is_enabled(&self) -> bool {
self.inner.borrow().enabled
}
pub fn set_enabled(&self, enabled: bool) {
self.inner.borrow_mut().enabled = enabled;
}
pub fn append(&self, item: &dyn IsMenuItem) -> Result<()> {
self.inner.borrow_mut().items.push(item.kind());
Ok(())
}
pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> Result<()> {
for item in items {
self.append(*item)?;
}
Ok(())
}
pub fn prepend(&self, item: &dyn IsMenuItem) -> Result<()> {
self.inner.borrow_mut().items.insert(0, item.kind());
Ok(())
}
pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> Result<()> {
let mut state = self.inner.borrow_mut();
let position = position.min(state.items.len());
state.items.insert(position, item.kind());
Ok(())
}
pub fn remove(&self, item: &dyn IsMenuItem) -> Result<()> {
let mut state = self.inner.borrow_mut();
let target = item.id();
if let Some(pos) = state.items.iter().position(|k| k.id() == target) {
state.items.remove(pos);
Ok(())
} else {
Err(Error::NotAChildOfThisMenu)
}
}
pub fn items(&self) -> Vec<MenuItemKind> {
self.inner.borrow().items.clone()
}
fn to_menu(&self) -> Menu {
let state = self.inner.borrow();
Menu {
inner: Rc::new(RefCell::new(MenuState {
id: state.id.clone(),
items: state.items.clone(),
mode: SurfaceMode::Undetermined,
})),
}
}
}
impl IsMenuItem for Submenu {
fn kind(&self) -> MenuItemKind {
MenuItemKind::Submenu(self.clone())
}
fn id(&self) -> MenuId {
Submenu::id(self)
}
}
impl ContextMenu for Submenu {
fn init_for_nsapp(&self) -> Result<()> {
self.to_menu().route_menu_bar()
}
unsafe fn init_for_hwnd(&self, _hwnd: isize) -> Result<()> {
self.to_menu().route_menu_bar()
}
fn init_for_gtk_window(&self) -> Result<()> {
self.to_menu().route_menu_bar()
}
unsafe fn show_context_menu_for_nsview(
&self,
_nsview: *mut c_void,
position: Option<Position>,
) -> Result<()> {
open_custom(&self.to_menu(), position)
}
unsafe fn show_context_menu_for_hwnd(
&self,
_hwnd: isize,
position: Option<Position>,
) -> Result<()> {
open_custom(&self.to_menu(), position)
}
fn show_context_menu_for_gtk_window(&self, position: Option<Position>) -> Result<()> {
open_custom(&self.to_menu(), position)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::menu::Item;
#[test]
fn menu_id_auto_generation_is_monotonic_and_unique() {
let a = MenuItem::new("A", true, None);
let b = MenuItem::new("B", true, None);
let na: u32 = a.id().0.parse().expect("auto id is an integer");
let nb: u32 = b.id().0.parse().expect("auto id is an integer");
assert!(
nb > na,
"auto ids are strictly increasing and unique (got {na} then {nb})"
);
}
#[test]
fn explicit_id_is_used_verbatim() {
let item = MenuItem::with_id("open", "Open", true, None);
assert_eq!(item.id(), MenuId::from("open"));
}
#[test]
fn from_rgba_rejects_overflowing_dimensions_without_panicking() {
let err = Icon::from_rgba(Vec::new(), u32::MAX, u32::MAX);
assert!(err.is_err(), "overflowing dimensions must error, not panic");
assert!(Icon::from_rgba(vec![0; 4], 1, 1).is_ok());
assert!(Icon::from_rgba(vec![0; 3], 1, 1).is_err());
assert!(Icon::from_rgba(Vec::new(), 0, 0).is_err());
assert!(Icon::from_rgba(Vec::new(), 0, 8).is_err());
assert!(Icon::from_rgba(Vec::new(), 8, 0).is_err());
}
#[test]
fn icon_header_and_tab_submenu_convert_to_leading_icon_and_flex_segments() {
use crate::menu::{Align, Flex, Icon as MuriIcon};
let menu = Menu::new();
let icon = Icon::from_rgba(vec![0u8; 4], 1, 1).expect("valid RGBA");
let header = IconMenuItem::with_id("hdr:claude", "Claude", false, Some(icon), None);
let acct = Submenu::with_id("acct:me", "me@example.com\t47% / 52%", true);
menu.append(&header).unwrap();
menu.append(&acct).unwrap();
let muri = menu.to_muri_menu();
let Item::Row(row) = &muri.items[0] else {
panic!(
"IconMenuItem must convert to Item::Row, got {:?}",
muri.items[0]
);
};
assert!(
matches!(row.leading, Some(MuriIcon::Png(_))),
"provider logo must be the row's leading icon"
);
assert!(
row.trailing.is_none(),
"the logo must not be a trailing icon"
);
let Item::Submenu { label, .. } = &muri.items[1] else {
panic!("Submenu must convert to Item::Submenu");
};
assert_eq!(
label.segments.len(),
2,
"label\\tvalue splits into two segments"
);
assert_eq!(label.segments[0].flex, Flex::Grow, "lead segment grows");
assert_eq!(
label.segments[1].align,
Align::Right,
"value segment is right-aligned"
);
}
#[test]
fn init_for_nsapp_tags_menu_bar_passthrough() {
let menu = Menu::new();
menu.append(&MenuItem::with_id("save", "Save", true, None))
.unwrap();
assert_eq!(menu.mode(), SurfaceMode::Undetermined);
menu.init_for_nsapp().unwrap();
assert_eq!(menu.mode(), SurfaceMode::MenuBar);
}
#[test]
fn build_custom_surface_tags_custom() {
let menu = Menu::new();
menu.append(&MenuItem::with_id("open", "Open", true, None))
.unwrap();
let _surface = menu.build_custom_surface();
assert_eq!(menu.mode(), SurfaceMode::Custom);
}
#[test]
fn item_type_translation_onto_muri_tree() {
let menu = Menu::new();
menu.append(&MenuItem::with_id("open", "Open", true, None))
.unwrap();
menu.append(&CheckMenuItem::with_id("chk", "Notify", true, true, None))
.unwrap();
menu.append(&IconMenuItem::with_native_icon(
"User",
true,
Some(NativeIcon::User),
None,
))
.unwrap();
menu.append(&PredefinedMenuItem::separator()).unwrap();
menu.append(&PredefinedMenuItem::quit(Some("Quit MyApp")))
.unwrap();
let sub = Submenu::with_id("acct", "Account", true);
sub.append(&MenuItem::with_id("switch", "Switch", true, None))
.unwrap();
menu.append(&sub).unwrap();
let muri = menu.to_muri_menu();
assert_eq!(muri.items.len(), 6);
match &muri.items[0] {
Item::Row(r) => {
assert_eq!(r.id, MenuId::from("open"));
assert!(r.enabled);
assert_eq!(r.checked, None);
}
_ => panic!("expected a Row"),
}
match &muri.items[1] {
Item::Row(r) => assert_eq!(r.checked, Some(true)),
_ => panic!("expected a checked Row"),
}
match &muri.items[2] {
Item::Row(r) => assert!(matches!(r.leading, Some(MuriIcon::Symbol(_)))),
_ => panic!("expected an icon Row"),
}
assert!(matches!(muri.items[3], Item::Separator));
match &muri.items[4] {
Item::Row(r) => {
assert!(!r.enabled);
assert_eq!(r.accessible_name(), "Quit MyApp");
}
_ => panic!("expected a disabled predefined Row"),
}
match &muri.items[5] {
Item::Submenu { label, menu } => {
assert_eq!(label.id, MenuId::from("acct"));
assert_eq!(menu.items.len(), 1);
}
_ => panic!("expected a Submenu"),
}
}
#[test]
fn tab_label_becomes_two_columns_and_checked_shows_a_checkmark() {
let menu = Menu::new();
menu.append(&MenuItem::with_id("a", "Account\t47% / 89%", true, None))
.unwrap();
menu.append(&CheckMenuItem::with_id("b", "Active", true, true, None))
.unwrap();
let muri = menu.to_muri_menu();
match &muri.items[0] {
Item::Row(r) => {
assert_eq!(r.segments.len(), 2, "tab splits into two segments");
assert_eq!(r.segments[0].text, "Account");
assert!(matches!(r.segments[0].flex, Flex::Grow));
assert_eq!(r.segments[1].text, "47% / 89%");
assert!(matches!(r.segments[1].align, Align::Right));
}
_ => panic!("expected a two-column Row"),
}
match &muri.items[1] {
Item::Row(r) => {
assert_eq!(r.checked, Some(true));
assert!(
matches!(r.leading, Some(MuriIcon::Checkmark)),
"a checked item shows the leading checkmark"
);
}
_ => panic!("expected a checked Row"),
}
}
#[test]
fn icon_menu_item_raw_rgba_renders_as_a_leading_png() {
let rgba = vec![9, 8, 7, 255];
let icon = Icon::from_rgba(rgba.clone(), 1, 1).expect("valid RGBA");
let menu = Menu::new();
menu.append(&IconMenuItem::new("Claude", true, Some(icon), None))
.unwrap();
let muri = menu.to_muri_menu();
match &muri.items[0] {
Item::Row(r) => match &r.leading {
Some(MuriIcon::Png(bytes)) => {
let (decoded, w, h) =
crate::render::decode_png(bytes).expect("leading icon PNG decodes");
assert_eq!((w, h), (1, 1));
assert_eq!(
decoded, rgba,
"the RGBA round-trips onto the row's leading icon"
);
}
other => panic!("expected a leading PNG icon, got {other:?}"),
},
_ => panic!("expected an icon Row"),
}
}
#[test]
fn items_round_trips_to_kinds() {
let menu = Menu::new();
menu.append(&MenuItem::with_id("a", "A", true, None))
.unwrap();
menu.append(&PredefinedMenuItem::separator()).unwrap();
let kinds = menu.items();
assert_eq!(kinds.len(), 2);
assert!(matches!(kinds[0], MenuItemKind::MenuItem(_)));
assert!(matches!(kinds[1], MenuItemKind::Predefined(ref p) if p.is_separator()));
}
#[test]
fn interior_mutability_setters_take_shared_ref() {
let item = MenuItem::with_id("x", "Before", true, None);
let clone = item.clone();
item.set_text("After");
item.set_enabled(false);
assert_eq!(clone.text(), "After");
assert!(!clone.is_enabled());
}
#[test]
fn remove_absent_item_errors() {
let menu = Menu::new();
let a = MenuItem::with_id("a", "A", true, None);
let b = MenuItem::with_id("b", "B", true, None);
menu.append(&a).unwrap();
assert!(matches!(menu.remove(&b), Err(Error::NotAChildOfThisMenu)));
assert!(menu.remove(&a).is_ok());
}
}