Skip to main content

ferroui_controls/
tray_icon.rs

1use 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
23/// The collection of the tray icons of an application: a list of
24/// [`TrayIcon`] whose clear is notified as a removal.
25pub struct TrayIcons {
26    items: FerroList<Ref<TrayIcon>>,
27    /// The subscription of [`TrayIcon`] to the changes of the collection
28    /// while it is the collection of the application.
29    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
48/// Collections compare by reference.
49impl PartialEq for TrayIcons {
50    fn eq(&self, other: &Self) -> bool {
51        std::ptr::eq(self, other)
52    }
53}
54
55/// An icon in the notification area of the platform.
56#[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
89/// The tray icon viewed through the interfaces it implements.
90struct 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        /// Defines the `Command` property.
107        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        /// Defines the `CommandParameter` property.
113        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
114            Button::command_parameter_property().add_owner::<TrayIcon>()
115        }
116
117        /// Defines the `Icons` attached property.
118        pub fn icons_property() -> AttachedProperty<Option<Rc<TrayIcons>>> {
119            FerroProperty::register_attached::<TrayIcon, Application, _>("Icons", None)
120        }
121
122        /// Defines the `Menu` property.
123        pub fn menu_property() -> StyledProperty<Option<Ref<NativeMenu>>> {
124            FerroProperty::register::<TrayIcon, _>("Menu", None)
125        }
126
127        /// Defines the `Icon` property.
128        pub fn icon_property() -> StyledProperty<Option<Rc<WindowIcon>>> {
129            Window::icon_property().add_owner::<TrayIcon>()
130        }
131
132        /// Defines the `ToolTipText` property.
133        pub fn tool_tip_text_property() -> StyledProperty<Option<String>> {
134            FerroProperty::register::<TrayIcon, _>("ToolTipText", None)
135        }
136
137        /// Defines the `IsVisible` property.
138        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    /// Creates the class data; see [`FerroObject::construct`].
175    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    /// Raised when the TrayIcon is clicked.
190    /// Note, this is only supported on Win32 and some Linux DEs,
191    /// on OSX this event is not raised.
192    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    /// Sets the tray icons of the application.
203    pub fn set_icons(o: &Application, tray_icons: Option<Rc<TrayIcons>>) {
204        o.set_value(Self::icons_property(), tray_icons)
205    }
206
207    /// Gets the tray icons of the application.
208    pub fn get_icons(o: &Application) -> Option<Rc<TrayIcons>> {
209        o.get_value(Self::icons_property())
210    }
211
212    /// Gets the `Command` property of a TrayIcon.
213    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    /// Gets the parameter to pass to the `Command` property of a
222    /// [`TrayIcon`].
223    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    /// Gets the Menu of the TrayIcon.
232    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    /// Gets the icon of the TrayIcon.
241    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    /// Gets the tooltip text of the TrayIcon.
250    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    /// Gets the visibility of the TrayIcon.
259    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    /// The native menu exporter of the platform implementation.
268    pub fn native_menu_exporter(&self) -> Option<Rc<dyn INativeMenuExporter>> {
269        self.platform_impl().and_then(|impl_| impl_.menu_exporter())
270    }
271
272    /// The platform implementation; upstream `Impl`.
273    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        // Keep the native icon hidden until all properties have been initialized.
324        impl_.set_is_visible(false);
325        // The implementation is owned by the icon, so its callback holds
326        // the icon weakly.
327        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            // The command and its parameter are read again for the
336            // execution, as in the reference.
337            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    /// Disposes the tray icon (removing it from the tray area).
370    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    /// The tray icon viewed as a disposable.
380    pub fn to_disposable(&self) -> Rc<dyn IDisposable> {
381        Rc::new(TrayIconHandle(self.to_ref()))
382    }
383}