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).  Web and Android: nothing (false).
45
46use alloc::string::String;
47use alloc::vec::Vec;
48use core::sync::atomic::{AtomicU8, Ordering};
49
50use rt::sync::SpinLock;
51
52use crate::tray::TrayIcon;
53
54/// what the taskbar button's progress bar shows
55#[derive(Clone, Copy, Debug, Default, PartialEq)]
56pub enum TaskbarProgress {
57    /// no bar
58    #[default]
59    None,
60    /// done so far (0.0 ..= 1.0)
61    Normal(f32),
62    /// paused at (Windows: yellow)
63    Paused(f32),
64    /// failed at (Windows: red)
65    Error(f32),
66    /// busy without a known end (Windows: a moving bar)
67    Indeterminate,
68}
69
70impl TaskbarProgress {
71    /// the fraction done, clamped to 0.0 ..= 1.0 (None / Indeterminate: 0.0)
72    pub fn value(&self) -> f32 {
73        match *self {
74            TaskbarProgress::Normal(v) | TaskbarProgress::Paused(v) | TaskbarProgress::Error(v) if v.is_finite() => v.clamp(0.0, 1.0),
75            _ => 0.0,
76        }
77    }
78
79    fn text(&self) -> String {
80        let pct = (self.value() * 100.0 + 0.5) as u32;
81        match self {
82            TaskbarProgress::None => String::from("none"),
83            TaskbarProgress::Normal(_) => alloc::format!("normal {pct}%"),
84            TaskbarProgress::Paused(_) => alloc::format!("paused {pct}%"),
85            TaskbarProgress::Error(_) => alloc::format!("error {pct}%"),
86            TaskbarProgress::Indeterminate => String::from("indeterminate"),
87        }
88    }
89}
90
91/// one task of the jump list: starts this program with args
92#[derive(Clone, Debug, Default, PartialEq, Eq)]
93pub struct JumpTask {
94    /// the row's text
95    pub title: String,
96    /// the arguments after the program
97    pub args: Vec<String>,
98    /// its tooltip ("" = none)
99    pub desc: String,
100}
101
102/// the state the platform code applies (again)
103#[derive(Clone, Default)]
104pub(crate) struct State {
105    pub(crate) progress: TaskbarProgress,
106    pub(crate) count: u32,
107    pub(crate) icon: Option<TrayIcon>,
108}
109
110static STATE: SpinLock<Option<State>> = SpinLock::new(None);
111
112/// (platform) a copy of the state
113#[allow(dead_code)]
114pub(crate) fn state() -> State {
115    STATE.lock().clone().unwrap_or_default()
116}
117
118// -> (the state before, after)
119fn update(f: impl FnOnce(&mut State)) -> (State, State) {
120    let mut g = STATE.lock();
121    let s = g.get_or_insert_with(State::default);
122    let old = s.clone();
123    f(s);
124    (old, s.clone())
125}
126
127// the answer of the last progress / badge call: 0 none yet, 1 false, 2 shown, 3 printed (dry run)
128static SHOWN: [AtomicU8; 2] = [AtomicU8::new(0), AtomicU8::new(0)];
129const K_PROGRESS: usize = 0;
130const K_BADGE: usize = 1;
131
132// whether the call can be skipped: nothing changed and the last answer stands
133fn unchanged(k: usize, same: bool, dry: bool) -> bool {
134    same && SHOWN[k].load(Ordering::Relaxed) == if dry { 3 } else { 2 }
135}
136
137fn answered(k: usize, ok: bool, dry: bool) -> bool {
138    SHOWN[k].store(if !ok { 1 } else if dry { 3 } else { 2 }, Ordering::Relaxed);
139    ok
140}
141
142// what the progress looks like: its kind and per mille
143fn look(p: TaskbarProgress) -> (u8, u32) {
144    let kind = match p {
145        TaskbarProgress::None => 0,
146        TaskbarProgress::Normal(_) => 1,
147        TaskbarProgress::Paused(_) => 2,
148        TaskbarProgress::Error(_) => 3,
149        TaskbarProgress::Indeterminate => 4,
150    };
151    (kind, (p.value() * 1000.0 + 0.5) as u32)
152}
153
154fn dry(line: &str) {
155    crate::write_stdout(alloc::format!("taskbar: {line}\n").as_bytes());
156}
157
158/// show progress on the app's taskbar button -> false: not shown here
159pub fn taskbar_progress(p: TaskbarProgress) -> bool {
160    let p = match p {
161        TaskbarProgress::Normal(_) => TaskbarProgress::Normal(p.value()),
162        TaskbarProgress::Paused(_) => TaskbarProgress::Paused(p.value()),
163        TaskbarProgress::Error(_) => TaskbarProgress::Error(p.value()),
164        other => other,
165    };
166    let (old, s) = update(|s| s.progress = p);
167    let is_dry = crate::tray::dry_run();
168    if unchanged(K_PROGRESS, look(old.progress) == look(p), is_dry) {
169        return true;
170    }
171    if is_dry {
172        dry(&alloc::format!("progress {}", p.text()));
173        return answered(K_PROGRESS, true, true);
174    }
175    answered(K_PROGRESS, native::progress(&s), false)
176}
177
178/// a badge on the button: count (0 = none), icon = Windows' overlay (None: no overlay there)
179pub fn taskbar_badge(count: u32, icon: Option<&TrayIcon>) -> bool {
180    let (old, s) = update(|s| {
181        s.count = count;
182        s.icon = if count == 0 { None } else { icon.cloned() };
183    });
184    let is_dry = crate::tray::dry_run();
185    if unchanged(K_BADGE, old.count == s.count && old.icon == s.icon, is_dry) {
186        return true;
187    }
188    if is_dry {
189        match &s.icon {
190            _ if count == 0 => dry("badge none"),
191            Some(i) => dry(&alloc::format!("badge {count} icon {}x{}", i.w, i.h)),
192            None => dry(&alloc::format!("badge {count}")),
193        }
194        return answered(K_BADGE, true, true);
195    }
196    answered(K_BADGE, native::badge(&s), false)
197}
198
199/// the tasks of the button's jump list (empty: none) -> false: no jump list here
200pub fn taskbar_tasks(tasks: &[JumpTask]) -> bool {
201    if crate::tray::dry_run() {
202        let rows: Vec<String> = tasks.iter().map(|t| alloc::format!("{}=[{}]", t.title, t.args.join(" "))).collect();
203        dry(&alloc::format!("tasks {}", if rows.is_empty() { String::from("none") } else { rows.join(",") }));
204        return true;
205    }
206    native::tasks(tasks)
207}
208
209/// whether a badge with count is shown already (with its icon): the UI then need not draw it
210/// again
211pub fn taskbar_badge_shown(count: u32) -> bool {
212    let s = state();
213    let want = if crate::tray::dry_run() { 3 } else { 2 };
214    s.count == count && s.icon.is_some() && SHOWN[K_BADGE].load(Ordering::Relaxed) == want
215}
216
217/// what was set last: the progress and the badge's count
218pub fn taskbar_state() -> (TaskbarProgress, u32) {
219    let s = state();
220    (s.progress, s.count)
221}
222
223/// whether this system shows the taskbar progress and badge
224pub fn taskbar_supported() -> bool {
225    crate::tray::dry_run() || native::supported()
226}
227
228/// (Windows) the process's explicit AppUserModelID (what the shell holds:
229/// GetCurrentProcessExplicitAppUserModelID); None: the one Windows derives from the program's
230/// path (other systems: always None)
231pub fn app_user_model_id() -> Option<String> {
232    native::app_id()
233}
234
235/// (Windows) run the process under the AppUserModelID id: before its first window -> false
236/// elsewhere, in an MSIX package and for an id Windows refuses
237pub fn set_app_user_model_id(id: &str) -> bool {
238    if id.is_empty() || id.chars().count() > 128 || id.chars().any(|c| c.is_whitespace() || c.is_control()) {
239        return false;
240    }
241    native::set_app_id(id)
242}
243
244/// (win_api.rs) a message of the main window: "TaskbarButtonCreated" applies the state again
245#[cfg(windows)]
246pub(crate) fn window_message(msg: u32) {
247    native::window_message(msg)
248}
249
250#[cfg(windows)]
251#[path = "win_taskbar.rs"]
252mod native;
253
254#[cfg(target_os = "linux")]
255#[path = "linux_taskbar.rs"]
256mod native;
257
258#[cfg(not(any(windows, target_os = "linux")))]
259mod native {
260    use super::{JumpTask, State};
261    pub fn progress(_s: &State) -> bool {
262        false
263    }
264    pub fn badge(_s: &State) -> bool {
265        false
266    }
267    pub fn tasks(_t: &[JumpTask]) -> bool {
268        false
269    }
270    pub fn supported() -> bool {
271        false
272    }
273    pub fn set_app_id(_id: &str) -> bool {
274        false
275    }
276    pub fn app_id() -> Option<alloc::string::String> {
277        None
278    }
279}