Skip to main content

plat/
taskbar.rs

1// taskbar -- the app's button in the taskbar or dock: progress, a badge, the jump list's tasks
2//
3//   taskbar_progress(TaskbarProgress) -> bool   None, Normal(f), Paused(f), Error(f) or
4//                                    Indeterminate (f: 0.0 ..= 1.0)
5//   taskbar_badge(count, icon) -> bool   count 0 = no badge; icon: the overlay Windows shows on
6//                                    the button (straight ARGB, 16 x 16 at 100 %; ui draws one
7//                                    with the number), Linux docks show the count themselves
8//   taskbar_tasks(&[JumpTask]) -> bool   the "Tasks" of the button's jump list (Windows): each
9//                                    starts this program with its arguments -- with
10//                                    AF_SINGLE_INSTANCE they reach the running app as
11//                                    UIC_OPEN; empty = the jump list removed.  Linux: false
12//   taskbar_state() -> (TaskbarProgress, u32)   what was set last (progress, count)
13//   taskbar_badge_shown(count) -> bool   that badge (with its icon) is up already (ui skips
14//                                    drawing it again)
15//   taskbar_supported() -> bool      whether this system shows any of it
16//   app_user_model_id() -> Option<id>   (Windows) the explicit AppUserModelID the process runs
17//                                    under: the taskbar button and its pins, the jump list, the
18//                                    recent files and the notifications all go by it.  ui::main!
19//                                    sets PACKAGE's app id once the setup installed the program
20//                                    or --register registered it (assoc::app_id_boot); None:
21//                                    Windows derives an id from the program's path
22//   set_app_user_model_id(id) -> bool   (Windows) run under id from now on: before the first
23//                                    window.  false elsewhere, in an MSIX package (the
24//                                    package's id counts) and for an id Windows refuses (more
25//                                    than 128 characters, blanks)
26//
27// The calls keep the state and return whether the system took it (Windows: also before the
28// window has its button -- it is applied when the button appears).  Setting what is already
29// shown does nothing (progress compared in per mille), so calling them every frame is cheap.
30// The tray's dry run
31// (tray::set_dry_run, jfgui_TRAY_DRYRUN=1; the UI's headless runs) prints "taskbar: ..."
32// instead.
33//
34// Windows (win_taskbar.rs): ITaskbarList3 on the main window (SetProgressState,
35// SetProgressValue, SetOverlayIcon with the count as its text), applied again on the
36// registered message "TaskbarButtonCreated" (the first show, Explorer restarted);
37// ICustomDestinationList with IShellLinkW tasks (title through IPropertyStore PKEY_Title).
38// COM on the UI thread: call these from the UI thread.  Linux (linux_taskbar.rs): the signal
39// com.canonical.Unity.LauncherEntry.Update(application://<desktop id>.desktop, {progress,
40// progress-visible, count, count-visible}) on a session-bus connection kept open (docks drop
41// the state when it closes; Query is answered), understood by KDE Plasma, Ubuntu's dock /
42// Dash to Dock and Plank.  Linux knows no paused / error / indeterminate look (paused and
43// error show the bar, indeterminate none) and no jump list at run time (that is the .desktop
44// file's Actions).  macOS (apple_mac_services.rs): the Dock tile -- badgeLabel, the progress drawn
45// into the tile (blue, yellow paused, red error, grey busy), the tasks as the Dock menu (a chosen
46// one reaches the app as UIC_OPEN, without a second start).  Web and Android: nothing (false).
47
48use alloc::string::String;
49use alloc::vec::Vec;
50use core::sync::atomic::{AtomicU8, Ordering};
51
52use rt::sync::SpinLock;
53
54use crate::tray::TrayIcon;
55
56/// what the taskbar button's progress bar shows
57#[derive(Clone, Copy, Debug, Default, PartialEq)]
58pub enum TaskbarProgress {
59    /// no bar
60    #[default]
61    None,
62    /// done so far (0.0 ..= 1.0)
63    Normal(f32),
64    /// paused at (Windows: yellow)
65    Paused(f32),
66    /// failed at (Windows: red)
67    Error(f32),
68    /// busy without a known end (Windows: a moving bar)
69    Indeterminate,
70}
71
72impl TaskbarProgress {
73    /// the fraction done, clamped to 0.0 ..= 1.0 (None / Indeterminate: 0.0)
74    pub fn value(&self) -> f32 {
75        match *self {
76            TaskbarProgress::Normal(v) | TaskbarProgress::Paused(v) | TaskbarProgress::Error(v) if v.is_finite() => v.clamp(0.0, 1.0),
77            _ => 0.0,
78        }
79    }
80
81    fn text(&self) -> String {
82        let pct = (self.value() * 100.0 + 0.5) as u32;
83        match self {
84            TaskbarProgress::None => String::from("none"),
85            TaskbarProgress::Normal(_) => alloc::format!("normal {pct}%"),
86            TaskbarProgress::Paused(_) => alloc::format!("paused {pct}%"),
87            TaskbarProgress::Error(_) => alloc::format!("error {pct}%"),
88            TaskbarProgress::Indeterminate => String::from("indeterminate"),
89        }
90    }
91}
92
93/// one task of the jump list: starts this program with args
94#[derive(Clone, Debug, Default, PartialEq, Eq)]
95pub struct JumpTask {
96    /// the row's text
97    pub title: String,
98    /// the arguments after the program
99    pub args: Vec<String>,
100    /// its tooltip ("" = none)
101    pub desc: String,
102}
103
104/// the state the platform code applies (again)
105#[derive(Clone, Default)]
106pub(crate) struct State {
107    pub(crate) progress: TaskbarProgress,
108    pub(crate) count: u32,
109    pub(crate) icon: Option<TrayIcon>,
110}
111
112static STATE: SpinLock<Option<State>> = SpinLock::new(None);
113
114/// (platform) a copy of the state
115#[allow(dead_code)]
116pub(crate) fn state() -> State {
117    STATE.lock().clone().unwrap_or_default()
118}
119
120// -> (the state before, after)
121fn update(f: impl FnOnce(&mut State)) -> (State, State) {
122    let mut g = STATE.lock();
123    let s = g.get_or_insert_with(State::default);
124    let old = s.clone();
125    f(s);
126    (old, s.clone())
127}
128
129// the answer of the last progress / badge call: 0 none yet, 1 false, 2 shown, 3 printed (dry run)
130static SHOWN: [AtomicU8; 2] = [AtomicU8::new(0), AtomicU8::new(0)];
131const K_PROGRESS: usize = 0;
132const K_BADGE: usize = 1;
133
134// whether the call can be skipped: nothing changed and the last answer stands
135fn unchanged(k: usize, same: bool, dry: bool) -> bool {
136    same && SHOWN[k].load(Ordering::Relaxed) == if dry { 3 } else { 2 }
137}
138
139fn answered(k: usize, ok: bool, dry: bool) -> bool {
140    SHOWN[k].store(if !ok { 1 } else if dry { 3 } else { 2 }, Ordering::Relaxed);
141    ok
142}
143
144// what the progress looks like: its kind and per mille
145fn look(p: TaskbarProgress) -> (u8, u32) {
146    let kind = match p {
147        TaskbarProgress::None => 0,
148        TaskbarProgress::Normal(_) => 1,
149        TaskbarProgress::Paused(_) => 2,
150        TaskbarProgress::Error(_) => 3,
151        TaskbarProgress::Indeterminate => 4,
152    };
153    (kind, (p.value() * 1000.0 + 0.5) as u32)
154}
155
156fn dry(line: &str) {
157    crate::write_stdout(alloc::format!("taskbar: {line}\n").as_bytes());
158}
159
160/// show progress on the app's taskbar button -> false: not shown here
161pub fn taskbar_progress(p: TaskbarProgress) -> bool {
162    let p = match p {
163        TaskbarProgress::Normal(_) => TaskbarProgress::Normal(p.value()),
164        TaskbarProgress::Paused(_) => TaskbarProgress::Paused(p.value()),
165        TaskbarProgress::Error(_) => TaskbarProgress::Error(p.value()),
166        other => other,
167    };
168    let (old, s) = update(|s| s.progress = p);
169    let is_dry = crate::tray::dry_run();
170    if unchanged(K_PROGRESS, look(old.progress) == look(p), is_dry) {
171        return true;
172    }
173    if is_dry {
174        dry(&alloc::format!("progress {}", p.text()));
175        return answered(K_PROGRESS, true, true);
176    }
177    answered(K_PROGRESS, native::progress(&s), false)
178}
179
180/// a badge on the button: count (0 = none), icon = Windows' overlay (None: no overlay there)
181pub fn taskbar_badge(count: u32, icon: Option<&TrayIcon>) -> bool {
182    let (old, s) = update(|s| {
183        s.count = count;
184        s.icon = if count == 0 { None } else { icon.cloned() };
185    });
186    let is_dry = crate::tray::dry_run();
187    if unchanged(K_BADGE, old.count == s.count && old.icon == s.icon, is_dry) {
188        return true;
189    }
190    if is_dry {
191        match &s.icon {
192            _ if count == 0 => dry("badge none"),
193            Some(i) => dry(&alloc::format!("badge {count} icon {}x{}", i.w, i.h)),
194            None => dry(&alloc::format!("badge {count}")),
195        }
196        return answered(K_BADGE, true, true);
197    }
198    answered(K_BADGE, native::badge(&s), false)
199}
200
201/// the tasks of the button's jump list (empty: none) -> false: no jump list here
202pub fn taskbar_tasks(tasks: &[JumpTask]) -> bool {
203    if crate::tray::dry_run() {
204        let rows: Vec<String> = tasks.iter().map(|t| alloc::format!("{}=[{}]", t.title, t.args.join(" "))).collect();
205        dry(&alloc::format!("tasks {}", if rows.is_empty() { String::from("none") } else { rows.join(",") }));
206        return true;
207    }
208    native::tasks(tasks)
209}
210
211/// whether a badge with count is shown already (with its icon): the UI then need not draw it
212/// again
213pub fn taskbar_badge_shown(count: u32) -> bool {
214    let s = state();
215    let want = if crate::tray::dry_run() { 3 } else { 2 };
216    s.count == count && s.icon.is_some() && SHOWN[K_BADGE].load(Ordering::Relaxed) == want
217}
218
219/// what was set last: the progress and the badge's count
220pub fn taskbar_state() -> (TaskbarProgress, u32) {
221    let s = state();
222    (s.progress, s.count)
223}
224
225/// whether this system shows the taskbar progress and badge
226pub fn taskbar_supported() -> bool {
227    crate::tray::dry_run() || native::supported()
228}
229
230/// (Windows) the process's explicit AppUserModelID (what the shell holds:
231/// GetCurrentProcessExplicitAppUserModelID); None: the one Windows derives from the program's
232/// path (other systems: always None)
233pub fn app_user_model_id() -> Option<String> {
234    native::app_id()
235}
236
237/// (Windows) run the process under the AppUserModelID id: before its first window -> false
238/// elsewhere, in an MSIX package and for an id Windows refuses
239pub fn set_app_user_model_id(id: &str) -> bool {
240    if id.is_empty() || id.chars().count() > 128 || id.chars().any(|c| c.is_whitespace() || c.is_control()) {
241        return false;
242    }
243    native::set_app_id(id)
244}
245
246/// (win_api.rs) a message of the main window: "TaskbarButtonCreated" applies the state again
247#[cfg(windows)]
248pub(crate) fn window_message(msg: u32) {
249    native::window_message(msg)
250}
251
252#[cfg(windows)]
253#[path = "win_taskbar.rs"]
254mod native;
255
256#[cfg(target_os = "linux")]
257#[path = "linux_taskbar.rs"]
258mod native;
259
260#[cfg(target_os = "macos")]
261use crate::apple::mac::services::dock as native;
262
263#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
264mod native {
265    use super::{JumpTask, State};
266    pub fn progress(_s: &State) -> bool {
267        false
268    }
269    pub fn badge(_s: &State) -> bool {
270        false
271    }
272    pub fn tasks(_t: &[JumpTask]) -> bool {
273        false
274    }
275    pub fn supported() -> bool {
276        false
277    }
278    pub fn set_app_id(_id: &str) -> bool {
279        false
280    }
281    pub fn app_id() -> Option<alloc::string::String> {
282        None
283    }
284}