use std::rc::Rc;
use crabshell_reactive::Memo;
use crate::cx::Cx;
use crate::popup::PopupWindow;
use crate::prop::{IntoProp, Prop};
use crate::style::{Color, Style};
use crate::widgets::{
Align, Button, Column, Element, ElementKind, Row, Spacer, Stack, Text, Widget,
};
#[derive(Clone, Debug, PartialEq)]
pub struct MenuEntry {
pub id: i32,
pub label: String,
pub separator: bool,
pub enabled: bool,
pub toggle: Option<MenuToggle>,
pub checked: bool,
pub submenu: bool,
pub children: Vec<MenuEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MenuToggle {
Checkmark,
Radio,
}
const ITEM: Style = Style::new().padding(4, 12);
const HOVER: Style = Style::new().background(Color::rgba(128, 128, 128, 64));
const SEPARATOR: Style = Style::new()
.background(Color::rgba(128, 128, 128, 128))
.padding(1, 0);
const DISABLED: Style = Style::new().opacity(0.5);
const HIDDEN: Style = Style::new().opacity(0.0);
pub struct Menu {
entries: Prop<Vec<MenuEntry>>,
style: Option<Prop<Style>>,
item_style: Option<Prop<Style>>,
hover_style: Option<Prop<Style>>,
separator_style: Option<Prop<Style>>,
on_activate: Option<EntryHandler>,
on_open_submenu: Option<EntryHandler>,
}
impl Menu {
pub fn new(entries: impl IntoProp<Vec<MenuEntry>>) -> Menu {
Menu {
entries: entries.into_prop(),
style: None,
item_style: None,
hover_style: None,
separator_style: None,
on_activate: None,
on_open_submenu: None,
}
}
pub fn style(mut self, style: impl IntoProp<Style>) -> Menu {
self.style = Some(style.into_prop());
self
}
pub fn item_style(mut self, style: impl IntoProp<Style>) -> Menu {
self.item_style = Some(style.into_prop());
self
}
pub fn hover_style(mut self, style: impl IntoProp<Style>) -> Menu {
self.hover_style = Some(style.into_prop());
self
}
pub fn separator_style(mut self, style: impl IntoProp<Style>) -> Menu {
self.separator_style = Some(style.into_prop());
self
}
pub fn on_activate(mut self, handler: impl Fn(&Cx, i32) + 'static) -> Menu {
self.on_activate = Some(Rc::new(handler));
self
}
pub fn on_open_submenu(mut self, handler: impl Fn(&Cx, i32) + 'static) -> Menu {
self.on_open_submenu = Some(Rc::new(handler));
self
}
}
struct Look {
menu: Shared<Style>,
item: Shared<Style>,
hover: Shared<Style>,
separator: Shared<Style>,
on_activate: Option<EntryHandler>,
on_open_submenu: Option<EntryHandler>,
}
type Shared<T> = Rc<dyn Fn() -> T>;
type EntryHandler = Rc<dyn Fn(&Cx, i32)>;
fn shared<T: Clone + 'static>(prop: Option<Prop<T>>, default: T) -> Shared<T> {
prop.map_or_else(|| Rc::new(move || default.clone()), Prop::shared)
}
impl Widget for Menu {
fn into_element(self: Box<Self>) -> Element {
let look = Rc::new(Look {
menu: shared(self.style, Style::new()),
item: shared(self.item_style, ITEM),
hover: shared(self.hover_style, HOVER),
separator: shared(self.separator_style, SEPARATOR),
on_activate: self.on_activate,
on_open_submenu: self.on_open_submenu,
});
menu(look, shared(Some(self.entries), Vec::new()))
}
}
struct Built(Element);
impl Widget for Built {
fn into_element(self: Box<Self>) -> Element {
self.0
}
}
fn menu(look: Rc<Look>, entries: Shared<Vec<MenuEntry>>) -> Element {
let style = Rc::clone(&look.menu);
let column = Column::new().style(move || style()).children_keyed(
move || entries(),
|entry: &MenuEntry| (entry.id, entry.separator),
move |_cx, entry| entry_view(&look, entry),
);
let mut element = Box::new(column).into_element();
element.name = "Menu";
element
}
fn entry_view(look: &Rc<Look>, entry: Memo<MenuEntry>) -> Built {
if entry.with_untracked(|entry| entry.separator) {
let separator = Rc::clone(&look.separator);
let line = Stack::new().style(move || separator());
return Built(Box::new(Column::new().padding(3, 0).child(line)).into_element());
}
let mark = move || match entry.get().toggle {
Some(MenuToggle::Radio) => "●".to_owned(),
_ => "✓".to_owned(),
};
let shown = move || {
let entry = entry.get();
if entry.toggle.is_some() && entry.checked {
Style::new()
} else {
HIDDEN
}
};
let arrow = move || if entry.get().submenu { "▸" } else { "" }.to_owned();
let marks = Stack::new()
.child(Text::new("✓").style(HIDDEN))
.child(Text::new("●").style(HIDDEN))
.child(Text::new(mark).style(shown));
let row = Row::new()
.gap(8)
.align(Align::Center)
.child(marks)
.child(Text::new(move || entry.get().label))
.child(Spacer::new())
.child(Text::new(arrow));
let mut row = Box::new(row).into_element();
if let ElementKind::Flex(flex) = &mut row.kind {
flex.grow = true;
}
let (item, hover) = (Rc::clone(&look.item), Rc::clone(&look.hover));
let clicked = Rc::clone(look);
let button = Button::new(Built(row))
.style(move || {
let style = item();
if entry.get().enabled {
style
} else {
style.merge(DISABLED)
}
})
.hover_style(move || {
if entry.get().enabled {
hover()
} else {
Style::new()
}
})
.on_click(move |cx| click(&clicked, cx, entry));
Built(Box::new(button).into_element())
}
fn click(look: &Rc<Look>, cx: &Cx, entry: Memo<MenuEntry>) {
let (id, enabled, submenu) =
entry.with_untracked(|entry| (entry.id, entry.enabled, entry.submenu));
if !enabled {
return;
}
if submenu {
if let Some(open) = &look.on_open_submenu {
open(cx, id);
}
let look = Rc::clone(look);
let children: Shared<Vec<MenuEntry>> = Rc::new(move || {
let children = entry.try_with(|entry| entry.children.clone());
children.unwrap_or_default()
});
cx.open_popup(move |_| PopupWindow::new().child(Built(menu(look, children))));
return;
}
if let Some(activate) = &look.on_activate {
activate(cx, id);
}
cx.close_popups_if_any();
}