Skip to main content

ui/
tray.rs

1// tray -- the app's symbol in the notification area and system notifications (plat::tray)
2//
3//   tray_set(icon, tooltip, menu) -> bool   show or update the symbol: icon None = the
4//                                    program's own; menu = Mi rows like a window menu (labels
5//                                    from the command table, CF_DISABLED / CF_CHECKED /
6//                                    CF_HIDDEN of cmd_state at this moment: call it again after
7//                                    a change); false: no notification area here
8//   tray_remove()
9//   notify(title, body, icon) -> id  a system notification (0: not shown)
10//   notify_close(id)                 tray_supported() -> bool
11//   bring_to_front()                 restore and activate the main window (e.g. on UIC_TRAY)
12// Headless runs (scripts, --shot) never show anything: plat::tray's dry run prints it.
13// Events (E_WAKE): a chosen menu row runs App::command(id, 0) like a row of the window's menu;
14//   a click on the symbol App::command(UIC_TRAY, TRAY_ACTIVATE), a double / middle click
15//   UIC_TRAY TRAY_SECONDARY; a clicked notification UIC_NOTE (arg = its id), one that went
16//   away unclicked UIC_NOTE_GONE (arg = its id).
17
18use alloc::string::String;
19use alloc::vec::Vec;
20
21use gfx::{Surface, FMT_PARGB, FMT_XRGB};
22use plat::tray::{self, Note, Tray, TrayEvent, TrayIcon, TrayItem, TI_CHECKED, TI_DISABLED, TI_SEPARATOR};
23
24use super::consts::*;
25use super::popup::Mi;
26use super::Ui;
27
28// straight ARGB pixels of a surface (at most 256 x 256; also the taskbar's badge, taskbar.rs)
29pub(super) fn icon_of(sf: &Surface) -> Option<TrayIcon> {
30    if sf.w <= 0 || sf.h <= 0 || sf.w > 256 || sf.h > 256 {
31        return None;
32    }
33    let mut px = Vec::with_capacity((sf.w * sf.h) as usize);
34    for y in 0..sf.h {
35        for x in 0..sf.w {
36            let o = (y * sf.stride + x * 4) as usize;
37            let v = u32::from_le_bytes(sf.px.get(o..o + 4)?.try_into().ok()?);
38            px.push(match sf.fmt {
39                FMT_XRGB => v | 0xFF00_0000,
40                FMT_PARGB => {
41                    let a = v >> 24;
42                    if a == 0 {
43                        0
44                    } else {
45                        let un = |c: u32| ((c * 255 + a / 2) / a).min(255);
46                        (a << 24) | (un((v >> 16) & 255) << 16) | (un((v >> 8) & 255) << 8) | un(v & 255)
47                    }
48                }
49                _ => v,
50            });
51        }
52    }
53    Some(TrayIcon { w: sf.w as u32, h: sf.h as u32, px })
54}
55
56impl Ui {
57    // the menu rows of Mi records (the commands' state now)
58    fn tray_items(&self, menu: &[Mi]) -> Vec<TrayItem> {
59        let mut out = Vec::new();
60        for m in menu {
61            match *m {
62                Mi::Cmd(c) => {
63                    let f = self.cmd_flags(c);
64                    if f & CF_HIDDEN != 0 {
65                        continue;
66                    }
67                    let label = self.cmd_find(c).map_or(String::new(), |r| String::from(r.label));
68                    let flags = if f & CF_DISABLED != 0 { TI_DISABLED } else { 0 } | if f & CF_CHECKED != 0 { TI_CHECKED } else { 0 };
69                    out.push(TrayItem { id: c, label, flags, sub: Vec::new() });
70                }
71                Mi::Sep => out.push(TrayItem { flags: TI_SEPARATOR, ..TrayItem::default() }),
72                Mi::Sub(label, items) | Mi::Menu(label, items) => {
73                    out.push(TrayItem { id: 0, label: String::from(label), flags: 0, sub: self.tray_items(items) })
74                }
75                Mi::Item(id, label, _) => out.push(TrayItem { id, label: String::from(label), flags: 0, sub: Vec::new() }),
76                Mi::List(base, max, placeholder) => {
77                    let rows: Vec<String> = self.m.popup.lists.iter().find(|e| e.0 == base).map(|e| e.1.clone()).unwrap_or_default();
78                    if rows.is_empty() && !placeholder.is_empty() {
79                        out.push(TrayItem { id: 0, label: String::from(placeholder), flags: TI_DISABLED, sub: Vec::new() });
80                    }
81                    for (k, r) in rows.iter().take(max as usize).enumerate() {
82                        out.push(TrayItem { id: base + k as u32, label: r.replace('&', "&&"), flags: 0, sub: Vec::new() });
83                    }
84                }
85            }
86        }
87        out
88    }
89
90    /// show or update the symbol in the notification area -> false: there is none here
91    pub fn tray_set(&mut self, icon: Option<&Surface>, tooltip: &str, menu: &[Mi]) -> bool {
92        tray::set_dry_run(self.headless);
93        let t = Tray { icon: icon.and_then(icon_of), tooltip: String::from(tooltip), menu: self.tray_items(menu) };
94        tray::tray_set(&t)
95    }
96
97    /// take the symbol away
98    pub fn tray_remove(&mut self) {
99        tray::tray_remove();
100    }
101
102    /// a system notification -> its id (0: not shown); UIC_NOTE / UIC_NOTE_GONE report it
103    pub fn notify(&mut self, title: &str, body: &str, icon: Option<&Surface>) -> u32 {
104        tray::set_dry_run(self.headless);
105        tray::notify(&Note { title: String::from(title), body: String::from(body), icon: icon.and_then(icon_of) })
106    }
107
108    /// take a notification away
109    pub fn notify_close(&mut self, id: u32) {
110        tray::notify_close(id);
111    }
112
113    /// whether this system shows the symbol (a notification area; GNOME: only with the
114    /// AppIndicator extension); notifications work without one
115    pub fn tray_supported(&self) -> bool {
116        tray::tray_supported()
117    }
118
119    /// restore the main window when it is minimised and activate it
120    pub fn bring_to_front(&mut self) {
121        if self.headless {
122            return;
123        }
124        if plat::win_state(0) & plat::WST_MINIMIZED != 0 {
125            plat::win_cmd(0, plat::WCMD_RESTORE);
126        }
127        plat::win_activate(0);
128    }
129
130    /// E_WAKE: the symbol's and the notifications' events -> App::command
131    pub(crate) fn tray_poll(&mut self) {
132        for e in tray::tray_take() {
133            match e {
134                TrayEvent::Activate => self.cmd_run(UIC_TRAY, TRAY_ACTIVATE),
135                TrayEvent::SecondaryActivate => self.cmd_run(UIC_TRAY, TRAY_SECONDARY),
136                TrayEvent::Menu(id) => self.cmd_run(id, 0),
137                TrayEvent::NoteClicked(id) => self.cmd_run(UIC_NOTE, id),
138                TrayEvent::NoteClosed(id) => self.cmd_run(UIC_NOTE_GONE, id),
139            }
140        }
141    }
142}