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, thiserror::Error)]
66pub enum ApplicationShellError {
67    #[error("{0:?} is not supported")]
68    Unsupported(ApplicationShellFeature),
69    #[error("application-shell permission denied")]
70    PermissionDenied,
71    #[error("platform configuration is required: {0}")]
72    PlatformConfigurationRequired(String),
73    #[error("application-shell resource is stale")]
74    StaleResource,
75    #[error("application has stopped")]
76    ApplicationStopped,
77    #[error("native shell operation failed: {0}")]
78    NativeFailure(String),
79}
80
81/// Native presentation for one application-owned tray/status item.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct TrayItemPresentation {
84    pub icon: Option<WindowIcon>,
85    pub tooltip: Option<String>,
86    pub title: Option<String>,
87    pub visible: bool,
88}
89
90impl TrayItemPresentation {
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    #[must_use]
97    pub fn icon(mut self, icon: WindowIcon) -> Self {
98        self.icon = Some(icon);
99        self
100    }
101
102    #[must_use]
103    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
104        self.tooltip = Some(tooltip.into());
105        self
106    }
107
108    #[must_use]
109    pub fn title(mut self, title: impl Into<String>) -> Self {
110        self.title = Some(title.into());
111        self
112    }
113
114    #[must_use]
115    pub fn visible(mut self, visible: bool) -> Self {
116        self.visible = visible;
117        self
118    }
119}
120
121impl Default for TrayItemPresentation {
122    fn default() -> Self {
123        Self {
124            icon: None,
125            tooltip: None,
126            title: None,
127            visible: true,
128        }
129    }
130}
131
132/// Stable application-defined identity for a notification action.
133#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
134pub struct NotificationActionId(String);
135
136impl NotificationActionId {
137    #[must_use]
138    pub fn new(id: impl Into<String>) -> Self {
139        Self(id.into())
140    }
141
142    #[must_use]
143    pub fn as_str(&self) -> &str {
144        &self.0
145    }
146}
147
148impl From<&str> for NotificationActionId {
149    fn from(value: &str) -> Self {
150        Self::new(value)
151    }
152}
153
154impl From<String> for NotificationActionId {
155    fn from(value: String) -> Self {
156        Self::new(value)
157    }
158}
159
160/// One optional native notification action.
161#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct NotificationAction {
163    pub id: NotificationActionId,
164    pub label: String,
165}
166
167impl NotificationAction {
168    #[must_use]
169    pub fn new(id: impl Into<NotificationActionId>, label: impl Into<String>) -> Self {
170        Self {
171            id: id.into(),
172            label: label.into(),
173        }
174    }
175}
176
177/// Complete desired presentation of one desktop notification.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub struct NotificationPresentation {
180    pub title: String,
181    pub body: String,
182    pub icon: Option<WindowIcon>,
183    pub actions: Vec<NotificationAction>,
184}
185
186impl NotificationPresentation {
187    #[must_use]
188    pub fn new(title: impl Into<String>, body: impl Into<String>) -> Self {
189        Self {
190            title: title.into(),
191            body: body.into(),
192            icon: None,
193            actions: Vec::new(),
194        }
195    }
196
197    #[must_use]
198    pub fn icon(mut self, icon: WindowIcon) -> Self {
199        self.icon = Some(icon);
200        self
201    }
202
203    #[must_use]
204    pub fn action(mut self, action: NotificationAction) -> Self {
205        self.actions.push(action);
206        self
207    }
208}
209
210/// Progress state rendered by a taskbar button or application Dock tile.
211#[derive(Clone, Copy, Debug, Default, PartialEq)]
212pub enum TaskbarProgress {
213    #[default]
214    None,
215    Indeterminate,
216    Normal(f64),
217    Paused(f64),
218    Error(f64),
219}
220
221impl TaskbarProgress {
222    pub fn normal(value: f64) -> Result<Self, TaskbarProgressError> {
223        validate_progress(value).map(Self::Normal)
224    }
225
226    pub fn paused(value: f64) -> Result<Self, TaskbarProgressError> {
227        validate_progress(value).map(Self::Paused)
228    }
229
230    pub fn error(value: f64) -> Result<Self, TaskbarProgressError> {
231        validate_progress(value).map(Self::Error)
232    }
233
234    #[must_use]
235    pub const fn fraction(self) -> Option<f64> {
236        match self {
237            Self::Normal(value) | Self::Paused(value) | Self::Error(value) => Some(value),
238            Self::None | Self::Indeterminate => None,
239        }
240    }
241}
242
243fn validate_progress(value: f64) -> Result<f64, TaskbarProgressError> {
244    (value.is_finite() && (0.0..=1.0).contains(&value))
245        .then_some(value)
246        .ok_or(TaskbarProgressError)
247}
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
250#[error("taskbar progress must be a finite fraction between 0 and 1")]
251pub struct TaskbarProgressError;
252
253/// Badge rendered by a taskbar/Dock integration when the active platform has
254/// native badge support.
255#[derive(Clone, Debug, Default, PartialEq, Eq)]
256pub enum ApplicationBadge {
257    #[default]
258    None,
259    Count(u64),
260    Text(String),
261}
262
263/// Desired application shell state. `window_id` is a native taskbar-button
264/// target on platforms that require one (notably Windows); application-scoped
265/// Dock implementations may ignore it.
266#[derive(Clone, Debug, Default, PartialEq)]
267pub struct TaskbarDockState {
268    pub window_id: Option<WindowId>,
269    pub progress: TaskbarProgress,
270    pub badge: ApplicationBadge,
271    pub overlay_icon: Option<WindowIcon>,
272}
273
274impl TaskbarDockState {
275    #[must_use]
276    pub fn for_window(window_id: WindowId) -> Self {
277        Self {
278            window_id: Some(window_id),
279            ..Self::default()
280        }
281    }
282
283    #[must_use]
284    pub fn progress(mut self, progress: TaskbarProgress) -> Self {
285        self.progress = progress;
286        self
287    }
288
289    #[must_use]
290    pub fn badge(mut self, badge: ApplicationBadge) -> Self {
291        self.badge = badge;
292        self
293    }
294
295    #[must_use]
296    pub fn overlay_icon(mut self, icon: WindowIcon) -> Self {
297        self.overlay_icon = Some(icon);
298        self
299    }
300
301    /// The most specific capability needed to apply this state.
302    #[must_use]
303    pub fn required_feature(&self) -> ApplicationShellFeature {
304        if self.overlay_icon.is_some() {
305            ApplicationShellFeature::TaskbarOverlayIcon
306        } else if self.badge != ApplicationBadge::None {
307            ApplicationShellFeature::ApplicationBadge
308        } else {
309            ApplicationShellFeature::TaskbarProgress
310        }
311    }
312}