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}