crabshell_services/tray/
widget.rs1use std::rc::Rc;
6
7use crabshell_reactive::Memo;
8use crabshell_ui::{
9 Align, Button, Color, Cx, Element, Icon, IconSource, IntoProp, Menu, PopupWindow, Prop, Row,
10 Style, Widget,
11};
12
13use super::{TrayHost, TrayItem};
14use crate::ServicesExt;
15
16const MENU: Style = Style::new()
19 .background(Color::rgb(0x2b, 0x2b, 0x2b))
20 .color(Color::rgb(0xe6, 0xe6, 0xe6))
21 .radius(6.0)
22 .padding(4, 0);
23
24const BUTTON: Style = Style::new().padding(2, 4).radius(4.0);
26
27const ICON_SIZE: u32 = 16;
29
30const NO_ICON: &str = "image-missing";
32
33type Shared<T> = Rc<dyn Fn() -> T>;
34
35#[derive(Default)]
49pub struct Tray {
50 icon_size: Option<Prop<u32>>,
51 menu_style: Option<Prop<Style>>,
52}
53
54impl Tray {
55 pub fn new() -> Tray {
57 Tray::default()
58 }
59
60 pub fn icon_size(mut self, size: impl IntoProp<u32>) -> Tray {
62 self.icon_size = Some(size.into_prop());
63 self
64 }
65
66 pub fn menu_style(mut self, style: impl IntoProp<Style>) -> Tray {
70 self.menu_style = Some(style.into_prop());
71 self
72 }
73}
74
75impl Widget for Tray {
76 fn into_element(self: Box<Self>) -> Element {
77 let size: Shared<u32> = self
78 .icon_size
79 .map_or_else(|| Rc::new(|| ICON_SIZE), Prop::shared);
80 let style: Shared<Style> = self
81 .menu_style
82 .map_or_else(|| Rc::new(|| MENU), Prop::shared);
83 let tray = Row::new().children_keyed(
86 || vec![()],
87 |_| (),
88 move |cx, _| {
89 let host = cx.tray();
90 let items = host.items();
91 let (size, style) = (Rc::clone(&size), Rc::clone(&style));
92 Row::new().align(Align::Center).children_keyed(
93 move || items.get(),
94 |item| item.id.clone(),
95 move |_, item| item_button(&host, item, &size, &style),
96 )
97 },
98 );
99 Box::new(tray).into_element()
100 }
101}
102
103fn item_button(
105 host: &TrayHost,
106 item: Memo<TrayItem>,
107 size: &Shared<u32>,
108 style: &Shared<Style>,
109) -> Button {
110 let id: Rc<str> = item.with_untracked(|item| item.id.as_str().into());
112 let (icon_size, pick) = (Rc::clone(size), Rc::clone(size));
113 let icon = Icon::new(move || {
114 let pixels = pick().saturating_mul(2);
118 let source = item.with(|item| item.icon(pixels));
119 source.unwrap_or_else(|| IconSource::Name(NO_ICON.into()))
120 })
121 .size(move || icon_size());
122 let click = {
123 let (host, id, style) = (host.clone(), Rc::clone(&id), Rc::clone(style));
124 move |cx: &Cx| {
125 if item.with_untracked(|item| item.item_is_menu) {
126 open_menu(cx, &host, &id, &style);
127 } else {
128 host.activate(&id);
129 }
130 }
131 };
132 let right_click = {
133 let (host, id, style) = (host.clone(), Rc::clone(&id), Rc::clone(style));
134 move |cx: &Cx| open_menu(cx, &host, &id, &style)
135 };
136 let (middle, scroll) = (host.clone(), host.clone());
137 let middle_id = Rc::clone(&id);
138 Button::new(icon)
139 .style(BUTTON)
140 .on_click(click)
141 .on_right_click(right_click)
142 .on_middle_click(move |_| middle.secondary_activate(&middle_id))
143 .on_scroll(move |_, steps| scroll.scroll(&id, steps))
144}
145
146fn open_menu(cx: &Cx, host: &TrayHost, id: &Rc<str>, style: &Shared<Style>) {
150 let Some(service) = host.service.clone() else {
151 return;
152 };
153 let Some(opened) = service.open_menu(id) else {
154 return;
155 };
156 let (host, id, style) = (host.clone(), Rc::clone(id), Rc::clone(style));
157 cx.open_popup(move |cx| {
158 let _following = cx.signal(Following {
159 service: Rc::clone(&service),
160 opened,
161 });
162 let menu = host.menu();
163 let closer = cx.clone();
166 cx.effect(move || {
167 menu.with(|_| ());
168 if !service.follows(opened) {
169 closer.close_popup();
170 }
171 });
172 let shown = {
173 let id = Rc::clone(&id);
174 move || menu.with(|menu| menu.as_ref().is_some_and(|menu| *menu.item == *id))
175 };
176 let entries = {
177 let id = Rc::clone(&id);
178 move || {
179 menu.with(|menu| {
180 let ours = menu.as_ref().filter(|menu| *menu.item == *id);
181 ours.map(|menu| menu.entries.clone()).unwrap_or_default()
182 })
183 }
184 };
185 let (activate, submenu) = (host.clone(), host);
186 let menu = Menu::new(entries)
188 .style(move || if shown() { style() } else { Style::new() })
189 .on_activate(move |_, entry| activate.menu_event(&id, entry))
190 .on_open_submenu(move |_, entry| submenu.open_submenu(entry));
191 PopupWindow::new().child(menu)
192 });
193}
194
195struct Following {
198 service: Rc<super::Service>,
199 opened: u64,
200}
201
202impl Drop for Following {
203 fn drop(&mut self) {
204 self.service.close_menu_opened(self.opened);
205 }
206}