use std::num::NonZeroU32;
use crate::core::CommandQueue;
use crate::kurbo::Point;
use crate::shell::{Counter, HotKey, IntoKey, Menu as PlatformMenu};
use crate::widget::LabelText;
use crate::{ArcStr, Command, Data, Env, Lens, RawMods, Target, WindowId};
static COUNTER: Counter = Counter::new();
pub mod sys;
type MenuBuild<T> = Box<dyn FnMut(Option<WindowId>, &T, &Env) -> Menu<T>>;
pub(crate) struct MenuManager<T> {
build: Option<MenuBuild<T>>,
popup: bool,
old_data: Option<T>,
menu: Option<Menu<T>>,
}
pub(crate) struct ContextMenu<T> {
pub(crate) menu: Menu<T>,
pub(crate) location: Point,
}
impl<T: Data> MenuManager<T> {
pub fn new(
build: impl FnMut(Option<WindowId>, &T, &Env) -> Menu<T> + 'static,
) -> MenuManager<T> {
MenuManager {
build: Some(Box::new(build)),
popup: false,
old_data: None,
menu: None,
}
}
pub fn new_for_popup(menu: Menu<T>) -> MenuManager<T> {
MenuManager {
build: None,
popup: true,
old_data: None,
menu: Some(menu),
}
}
#[allow(unreachable_code)]
pub fn platform_default() -> Option<MenuManager<T>> {
#[cfg(target_os = "macos")]
return Some(MenuManager::new(|_, _, _| sys::mac::menu_bar()));
#[cfg(any(
target_os = "windows",
target_os = "freebsd",
target_os = "linux",
target_os = "openbsd"
))]
return None;
tracing::warn!("MenuManager::platform_default is not implemented for this platform.");
None
}
pub fn event(
&mut self,
queue: &mut CommandQueue,
window: Option<WindowId>,
id: MenuItemId,
data: &mut T,
env: &Env,
) {
if let Some(m) = &mut self.menu {
let mut ctx = MenuEventCtx { window, queue };
m.activate(&mut ctx, id, data, env);
}
}
pub fn initialize(&mut self, window: Option<WindowId>, data: &T, env: &Env) -> PlatformMenu {
if let Some(build) = &mut self.build {
self.menu = Some((build)(window, data, env));
}
self.old_data = Some(data.clone());
self.refresh(data, env)
}
pub fn update(
&mut self,
window: Option<WindowId>,
data: &T,
env: &Env,
) -> Option<PlatformMenu> {
if let (Some(menu), Some(old_data)) = (self.menu.as_mut(), self.old_data.as_ref()) {
let ret = match menu.update(old_data, data, env) {
MenuUpdate::NeedsRebuild => {
if let Some(build) = &mut self.build {
self.menu = Some((build)(window, data, env));
} else {
tracing::warn!("tried to rebuild a context menu");
}
Some(self.refresh(data, env))
}
MenuUpdate::NeedsRefresh => Some(self.refresh(data, env)),
MenuUpdate::UpToDate => None,
};
self.old_data = Some(data.clone());
ret
} else {
tracing::error!("tried to update uninitialized menus");
None
}
}
pub fn refresh(&mut self, data: &T, env: &Env) -> PlatformMenu {
if let Some(menu) = self.menu.as_mut() {
let mut ctx = MenuBuildCtx::new(self.popup);
menu.refresh_children(&mut ctx, data, env);
ctx.current
} else {
tracing::error!("tried to refresh uninitialized menus");
PlatformMenu::new()
}
}
}
pub struct MenuEventCtx<'a> {
window: Option<WindowId>,
queue: &'a mut CommandQueue,
}
struct MenuBuildCtx {
current: PlatformMenu,
}
impl MenuBuildCtx {
fn new(popup: bool) -> MenuBuildCtx {
MenuBuildCtx {
current: if popup {
PlatformMenu::new_for_popup()
} else {
PlatformMenu::new()
},
}
}
fn with_submenu(&mut self, text: &str, enabled: bool, f: impl FnOnce(&mut MenuBuildCtx)) {
let mut child = MenuBuildCtx::new(false);
f(&mut child);
self.current.add_dropdown(child.current, text, enabled);
}
fn add_item(
&mut self,
id: u32,
text: &str,
key: Option<&HotKey>,
selected: Option<bool>,
enabled: bool,
) {
self.current.add_item(id, text, key, selected, enabled);
}
fn add_separator(&mut self) {
self.current.add_separator();
}
}
impl<'a> MenuEventCtx<'a> {
pub fn submit_command(&mut self, cmd: impl Into<Command>) {
self.queue.push_back(
cmd.into()
.default_to(self.window.map(Target::Window).unwrap_or(Target::Global)),
);
}
}
#[derive(Clone, Copy, Debug)]
enum MenuUpdate {
NeedsRefresh,
NeedsRebuild,
UpToDate,
}
impl MenuUpdate {
fn combine(self, other: MenuUpdate) -> MenuUpdate {
use MenuUpdate::*;
match (self, other) {
(NeedsRebuild, _) | (_, NeedsRebuild) => NeedsRebuild,
(NeedsRefresh, _) | (_, NeedsRefresh) => NeedsRefresh,
_ => UpToDate,
}
}
}
trait MenuVisitor<T> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env);
fn update(&mut self, old_data: &T, data: &T, env: &Env) -> MenuUpdate;
fn refresh(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env);
}
pub struct MenuLensWrap<L, U> {
lens: L,
inner: Box<dyn MenuVisitor<U>>,
old_data: Option<U>,
old_env: Option<Env>,
}
impl<T: Data, U: Data, L: Lens<T, U>> MenuVisitor<T> for MenuLensWrap<L, U> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env) {
let inner = &mut self.inner;
self.lens
.with_mut(data, |u| inner.activate(ctx, id, u, env));
}
fn update(&mut self, old_data: &T, data: &T, env: &Env) -> MenuUpdate {
let inner = &mut self.inner;
let lens = &self.lens;
let cached_old_data = &mut self.old_data;
let cached_old_env = &mut self.old_env;
lens.with(old_data, |old| {
lens.with(data, |new| {
let ret = if cached_old_data.as_ref().map(|x| x.same(old)) == Some(true)
&& cached_old_env.as_ref().map(|x| x.same(env)) == Some(true)
{
MenuUpdate::UpToDate
} else {
inner.update(old, new, env)
};
*cached_old_data = Some(new.clone());
*cached_old_env = Some(env.clone());
ret
})
})
}
fn refresh(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env) {
let inner = &mut self.inner;
self.lens.with(data, |u| inner.refresh(ctx, u, env))
}
}
impl<T: Data, U: Data, L: Lens<T, U> + 'static> From<MenuLensWrap<L, U>> for MenuEntry<T> {
fn from(m: MenuLensWrap<L, U>) -> MenuEntry<T> {
MenuEntry { inner: Box::new(m) }
}
}
pub struct MenuEntry<T> {
inner: Box<dyn MenuVisitor<T>>,
}
type MenuPredicate<T> = Box<dyn FnMut(&T, &T, &Env) -> bool>;
pub struct Menu<T> {
rebuild_on: Option<MenuPredicate<T>>,
refresh_on: Option<MenuPredicate<T>>,
item: MenuItem<T>,
children: Vec<MenuEntry<T>>,
}
#[doc(hidden)]
#[deprecated(since = "0.8.0", note = "Renamed to Menu")]
pub type MenuDesc<T> = Menu<T>;
impl<T: Data> From<Menu<T>> for MenuEntry<T> {
fn from(menu: Menu<T>) -> MenuEntry<T> {
MenuEntry {
inner: Box::new(menu),
}
}
}
type MenuCallback<T> = Box<dyn FnMut(&mut MenuEventCtx, &mut T, &Env)>;
type HotKeyCallback<T> = Box<dyn FnMut(&T, &Env) -> Option<HotKey>>;
pub struct MenuItem<T> {
id: MenuItemId,
title: LabelText<T>,
callback: Option<MenuCallback<T>>,
hotkey: Option<HotKeyCallback<T>>,
selected: Option<Box<dyn FnMut(&T, &Env) -> bool>>,
enabled: Option<Box<dyn FnMut(&T, &Env) -> bool>>,
old_state: Option<MenuItemState>,
}
impl<T: Data> From<MenuItem<T>> for MenuEntry<T> {
fn from(i: MenuItem<T>) -> MenuEntry<T> {
MenuEntry { inner: Box::new(i) }
}
}
struct Separator;
impl<T: Data> From<Separator> for MenuEntry<T> {
fn from(s: Separator) -> MenuEntry<T> {
MenuEntry { inner: Box::new(s) }
}
}
impl<T: Data> Menu<T> {
pub fn empty() -> Menu<T> {
Menu {
rebuild_on: None,
refresh_on: None,
item: MenuItem::new(""),
children: Vec::new(),
}
}
pub fn new(title: impl Into<LabelText<T>>) -> Menu<T> {
Menu {
rebuild_on: None,
refresh_on: None,
item: MenuItem::new(title),
children: Vec::new(),
}
}
pub fn enabled_if(mut self, enabled: impl FnMut(&T, &Env) -> bool + 'static) -> Self {
self.item = self.item.enabled_if(enabled);
self
}
pub fn enabled(self, enabled: bool) -> Self {
self.enabled_if(move |_data, _env| enabled)
}
#[doc(hidden)]
#[deprecated(since = "0.8.0", note = "use entry instead")]
pub fn append_entry(self, entry: impl Into<MenuEntry<T>>) -> Self {
self.entry(entry)
}
#[doc(hidden)]
#[deprecated(since = "0.8.0", note = "use separator instead")]
pub fn append_separator(self) -> Self {
self.separator()
}
pub fn entry(mut self, entry: impl Into<MenuEntry<T>>) -> Self {
self.children.push(entry.into());
self
}
pub fn separator(self) -> Self {
self.entry(Separator)
}
pub fn refresh_on(mut self, refresh: impl FnMut(&T, &T, &Env) -> bool + 'static) -> Self {
self.refresh_on = Some(Box::new(refresh));
self
}
pub fn rebuild_on(mut self, rebuild: impl FnMut(&T, &T, &Env) -> bool + 'static) -> Self {
self.rebuild_on = Some(Box::new(rebuild));
self
}
pub fn lens<S: Data>(self, lens: impl Lens<S, T> + 'static) -> MenuEntry<S> {
MenuLensWrap {
lens,
inner: Box::new(self),
old_data: None,
old_env: None,
}
.into()
}
fn refresh_children(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env) {
self.item.resolve(data, env);
for child in &mut self.children {
child.refresh(ctx, data, env);
}
}
}
impl<T: Data> MenuItem<T> {
pub fn new(title: impl Into<LabelText<T>>) -> MenuItem<T> {
let mut id = COUNTER.next() as u32;
if id == 0 {
id = COUNTER.next() as u32;
}
MenuItem {
id: MenuItemId(std::num::NonZeroU32::new(id)),
title: title.into(),
callback: None,
hotkey: None,
selected: None,
enabled: None,
old_state: None,
}
}
pub fn on_activate(
mut self,
on_activate: impl FnMut(&mut MenuEventCtx, &mut T, &Env) + 'static,
) -> Self {
self.callback = Some(Box::new(on_activate));
self
}
pub fn command(self, cmd: impl Into<Command>) -> Self {
let cmd = cmd.into();
self.on_activate(move |ctx, _data, _env| ctx.submit_command(cmd.clone()))
}
pub fn hotkey(self, mods: impl Into<Option<RawMods>>, key: impl IntoKey) -> Self {
let hotkey = HotKey::new(mods, key);
self.dynamic_hotkey(move |_, _| Some(hotkey.clone()))
}
pub fn dynamic_hotkey(
mut self,
hotkey: impl FnMut(&T, &Env) -> Option<HotKey> + 'static,
) -> Self {
self.hotkey = Some(Box::new(hotkey));
self
}
pub fn enabled_if(mut self, enabled: impl FnMut(&T, &Env) -> bool + 'static) -> Self {
self.enabled = Some(Box::new(enabled));
self
}
pub fn enabled(self, enabled: bool) -> Self {
self.enabled_if(move |_data, _env| enabled)
}
pub fn selected_if(mut self, selected: impl FnMut(&T, &Env) -> bool + 'static) -> Self {
self.selected = Some(Box::new(selected));
self
}
pub fn selected(self, selected: bool) -> Self {
self.selected_if(move |_data, _env| selected)
}
pub fn lens<S: Data>(self, lens: impl Lens<S, T> + 'static) -> MenuEntry<S> {
MenuLensWrap {
lens,
inner: Box::new(self),
old_data: None,
old_env: None,
}
.into()
}
fn resolve(&mut self, data: &T, env: &Env) -> bool {
self.title.resolve(data, env);
let new_state = MenuItemState {
title: self.title.display_text(),
hotkey: self.hotkey.as_mut().and_then(|h| h(data, env)),
selected: self.selected.as_mut().map(|s| s(data, env)),
enabled: self.enabled.as_mut().map(|e| e(data, env)).unwrap_or(true),
};
let ret = self.old_state.as_ref() != Some(&new_state);
self.old_state = Some(new_state);
ret
}
fn text(&self) -> &str {
&self.old_state.as_ref().unwrap().title
}
fn is_enabled(&self) -> bool {
self.old_state.as_ref().unwrap().enabled
}
}
impl<T: Data> MenuVisitor<T> for Menu<T> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env) {
for child in &mut self.children {
child.activate(ctx, id, data, env);
}
}
fn update(&mut self, old_data: &T, data: &T, env: &Env) -> MenuUpdate {
if let Some(rebuild_on) = &mut self.rebuild_on {
if rebuild_on(old_data, data, env) {
return MenuUpdate::NeedsRebuild;
}
}
if let Some(refresh_on) = &mut self.refresh_on {
if refresh_on(old_data, data, env) {
return MenuUpdate::NeedsRefresh;
}
}
let mut ret = self.item.update(old_data, data, env);
for child in &mut self.children {
ret = ret.combine(child.update(old_data, data, env));
}
ret
}
fn refresh(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env) {
self.item.resolve(data, env);
let children = &mut self.children;
ctx.with_submenu(self.item.text(), self.item.is_enabled(), |ctx| {
for child in children {
child.refresh(ctx, data, env);
}
});
}
}
impl<T: Data> MenuVisitor<T> for MenuEntry<T> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env) {
self.inner.activate(ctx, id, data, env);
}
fn update(&mut self, old_data: &T, data: &T, env: &Env) -> MenuUpdate {
self.inner.update(old_data, data, env)
}
fn refresh(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env) {
self.inner.refresh(ctx, data, env);
}
}
impl<T: Data> MenuVisitor<T> for MenuItem<T> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env) {
if id == self.id {
if let Some(callback) = &mut self.callback {
callback(ctx, data, env);
}
}
}
fn update(&mut self, _old_data: &T, data: &T, env: &Env) -> MenuUpdate {
if self.resolve(data, env) {
MenuUpdate::NeedsRefresh
} else {
MenuUpdate::UpToDate
}
}
fn refresh(&mut self, ctx: &mut MenuBuildCtx, data: &T, env: &Env) {
self.resolve(data, env);
let state = self.old_state.as_ref().unwrap();
ctx.add_item(
self.id.0.map(|x| x.get()).unwrap_or(0),
&state.title,
state.hotkey.as_ref(),
state.selected,
state.enabled,
);
}
}
impl<T: Data> MenuVisitor<T> for Separator {
fn activate(&mut self, _ctx: &mut MenuEventCtx, _id: MenuItemId, _data: &mut T, _env: &Env) {}
fn update(&mut self, _old_data: &T, _data: &T, _env: &Env) -> MenuUpdate {
MenuUpdate::UpToDate
}
fn refresh(&mut self, ctx: &mut MenuBuildCtx, _data: &T, _env: &Env) {
ctx.add_separator();
}
}
#[derive(PartialEq)]
struct MenuItemState {
title: ArcStr,
hotkey: Option<HotKey>,
selected: Option<bool>,
enabled: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MenuItemId(Option<NonZeroU32>);
impl MenuItemId {
pub(crate) fn new(id: u32) -> MenuItemId {
MenuItemId(NonZeroU32::new(id))
}
}