use std::rc::Rc;
use crabshell_reactive::Memo;
use crabshell_ui::{
Align, Button, Color, Cx, Element, Icon, IconSource, IntoProp, Menu, PopupWindow, Prop, Row,
Style, Widget,
};
use super::{TrayHost, TrayItem};
use crate::ServicesExt;
const MENU: Style = Style::new()
.background(Color::rgb(0x2b, 0x2b, 0x2b))
.color(Color::rgb(0xe6, 0xe6, 0xe6))
.radius(6.0)
.padding(4, 0);
const BUTTON: Style = Style::new().padding(2, 4).radius(4.0);
const ICON_SIZE: u32 = 16;
const NO_ICON: &str = "image-missing";
type Shared<T> = Rc<dyn Fn() -> T>;
#[derive(Default)]
pub struct Tray {
icon_size: Option<Prop<u32>>,
menu_style: Option<Prop<Style>>,
}
impl Tray {
pub fn new() -> Tray {
Tray::default()
}
pub fn icon_size(mut self, size: impl IntoProp<u32>) -> Tray {
self.icon_size = Some(size.into_prop());
self
}
pub fn menu_style(mut self, style: impl IntoProp<Style>) -> Tray {
self.menu_style = Some(style.into_prop());
self
}
}
impl Widget for Tray {
fn into_element(self: Box<Self>) -> Element {
let size: Shared<u32> = self
.icon_size
.map_or_else(|| Rc::new(|| ICON_SIZE), Prop::shared);
let style: Shared<Style> = self
.menu_style
.map_or_else(|| Rc::new(|| MENU), Prop::shared);
let tray = Row::new().children_keyed(
|| vec![()],
|_| (),
move |cx, _| {
let host = cx.tray();
let items = host.items();
let (size, style) = (Rc::clone(&size), Rc::clone(&style));
Row::new().align(Align::Center).children_keyed(
move || items.get(),
|item| item.id.clone(),
move |_, item| item_button(&host, item, &size, &style),
)
},
);
Box::new(tray).into_element()
}
}
fn item_button(
host: &TrayHost,
item: Memo<TrayItem>,
size: &Shared<u32>,
style: &Shared<Style>,
) -> Button {
let id: Rc<str> = item.with_untracked(|item| item.id.as_str().into());
let (icon_size, pick) = (Rc::clone(size), Rc::clone(size));
let icon = Icon::new(move || {
let pixels = pick().saturating_mul(2);
let source = item.with(|item| item.icon(pixels));
source.unwrap_or_else(|| IconSource::Name(NO_ICON.into()))
})
.size(move || icon_size());
let click = {
let (host, id, style) = (host.clone(), Rc::clone(&id), Rc::clone(style));
move |cx: &Cx| {
if item.with_untracked(|item| item.item_is_menu) {
open_menu(cx, &host, &id, &style);
} else {
host.activate(&id);
}
}
};
let right_click = {
let (host, id, style) = (host.clone(), Rc::clone(&id), Rc::clone(style));
move |cx: &Cx| open_menu(cx, &host, &id, &style)
};
let (middle, scroll) = (host.clone(), host.clone());
let middle_id = Rc::clone(&id);
Button::new(icon)
.style(BUTTON)
.on_click(click)
.on_right_click(right_click)
.on_middle_click(move |_| middle.secondary_activate(&middle_id))
.on_scroll(move |_, steps| scroll.scroll(&id, steps))
}
fn open_menu(cx: &Cx, host: &TrayHost, id: &Rc<str>, style: &Shared<Style>) {
let Some(service) = host.service.clone() else {
return;
};
let Some(opened) = service.open_menu(id) else {
return;
};
let (host, id, style) = (host.clone(), Rc::clone(id), Rc::clone(style));
cx.open_popup(move |cx| {
let _following = cx.signal(Following {
service: Rc::clone(&service),
opened,
});
let menu = host.menu();
let closer = cx.clone();
cx.effect(move || {
menu.with(|_| ());
if !service.follows(opened) {
closer.close_popup();
}
});
let shown = {
let id = Rc::clone(&id);
move || menu.with(|menu| menu.as_ref().is_some_and(|menu| *menu.item == *id))
};
let entries = {
let id = Rc::clone(&id);
move || {
menu.with(|menu| {
let ours = menu.as_ref().filter(|menu| *menu.item == *id);
ours.map(|menu| menu.entries.clone()).unwrap_or_default()
})
}
};
let (activate, submenu) = (host.clone(), host);
let menu = Menu::new(entries)
.style(move || if shown() { style() } else { Style::new() })
.on_activate(move |_, entry| activate.menu_event(&id, entry))
.on_open_submenu(move |_, entry| submenu.open_submenu(entry));
PopupWindow::new().child(menu)
});
}
struct Following {
service: Rc<super::Service>,
opened: u64,
}
impl Drop for Following {
fn drop(&mut self) {
self.service.close_menu_opened(self.opened);
}
}