Skip to main content

incular_platform/
application_shell.rs

1//! Portable application-shell resource descriptions.
2//!
3//! These values deliberately contain no native handles. Runtime owns resource
4//! identity and callback lifetime; desktop adapters translate immutable
5//! snapshots into notification-area/status-bar, notification, and taskbar/Dock
6//! APIs at the native boundary.
7
8use crate::{WindowIcon, WindowId};
9use std::fmt;
10
11macro_rules! generational_id {
12    ($name:ident, $label:literal) => {
13        #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14        pub struct $name {
15            index: u32,
16            generation: u32,
17        }
18
19        impl $name {
20            #[must_use]
21            pub const fn from_parts(index: u32, generation: u32) -> Self {
22                Self { index, generation }
23            }
24
25            #[must_use]
26            pub const fn index(self) -> u32 {
27                self.index
28            }
29
30            #[must_use]
31            pub const fn generation(self) -> u32 {
32                self.generation
33            }
34        }
35
36        impl fmt::Debug for $name {
37            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38                write!(
39                    formatter,
40                    concat!($label, "Id({}, {})"),
41                    self.index, self.generation
42                )
43            }
44        }
45    };
46}
47
48generational_id!(TrayItemId, "TrayItem");
49generational_id!(NotificationId, "Notification");
50
51/// Shell facility requested by an application-scoped operation.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub enum ApplicationShellFeature {
54    TrayOrStatusItem,
55    Notifications,
56    NotificationActions,
57    NotificationUpdate,
58    NotificationDismiss,
59    TaskbarProgress,
60    ApplicationBadge,
61    TaskbarOverlayIcon,
62}
63
64/// Typed failure from a native application-shell operation.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ApplicationShellError {
67    Unsupported(ApplicationShellFeature),
68    PermissionDenied,
69    PlatformConfigurationRequired(String),
70    StaleResource,
71    ApplicationStopped,
72    NativeFailure(String),
73}
74
75impl fmt::Display for ApplicationShellError {
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Unsupported(feature) => write!(formatter, "{feature:?} is not supported"),
79            Self::PermissionDenied => formatter.write_str("application-shell permission denied"),
80            Self::PlatformConfigurationRequired(detail) => {
81                write!(formatter, "platform configuration is required: {detail}")
82            }
83            Self::StaleResource => formatter.write_str("application-shell resource is stale"),
84            Self::ApplicationStopped => formatter.write_str("application has stopped"),
85            Self::NativeFailure(detail) => {
86                write!(formatter, "native shell operation failed: {detail}")
87            }
88        }
89    }
90}
91
92impl std::error::Error for ApplicationShellError {}
93
94/// Native presentation for one application-owned tray/status item.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct TrayItemPresentation {
97    pub icon: Option<WindowIcon>,
98    pub tooltip: Option<String>,
99    pub title: Option<String>,
100    pub visible: bool,
101}
102
103impl TrayItemPresentation {
104    #[must_use]
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    #[must_use]
110    pub fn icon(mut self, icon: WindowIcon) -> Self {
111        self.icon = Some(icon);
112        self
113    }
114
115    #[must_use]
116    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
117        self.tooltip = Some(tooltip.into());
118        self
119    }
120
121    #[must_use]
122    pub fn title(mut self, title: impl Into<String>) -> Self {
123        self.title = Some(title.into());
124        self
125    }
126
127    #[must_use]
128    pub fn visible(mut self, visible: bool) -> Self {
129        self.visible = visible;
130        self
131    }
132}
133
134impl Default for TrayItemPresentation {
135    fn default() -> Self {
136        Self {
137            icon: None,
138            tooltip: None,
139            title: None,
140            visible: true,
141        }
142    }
143}
144
145/// Stable application-defined identity for a notification action.
146#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
147pub struct NotificationActionId(String);
148
149impl NotificationActionId {
150    #[must_use]
151    pub fn new(id: impl Into<String>) -> Self {
152        Self(id.into())
153    }
154
155    #[must_use]
156    pub fn as_str(&self) -> &str {
157        &self.0
158    }
159}
160
161impl From<&str> for NotificationActionId {
162    fn from(value: &str) -> Self {
163        Self::new(value)
164    }
165}
166
167impl From<String> for NotificationActionId {
168    fn from(value: String) -> Self {
169        Self::new(value)
170    }
171}
172
173/// One optional native notification action.
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct NotificationAction {
176    pub id: NotificationActionId,
177    pub label: String,
178}
179
180impl NotificationAction {
181    #[must_use]
182    pub fn new(id: impl Into<NotificationActionId>, label: impl Into<String>) -> Self {
183        Self {
184            id: id.into(),
185            label: label.into(),
186        }
187    }
188}
189
190/// Complete desired presentation of one desktop notification.
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct NotificationPresentation {
193    pub title: String,
194    pub body: String,
195    pub icon: Option<WindowIcon>,
196    pub actions: Vec<NotificationAction>,
197}
198
199impl NotificationPresentation {
200    #[must_use]
201    pub fn new(title: impl Into<String>, body: impl Into<String>) -> Self {
202        Self {
203            title: title.into(),
204            body: body.into(),
205            icon: None,
206            actions: Vec::new(),
207        }
208    }
209
210    #[must_use]
211    pub fn icon(mut self, icon: WindowIcon) -> Self {
212        self.icon = Some(icon);
213        self
214    }
215
216    #[must_use]
217    pub fn action(mut self, action: NotificationAction) -> Self {
218        self.actions.push(action);
219        self
220    }
221}
222
223/// Progress state rendered by a taskbar button or application Dock tile.
224#[derive(Clone, Copy, Debug, Default, PartialEq)]
225pub enum TaskbarProgress {
226    #[default]
227    None,
228    Indeterminate,
229    Normal(f64),
230    Paused(f64),
231    Error(f64),
232}
233
234impl TaskbarProgress {
235    pub fn normal(value: f64) -> Result<Self, TaskbarProgressError> {
236        validate_progress(value).map(Self::Normal)
237    }
238
239    pub fn paused(value: f64) -> Result<Self, TaskbarProgressError> {
240        validate_progress(value).map(Self::Paused)
241    }
242
243    pub fn error(value: f64) -> Result<Self, TaskbarProgressError> {
244        validate_progress(value).map(Self::Error)
245    }
246
247    #[must_use]
248    pub const fn fraction(self) -> Option<f64> {
249        match self {
250            Self::Normal(value) | Self::Paused(value) | Self::Error(value) => Some(value),
251            Self::None | Self::Indeterminate => None,
252        }
253    }
254}
255
256fn validate_progress(value: f64) -> Result<f64, TaskbarProgressError> {
257    (value.is_finite() && (0.0..=1.0).contains(&value))
258        .then_some(value)
259        .ok_or(TaskbarProgressError)
260}
261
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub struct TaskbarProgressError;
264
265impl fmt::Display for TaskbarProgressError {
266    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
267        formatter.write_str("taskbar progress must be a finite fraction between 0 and 1")
268    }
269}
270
271impl std::error::Error for TaskbarProgressError {}
272
273/// Badge rendered by a taskbar/Dock integration when the active platform has
274/// native badge support.
275#[derive(Clone, Debug, Default, PartialEq, Eq)]
276pub enum ApplicationBadge {
277    #[default]
278    None,
279    Count(u64),
280    Text(String),
281}
282
283/// Desired application shell state. `window_id` is a native taskbar-button
284/// target on platforms that require one (notably Windows); application-scoped
285/// Dock implementations may ignore it.
286#[derive(Clone, Debug, Default, PartialEq)]
287pub struct TaskbarDockState {
288    pub window_id: Option<WindowId>,
289    pub progress: TaskbarProgress,
290    pub badge: ApplicationBadge,
291    pub overlay_icon: Option<WindowIcon>,
292}
293
294impl TaskbarDockState {
295    #[must_use]
296    pub fn for_window(window_id: WindowId) -> Self {
297        Self {
298            window_id: Some(window_id),
299            ..Self::default()
300        }
301    }
302
303    #[must_use]
304    pub fn progress(mut self, progress: TaskbarProgress) -> Self {
305        self.progress = progress;
306        self
307    }
308
309    #[must_use]
310    pub fn badge(mut self, badge: ApplicationBadge) -> Self {
311        self.badge = badge;
312        self
313    }
314
315    #[must_use]
316    pub fn overlay_icon(mut self, icon: WindowIcon) -> Self {
317        self.overlay_icon = Some(icon);
318        self
319    }
320
321    /// The most specific capability needed to apply this state.
322    #[must_use]
323    pub fn required_feature(&self) -> ApplicationShellFeature {
324        if self.overlay_icon.is_some() {
325            ApplicationShellFeature::TaskbarOverlayIcon
326        } else if self.badge != ApplicationBadge::None {
327            ApplicationShellFeature::ApplicationBadge
328        } else {
329            ApplicationShellFeature::TaskbarProgress
330        }
331    }
332}