use std::any::Any;
use std::sync::{RwLock, Arc, OnceLock};
use super::{Element, ElementPtr, ViewLimits, ViewStretch, share};
use super::context::{BasicContext, Context};
use crate::support::point::Point;
use crate::support::rect::Rect;
use crate::support::color::Color;
use crate::support::theme::get_theme;
use crate::view::{MouseButton, MouseButtonKind, CursorTracking};
pub type MenuItemCallback = Box<dyn Fn() + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MenuModifiers {
pub command: bool,
pub shift: bool,
pub option: bool,
pub control: bool,
}
impl MenuModifiers {
pub fn none() -> Self {
Self::default()
}
pub fn command() -> Self {
Self { command: true, ..Default::default() }
}
pub fn shift() -> Self {
Self { shift: true, ..Default::default() }
}
pub fn option() -> Self {
Self { option: true, ..Default::default() }
}
pub fn control() -> Self {
Self { control: true, ..Default::default() }
}
pub fn with_command(mut self) -> Self {
self.command = true;
self
}
pub fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub fn with_option(mut self) -> Self {
self.option = true;
self
}
pub fn with_control(mut self) -> Self {
self.control = true;
self
}
}
#[derive(Debug, Clone)]
pub struct MenuShortcut {
pub key: char,
pub modifiers: MenuModifiers,
}
impl MenuShortcut {
pub fn cmd(key: char) -> Self {
Self {
key,
modifiers: MenuModifiers::command(),
}
}
pub fn cmd_shift(key: char) -> Self {
Self {
key,
modifiers: MenuModifiers::command().with_shift(),
}
}
pub fn cmd_option(key: char) -> Self {
Self {
key,
modifiers: MenuModifiers::command().with_option(),
}
}
pub fn with_modifiers(key: char, modifiers: MenuModifiers) -> Self {
Self { key, modifiers }
}
pub fn display_string(&self) -> String {
let mut s = String::new();
if self.modifiers.control {
s.push_str("Ctrl+");
}
if self.modifiers.option {
s.push_str("Opt+");
}
if self.modifiers.shift {
s.push_str("Shift+");
}
if self.modifiers.command {
s.push_str("Cmd+");
}
s.push(self.key.to_ascii_uppercase());
s
}
}
pub struct MenuItem {
label: String,
shortcut: Option<String>,
enabled: bool,
checked: bool,
submenu: Option<Vec<MenuItem>>,
on_select: Option<MenuItemCallback>,
hover: RwLock<bool>,
}
impl MenuItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
shortcut: None,
enabled: true,
checked: false,
submenu: None,
on_select: None,
hover: RwLock::new(false),
}
}
pub fn separator() -> Self {
Self {
label: String::new(),
shortcut: None,
enabled: false,
checked: false,
submenu: None,
on_select: None,
hover: RwLock::new(false),
}
}
pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
self.shortcut = Some(shortcut.into());
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn submenu(mut self, items: Vec<MenuItem>) -> Self {
self.submenu = Some(items);
self
}
pub fn on_select<F: Fn() + Send + Sync + 'static>(mut self, callback: F) -> Self {
self.on_select = Some(Box::new(callback));
self
}
pub fn is_separator(&self) -> bool {
self.label.is_empty()
}
pub fn label(&self) -> &str {
&self.label
}
fn height(&self) -> f32 {
if self.is_separator() {
8.0
} else {
28.0
}
}
}
pub struct Menu {
items: Vec<MenuItem>,
background_color: Color,
hover_color: Color,
text_color: Color,
disabled_color: Color,
check_color: Color,
separator_color: Color,
corner_radius: f32,
padding: f32,
min_width: f32,
visible: RwLock<bool>,
hovered_index: RwLock<Option<usize>>,
}
impl Menu {
pub fn new(items: Vec<MenuItem>) -> Self {
let theme = get_theme();
Self {
items,
background_color: theme.menu_background_color,
hover_color: theme.menu_item_hilite_color,
text_color: theme.menu_font_color,
disabled_color: theme.menu_font_color.with_alpha(0.5),
check_color: theme.indicator_bright_color,
separator_color: theme.menu_separator_color,
corner_radius: 6.0,
padding: 4.0,
min_width: 150.0,
visible: RwLock::new(false),
hovered_index: RwLock::new(None),
}
}
pub fn background_color(mut self, color: Color) -> Self {
self.background_color = color;
self
}
pub fn min_width(mut self, width: f32) -> Self {
self.min_width = width;
self
}
pub fn show(&self) {
*self.visible.write().unwrap() = true;
}
pub fn hide(&self) {
*self.visible.write().unwrap() = false;
*self.hovered_index.write().unwrap() = None;
}
pub fn is_visible(&self) -> bool {
*self.visible.read().unwrap()
}
fn calculate_size(&self) -> (f32, f32) {
let theme = get_theme();
let mut max_width = self.min_width;
let mut total_height = self.padding * 2.0;
for item in &self.items {
let text_width = item.label.len() as f32 * theme.menu_font_size * 0.6;
let shortcut_width = item.shortcut.as_ref()
.map(|s| s.len() as f32 * theme.menu_font_size * 0.5 + 20.0)
.unwrap_or(0.0);
let item_width = self.padding * 2.0 + 24.0 + text_width + shortcut_width + 8.0;
max_width = max_width.max(item_width);
total_height += item.height();
}
(max_width, total_height)
}
fn item_bounds(&self, ctx: &Context, index: usize) -> Rect {
let mut y = ctx.bounds.top + self.padding;
for (i, item) in self.items.iter().enumerate() {
let height = item.height();
if i == index {
return Rect::new(
ctx.bounds.left + self.padding,
y,
ctx.bounds.right - self.padding,
y + height,
);
}
y += height;
}
Rect::zero()
}
fn draw_background(&self, ctx: &Context) {
let mut canvas = ctx.canvas.borrow_mut();
let shadow_rect = ctx.bounds.translate(2.0, 2.0);
canvas.fill_style(Color::new(0.0, 0.0, 0.0, 0.3));
canvas.fill_round_rect(shadow_rect, self.corner_radius);
canvas.fill_style(self.background_color);
canvas.fill_round_rect(ctx.bounds, self.corner_radius);
}
fn draw_item(&self, ctx: &Context, item: &MenuItem, bounds: Rect, hovered: bool) {
let mut canvas = ctx.canvas.borrow_mut();
let theme = get_theme();
if item.is_separator() {
let y = bounds.center().y;
canvas.stroke_style(self.separator_color);
canvas.line_width(1.0);
canvas.begin_path();
canvas.move_to(Point::new(bounds.left + 8.0, y));
canvas.line_to(Point::new(bounds.right - 8.0, y));
canvas.stroke();
return;
}
if hovered && item.enabled {
canvas.fill_style(self.hover_color);
canvas.fill_round_rect(bounds, 4.0);
}
let text_color = if item.enabled {
self.text_color
} else {
self.disabled_color
};
if item.checked {
canvas.fill_style(self.check_color);
let check_x = bounds.left + 8.0;
let check_y = bounds.center().y;
canvas.fill_text("✓", Point::new(check_x, check_y + 4.0));
}
canvas.fill_style(text_color);
canvas.font_size(theme.menu_font_size);
let x = bounds.left + 24.0;
let y = bounds.center().y + theme.menu_font_size * 0.35;
canvas.fill_text(&item.label, Point::new(x, y));
if let Some(ref shortcut) = item.shortcut {
let shortcut_color = text_color.with_alpha(0.6);
canvas.fill_style(shortcut_color);
let shortcut_x = bounds.right - 8.0 - shortcut.len() as f32 * theme.menu_font_size * 0.5;
canvas.fill_text(shortcut, Point::new(shortcut_x, y));
}
if item.submenu.is_some() {
canvas.fill_style(text_color);
canvas.fill_text("â–¶", Point::new(bounds.right - 16.0, y));
}
}
}
impl Element for Menu {
fn limits(&self, _ctx: &BasicContext) -> ViewLimits {
let (width, height) = self.calculate_size();
ViewLimits::fixed(width, height)
}
fn stretch(&self) -> ViewStretch {
ViewStretch::new(0.0, 0.0)
}
fn draw(&self, ctx: &Context) {
if !self.is_visible() {
return;
}
self.draw_background(ctx);
let hovered = *self.hovered_index.read().unwrap();
for (i, item) in self.items.iter().enumerate() {
let bounds = self.item_bounds(ctx, i);
let is_hovered = hovered == Some(i);
self.draw_item(ctx, item, bounds, is_hovered);
}
}
fn hit_test(&self, ctx: &Context, p: Point, _leaf: bool, _control: bool) -> Option<&dyn Element> {
if self.is_visible() && ctx.bounds.contains(p) {
Some(self)
} else {
None
}
}
fn wants_control(&self) -> bool {
self.is_visible()
}
fn handle_click(&self, ctx: &Context, btn: MouseButton) -> bool {
if !self.is_visible() || btn.button != MouseButtonKind::Left {
return false;
}
if !btn.down {
for (i, item) in self.items.iter().enumerate() {
if !item.is_separator() && item.enabled {
let bounds = self.item_bounds(ctx, i);
if bounds.contains(btn.pos) {
if let Some(ref callback) = item.on_select {
callback();
}
self.hide();
return true;
}
}
}
if !ctx.bounds.contains(btn.pos) {
self.hide();
}
}
true
}
fn cursor(&mut self, ctx: &Context, p: Point, status: CursorTracking) -> bool {
if !self.is_visible() {
return false;
}
match status {
CursorTracking::Leaving => {
*self.hovered_index.write().unwrap() = None;
}
_ => {
let mut hovered = self.hovered_index.write().unwrap();
*hovered = None;
for (i, item) in self.items.iter().enumerate() {
if !item.is_separator() {
let bounds = self.item_bounds(ctx, i);
if bounds.contains(p) {
*hovered = Some(i);
break;
}
}
}
}
}
true
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
pub struct Popup {
content: Option<ElementPtr>,
visible: RwLock<bool>,
background_color: Color,
corner_radius: f32,
shadow: bool,
}
impl Popup {
pub fn new() -> Self {
let theme = get_theme();
Self {
content: None,
visible: RwLock::new(false),
background_color: theme.menu_background_color,
corner_radius: 8.0,
shadow: true,
}
}
pub fn content<E: Element + 'static>(mut self, content: E) -> Self {
self.content = Some(share(content));
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.background_color = color;
self
}
pub fn shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
pub fn show(&self) {
*self.visible.write().unwrap() = true;
}
pub fn hide(&self) {
*self.visible.write().unwrap() = false;
}
pub fn is_visible(&self) -> bool {
*self.visible.read().unwrap()
}
pub fn toggle(&self) {
let mut visible = self.visible.write().unwrap();
*visible = !*visible;
}
}
impl Default for Popup {
fn default() -> Self {
Self::new()
}
}
impl Element for Popup {
fn limits(&self, ctx: &BasicContext) -> ViewLimits {
if let Some(ref content) = self.content {
content.limits(ctx)
} else {
ViewLimits::fixed(100.0, 100.0)
}
}
fn stretch(&self) -> ViewStretch {
ViewStretch::new(0.0, 0.0)
}
fn draw(&self, ctx: &Context) {
if !self.is_visible() {
return;
}
let mut canvas = ctx.canvas.borrow_mut();
if self.shadow {
let shadow_rect = ctx.bounds.translate(3.0, 3.0);
canvas.fill_style(Color::new(0.0, 0.0, 0.0, 0.25));
canvas.fill_round_rect(shadow_rect, self.corner_radius);
}
canvas.fill_style(self.background_color);
canvas.fill_round_rect(ctx.bounds, self.corner_radius);
drop(canvas);
if let Some(ref content) = self.content {
let inset = 8.0;
let content_bounds = ctx.bounds.inset(inset, inset);
let content_ctx = ctx.with_bounds(content_bounds);
content.draw(&content_ctx);
}
}
fn hit_test(&self, ctx: &Context, p: Point, leaf: bool, control: bool) -> Option<&dyn Element> {
if !self.is_visible() {
return None;
}
if ctx.bounds.contains(p) {
if let Some(ref content) = self.content {
let inset = 8.0;
let content_bounds = ctx.bounds.inset(inset, inset);
let content_ctx = ctx.with_bounds(content_bounds);
if let Some(hit) = content.hit_test(&content_ctx, p, leaf, control) {
return Some(hit);
}
}
Some(self)
} else {
None
}
}
fn wants_control(&self) -> bool {
self.is_visible()
}
fn handle_click(&self, ctx: &Context, btn: MouseButton) -> bool {
if !self.is_visible() {
return false;
}
if !btn.down && !ctx.bounds.contains(btn.pos) {
self.hide();
return true;
}
if let Some(ref content) = self.content {
let inset = 8.0;
let content_bounds = ctx.bounds.inset(inset, inset);
let content_ctx = ctx.with_bounds(content_bounds);
if content.handle_click(&content_ctx, btn) {
return true;
}
}
true
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Clone)]
pub struct NativeMenuItem {
pub label: String,
pub shortcut: Option<MenuShortcut>,
pub enabled: bool,
pub checked: bool,
pub submenu: Option<Vec<NativeMenuItem>>,
pub action: Option<Arc<dyn Fn() + Send + Sync>>,
pub id: Option<String>,
}
impl NativeMenuItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
shortcut: None,
enabled: true,
checked: false,
submenu: None,
action: None,
id: None,
}
}
pub fn separator() -> Self {
Self {
label: String::new(),
shortcut: None,
enabled: false,
checked: false,
submenu: None,
action: None,
id: None,
}
}
pub fn is_separator(&self) -> bool {
self.label.is_empty() && self.submenu.is_none()
}
pub fn shortcut_cmd(mut self, key: char) -> Self {
self.shortcut = Some(MenuShortcut::cmd(key));
self
}
pub fn shortcut_cmd_shift(mut self, key: char) -> Self {
self.shortcut = Some(MenuShortcut::cmd_shift(key));
self
}
pub fn shortcut_cmd_option(mut self, key: char) -> Self {
self.shortcut = Some(MenuShortcut::cmd_option(key));
self
}
pub fn shortcut(mut self, shortcut: MenuShortcut) -> Self {
self.shortcut = Some(shortcut);
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn submenu(mut self, items: Vec<NativeMenuItem>) -> Self {
self.submenu = Some(items);
self
}
pub fn on_select<F: Fn() + Send + Sync + 'static>(mut self, callback: F) -> Self {
self.action = Some(Arc::new(callback));
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
}
#[derive(Clone)]
pub struct NativeMenu {
pub title: String,
pub items: Vec<NativeMenuItem>,
}
impl NativeMenu {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
items: Vec::new(),
}
}
pub fn with_items(title: impl Into<String>, items: Vec<NativeMenuItem>) -> Self {
Self {
title: title.into(),
items,
}
}
pub fn add_item(mut self, item: NativeMenuItem) -> Self {
self.items.push(item);
self
}
pub fn add_separator(mut self) -> Self {
self.items.push(NativeMenuItem::separator());
self
}
pub fn add_items(mut self, items: Vec<NativeMenuItem>) -> Self {
self.items.extend(items);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardAction {
About,
Preferences,
Hide,
HideOthers,
ShowAll,
Quit,
Undo,
Redo,
Cut,
Copy,
Paste,
SelectAll,
Delete,
Minimize,
Zoom,
BringAllToFront,
Close,
}
#[derive(Clone, Default)]
pub struct NativeMenuBar {
pub app_name: Option<String>,
pub menus: Vec<NativeMenu>,
pub include_app_menu: bool,
pub include_edit_menu: bool,
pub include_window_menu: bool,
}
impl NativeMenuBar {
pub fn new() -> Self {
Self {
app_name: None,
menus: Vec::new(),
include_app_menu: true,
include_edit_menu: true,
include_window_menu: true,
}
}
pub fn app_name(mut self, name: impl Into<String>) -> Self {
self.app_name = Some(name.into());
self
}
pub fn add_menu(mut self, menu: NativeMenu) -> Self {
self.menus.push(menu);
self
}
pub fn include_app_menu(mut self, include: bool) -> Self {
self.include_app_menu = include;
self
}
pub fn include_edit_menu(mut self, include: bool) -> Self {
self.include_edit_menu = include;
self
}
pub fn include_window_menu(mut self, include: bool) -> Self {
self.include_window_menu = include;
self
}
pub fn file_menu(items: Vec<NativeMenuItem>) -> NativeMenu {
NativeMenu::with_items("File", items)
}
pub fn standard_file_menu() -> NativeMenu {
NativeMenu::with_items("File", vec![
NativeMenuItem::new("New").shortcut_cmd('n'),
NativeMenuItem::new("Open...").shortcut_cmd('o'),
NativeMenuItem::separator(),
NativeMenuItem::new("Save").shortcut_cmd('s'),
NativeMenuItem::new("Save As...").shortcut_cmd_shift('s'),
NativeMenuItem::separator(),
NativeMenuItem::new("Close").shortcut_cmd('w'),
])
}
pub fn view_menu(items: Vec<NativeMenuItem>) -> NativeMenu {
NativeMenu::with_items("View", items)
}
pub fn help_menu(items: Vec<NativeMenuItem>) -> NativeMenu {
NativeMenu::with_items("Help", items)
}
}
static NATIVE_MENU_BAR: OnceLock<RwLock<Option<NativeMenuBar>>> = OnceLock::new();
pub fn set_native_menu_bar(menu_bar: NativeMenuBar) {
let storage = NATIVE_MENU_BAR.get_or_init(|| RwLock::new(None));
*storage.write().unwrap() = Some(menu_bar);
}
pub fn get_native_menu_bar() -> Option<NativeMenuBar> {
NATIVE_MENU_BAR.get()
.and_then(|storage| storage.read().ok())
.and_then(|guard| guard.clone())
}
pub fn menu(items: Vec<MenuItem>) -> Menu {
Menu::new(items)
}
pub fn menu_item(label: impl Into<String>) -> MenuItem {
MenuItem::new(label)
}
pub fn menu_separator() -> MenuItem {
MenuItem::separator()
}
pub fn popup() -> Popup {
Popup::new()
}
pub fn native_menu_item(label: impl Into<String>) -> NativeMenuItem {
NativeMenuItem::new(label)
}
pub fn native_separator() -> NativeMenuItem {
NativeMenuItem::separator()
}
pub fn native_menu(title: impl Into<String>) -> NativeMenu {
NativeMenu::new(title)
}
pub fn native_menu_bar() -> NativeMenuBar {
NativeMenuBar::new()
}