ferroui_controls/
tray_icon.rs1use crate::application::Application;
2use crate::platform::{
3 register_native_menu_exporter_provider, INativeMenuExporter, INativeMenuExporterProvider, ITrayIconImpl,
4 MacOSProperties, PlatformManager,
5};
6use crate::{Button, NativeMenu, Window, WindowIcon};
7use ferroui_base::collections::{
8 FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs, ResetBehavior,
9};
10use ferroui_base::input::ICommand;
11use ferroui_base::reactive::{Disposable, IDisposable};
12use ferroui_base::threading::Dispatcher;
13use ferroui_base::utilities::HandlerList;
14use ferroui_base::{
15 ferro_class, ferro_class_info, ferro_properties, instantiate, AttachedProperty, BoxedValue, FerroObject,
16 FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledProperty,
17 StyledPropertyMetadata, Visual,
18};
19use std::cell::{Cell, RefCell};
20use std::ops::Deref;
21use std::rc::Rc;
22
23pub struct TrayIcons {
26 items: FerroList<Ref<TrayIcon>>,
27 subscription: Cell<Option<u64>>,
30}
31
32impl TrayIcons {
33 pub fn new() -> Rc<Self> {
34 let items = FerroList::new();
35 items.set_reset_behavior(ResetBehavior::Remove);
36 Rc::new(Self { items, subscription: Cell::new(None) })
37 }
38}
39
40impl Deref for TrayIcons {
41 type Target = FerroList<Ref<TrayIcon>>;
42
43 fn deref(&self) -> &Self::Target {
44 &self.items
45 }
46}
47
48impl PartialEq for TrayIcons {
50 fn eq(&self, other: &Self) -> bool {
51 std::ptr::eq(self, other)
52 }
53}
54
55#[repr(C)]
57pub struct TrayIcon {
58 base: FerroObject,
59 impl_: RefCell<Option<Rc<dyn ITrayIconImpl>>>,
60 is_attached: Cell<bool>,
61 is_disposed: Cell<bool>,
62 clicked: HandlerList<dyn Fn(&TrayIcon)>,
63}
64
65ferro_class!(TrayIcon: FerroObject);
66ferro_class_info!(TrayIcon { new: TrayIcon::new });
67
68impl FerroObjectImpl for TrayIcon {
69 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
70 Self::parent_on_property_changed(this, change);
71
72 let property = change.property();
73 let Some(impl_) = this.platform_impl() else { return };
74
75 if property == Self::icon_property().as_property() {
76 impl_.set_icon(this.icon().map(|icon| icon.platform_impl()));
77 } else if property == Self::is_visible_property().as_property() {
78 impl_.set_is_visible(change.get_new_value::<bool>());
79 } else if property == Self::tool_tip_text_property().as_property() {
80 impl_.set_tool_tip_text(change.get_new_value::<Option<String>>().as_deref());
81 } else if property == Self::menu_property().as_property() {
82 if let Some(exporter) = impl_.menu_exporter() {
83 exporter.set_native_menu(change.get_new_value::<Option<Ref<NativeMenu>>>());
84 }
85 }
86 }
87}
88
89struct TrayIconHandle(Ref<TrayIcon>);
91
92impl INativeMenuExporterProvider for TrayIconHandle {
93 fn native_menu_exporter(&self) -> Option<Rc<dyn INativeMenuExporter>> {
94 self.0.native_menu_exporter()
95 }
96}
97
98impl IDisposable for TrayIconHandle {
99 fn dispose(&self) {
100 self.0.dispose();
101 }
102}
103
104ferro_properties! {
105 impl TrayIcon {
106 pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
108 Button::command_property()
109 .add_owner_with::<TrayIcon>(StyledPropertyMetadata::new(None).with_enable_data_validation(true))
110 }
111
112 pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
114 Button::command_parameter_property().add_owner::<TrayIcon>()
115 }
116
117 pub fn icons_property() -> AttachedProperty<Option<Rc<TrayIcons>>> {
119 FerroProperty::register_attached::<TrayIcon, Application, _>("Icons", None)
120 }
121
122 pub fn menu_property() -> StyledProperty<Option<Ref<NativeMenu>>> {
124 FerroProperty::register::<TrayIcon, _>("Menu", None)
125 }
126
127 pub fn icon_property() -> StyledProperty<Option<Rc<WindowIcon>>> {
129 Window::icon_property().add_owner::<TrayIcon>()
130 }
131
132 pub fn tool_tip_text_property() -> StyledProperty<Option<String>> {
134 FerroProperty::register::<TrayIcon, _>("ToolTipText", None)
135 }
136
137 pub fn is_visible_property() -> StyledProperty<bool> {
139 Visual::is_visible_property().add_owner::<TrayIcon>()
140 }
141 }
142}
143
144impl TrayIcon {
145 fn static_constructor() {
146 register_native_menu_exporter_provider::<TrayIcon>(|tray_icon| Rc::new(TrayIconHandle(tray_icon)));
147
148 Self::icons_property().changed().subscribe(|args| {
149 if args.sender().is::<Application>() {
150 let (old_value, new_value) = args.get_old_and_new_value::<Option<Rc<TrayIcons>>>();
151
152 if let Some(old_value) = old_value {
153 if let Some(token) = old_value.subscription.take() {
154 old_value.remove_collection_changed(token);
155 }
156 Self::detach_icons(&old_value.to_vec());
157 }
158
159 if let Some(new_value) = new_value {
160 let token = new_value.add_collection_changed(Rc::new(Self::icons_collection_changed));
161 if let Some(old_token) = new_value.subscription.replace(Some(token)) {
162 new_value.remove_collection_changed(old_token);
163 }
164 Self::attach_icons(&new_value.to_vec());
165 }
166 } else {
167 panic!("TrayIcon.Icons must be set on the Application.");
168 }
169 });
170
171 let _ = Dispatcher::ui_thread().shutdown_started(|_| Self::shutdown_started());
172 }
173
174 pub fn construct() -> Self {
176 Self {
177 base: FerroObject::construct(),
178 impl_: RefCell::new(None),
179 is_attached: Cell::new(false),
180 is_disposed: Cell::new(false),
181 clicked: HandlerList::new(),
182 }
183 }
184
185 pub fn new() -> Ref<Self> {
186 instantiate(Self::construct())
187 }
188
189 pub fn clicked(&self, handler: impl Fn(&TrayIcon) + 'static) -> Rc<dyn IDisposable> {
193 let token = self.clicked.add(Rc::new(handler));
194 let weak = self.to_ref().downgrade();
195 Disposable::create(move || {
196 if let Some(this) = weak.upgrade() {
197 this.clicked.remove(token);
198 }
199 })
200 }
201
202 pub fn set_icons(o: &Application, tray_icons: Option<Rc<TrayIcons>>) {
204 o.set_value(Self::icons_property(), tray_icons)
205 }
206
207 pub fn get_icons(o: &Application) -> Option<Rc<TrayIcons>> {
209 o.get_value(Self::icons_property())
210 }
211
212 pub fn command(&self) -> Option<Rc<dyn ICommand>> {
214 self.get_value(Self::command_property())
215 }
216
217 pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
218 self.set_value(Self::command_property(), value)
219 }
220
221 pub fn command_parameter(&self) -> Option<BoxedValue> {
224 self.get_value(Self::command_parameter_property())
225 }
226
227 pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
228 self.set_value(Self::command_parameter_property(), value)
229 }
230
231 pub fn menu(&self) -> Option<Ref<NativeMenu>> {
233 self.get_value(Self::menu_property())
234 }
235
236 pub fn set_menu(&self, value: Option<Ref<NativeMenu>>) {
237 self.set_value(Self::menu_property(), value)
238 }
239
240 pub fn icon(&self) -> Option<Rc<WindowIcon>> {
242 self.get_value(Self::icon_property())
243 }
244
245 pub fn set_icon(&self, value: Option<Rc<WindowIcon>>) {
246 self.set_value(Self::icon_property(), value)
247 }
248
249 pub fn tool_tip_text(&self) -> Option<String> {
251 self.get_value(Self::tool_tip_text_property())
252 }
253
254 pub fn set_tool_tip_text(&self, value: Option<String>) {
255 self.set_value(Self::tool_tip_text_property(), value)
256 }
257
258 pub fn is_visible(&self) -> bool {
260 self.get_value(Self::is_visible_property())
261 }
262
263 pub fn set_is_visible(&self, value: bool) {
264 self.set_value(Self::is_visible_property(), value)
265 }
266
267 pub fn native_menu_exporter(&self) -> Option<Rc<dyn INativeMenuExporter>> {
269 self.platform_impl().and_then(|impl_| impl_.menu_exporter())
270 }
271
272 pub(crate) fn platform_impl(&self) -> Option<Rc<dyn ITrayIconImpl>> {
274 self.impl_.borrow().clone()
275 }
276
277 fn shutdown_started() {
278 let app = Application::current().unwrap_or_else(|| panic!("Application not yet initialized."));
279 let tray_icons = Self::get_icons(&app);
280
281 if let Some(tray_icons) = tray_icons {
282 Self::dispose_icons(&tray_icons.to_vec());
283 }
284 }
285
286 fn icons_collection_changed(e: &NotifyCollectionChangedEventArgs<'_, Ref<TrayIcon>>) {
287 if e.action == NotifyCollectionChangedAction::Move {
288 return;
289 }
290
291 Self::detach_icons(e.old_items);
292 Self::attach_icons(e.new_items);
293 }
294
295 fn attach_icons(icons: &[Ref<TrayIcon>]) {
296 for icon in icons {
297 icon.attach();
298 }
299 }
300
301 fn detach_icons(icons: &[Ref<TrayIcon>]) {
302 for icon in icons {
303 icon.detach();
304 }
305 }
306
307 fn dispose_icons(icons: &[Ref<TrayIcon>]) {
308 for icon in icons {
309 icon.dispose();
310 }
311 }
312
313 fn attach(&self) {
314 if self.is_attached.get() || self.is_disposed.get() {
315 return;
316 }
317 self.is_attached.set(true);
318
319 let impl_ = PlatformManager::create_tray_icon();
320 *self.impl_.borrow_mut() = impl_.clone();
321 let Some(impl_) = impl_ else { return };
322
323 impl_.set_is_visible(false);
325 let weak = self.to_ref().downgrade();
328 impl_.set_on_clicked(Some(Rc::new(move || {
329 let Some(this) = weak.upgrade() else { return };
330
331 for (_, handler) in this.clicked.snapshot().iter() {
332 handler(&this);
333 }
334
335 if this.command().is_some_and(|command| command.can_execute(this.command_parameter().as_ref())) {
338 if let Some(command) = this.command() {
339 command.execute(this.command_parameter().as_ref());
340 }
341 }
342 })));
343
344 impl_.set_icon(self.icon().map(|icon| icon.platform_impl()));
345 impl_.set_tool_tip_text(self.tool_tip_text().as_deref());
346 if let Some(exporter) = impl_.menu_exporter() {
347 exporter.set_native_menu(self.menu());
348 }
349
350 if let Some(template_impl) = impl_.as_tray_icon_with_is_template_impl() {
351 template_impl.set_is_template_icon(MacOSProperties::get_is_template_icon(self));
352 }
353
354 impl_.set_is_visible(self.is_visible());
355 }
356
357 fn detach(&self) {
358 if !self.is_attached.get() {
359 return;
360 }
361
362 self.is_attached.set(false);
363 let impl_ = self.impl_.borrow_mut().take();
364 if let Some(impl_) = impl_ {
365 impl_.dispose();
366 }
367 }
368
369 pub fn dispose(&self) {
371 if self.is_disposed.get() {
372 return;
373 }
374
375 self.is_disposed.set(true);
376 self.detach();
377 }
378
379 pub fn to_disposable(&self) -> Rc<dyn IDisposable> {
381 Rc::new(TrayIconHandle(self.to_ref()))
382 }
383}