1use crate::{request_registry::RequestRegistry, tasks::RuntimeWake};
4use incular_platform::{
5 ApplicationShellError, ApplicationShellFeature, CapabilitySupport, NotificationActionId,
6 NotificationId, NotificationPresentation, PlatformCapabilities, TaskbarDockState, TrayItemId,
7 TrayItemPresentation,
8};
9use incular_widgets::internal::PlatformMenuCommandModel;
10use incular_widgets::{
11 PlatformMenu, PlatformMenuBuildError, PlatformMenuEvent, PlatformMenuSnapshot,
12};
13use std::{
14 cell::RefCell,
15 collections::VecDeque,
16 rc::Rc,
17 sync::{Arc, Mutex, RwLock},
18};
19
20#[derive(Clone, Debug)]
21pub enum NativeApplicationShellOperation {
22 CreateTray {
23 id: TrayItemId,
24 presentation: TrayItemPresentation,
25 menu: PlatformMenuSnapshot,
26 },
27 UpdateTray {
28 id: TrayItemId,
29 presentation: TrayItemPresentation,
30 menu: PlatformMenuSnapshot,
31 },
32 RemoveTray {
33 id: TrayItemId,
34 },
35 ShowNotification {
36 id: NotificationId,
37 presentation: NotificationPresentation,
38 },
39 UpdateNotification {
40 id: NotificationId,
41 presentation: NotificationPresentation,
42 },
43 CloseNotification {
44 id: NotificationId,
45 },
46 SetTaskbarDockState(TaskbarDockState),
47}
48
49impl NativeApplicationShellOperation {
50 #[must_use]
52 pub fn required_feature(&self) -> ApplicationShellFeature {
53 match self {
54 Self::CreateTray { .. } | Self::UpdateTray { .. } | Self::RemoveTray { .. } => {
55 ApplicationShellFeature::TrayOrStatusItem
56 }
57 Self::ShowNotification { .. } => ApplicationShellFeature::Notifications,
58 Self::UpdateNotification { .. } => ApplicationShellFeature::NotificationUpdate,
59 Self::CloseNotification { .. } => ApplicationShellFeature::NotificationDismiss,
60 Self::SetTaskbarDockState(state) => state.required_feature(),
61 }
62 }
63}
64
65#[derive(Clone, Debug)]
66pub struct NativeApplicationShellRequest {
67 pub request_id: ApplicationShellRequestId,
68 pub operation: NativeApplicationShellOperation,
69}
70
71#[derive(Clone, Debug)]
72pub struct NativeApplicationShellCompletion {
73 pub request_id: ApplicationShellRequestId,
74 pub result: Result<(), ApplicationShellError>,
75}
76
77#[derive(Clone, Debug)]
86pub enum NativeApplicationShellApplyResult {
87 Completed(Result<(), ApplicationShellError>),
88 Deferred,
89}
90
91impl From<Result<(), ApplicationShellError>> for NativeApplicationShellApplyResult {
92 fn from(result: Result<(), ApplicationShellError>) -> Self {
93 Self::Completed(result)
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub struct ApplicationShellRequestId(u64);
99
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum NativeApplicationShellEvent {
102 TrayMenu {
103 id: TrayItemId,
104 event: PlatformMenuEvent,
105 },
106 TrayActivated {
107 id: TrayItemId,
108 },
109 NotificationActivated {
110 id: NotificationId,
111 },
112 NotificationAction {
113 id: NotificationId,
114 action: NotificationActionId,
115 },
116 NotificationDismissed {
117 id: NotificationId,
118 },
119}
120
121struct Slot<T> {
122 generation: u32,
123 retired: bool,
124 value: Option<T>,
125}
126
127impl<T> Default for Slot<T> {
128 fn default() -> Self {
129 Self {
130 generation: 0,
131 retired: false,
132 value: None,
133 }
134 }
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138enum ResourcePhase {
139 Queued,
140 Dispatched,
141 Live,
142 Failed,
143}
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145enum ShellResource {
146 Tray(TrayItemId),
147 Notification(NotificationId),
148}
149
150struct TrayRecord {
151 phase: ResourcePhase,
152 presentation: TrayItemPresentation,
153 menu: PlatformMenuCommandModel,
154 on_activated: Option<Arc<dyn Fn() + Send + Sync>>,
155}
156
157struct NotificationRecord {
158 phase: ResourcePhase,
159 presentation: NotificationPresentation,
160 on_activated: Option<Arc<dyn Fn() + Send + Sync>>,
161 on_action: Option<Arc<dyn Fn(NotificationActionId) + Send + Sync>>,
162 on_dismissed: Option<Arc<dyn Fn() + Send + Sync>>,
163}
164
165#[derive(Default)]
166struct ShellState {
167 trays: Vec<Slot<TrayRecord>>,
168 notifications: Vec<Slot<NotificationRecord>>,
169 requests: VecDeque<NativeApplicationShellRequest>,
170 pending: RequestRegistry<ApplicationShellRequestId, NativeApplicationShellOperation>,
171 completions: VecDeque<NativeApplicationShellCompletion>,
172 next_request: u64,
173 stopped: bool,
174 notification_identity: Option<String>,
175}
176
177#[derive(Clone)]
178pub struct ApplicationShellService {
179 state: Rc<RefCell<ShellState>>,
180 capabilities: Arc<RwLock<PlatformCapabilities>>,
181 wake: Rc<RefCell<Option<Arc<dyn RuntimeWake>>>>,
182}
183
184impl ApplicationShellService {
185 pub(crate) fn new(capabilities: Arc<RwLock<PlatformCapabilities>>) -> Self {
186 Self {
187 state: Rc::new(RefCell::new(ShellState::default())),
188 capabilities,
189 wake: Rc::new(RefCell::new(None)),
190 }
191 }
192
193 pub(crate) fn set_wake(&self, wake: Arc<dyn RuntimeWake>) {
194 *self.wake.borrow_mut() = Some(wake);
195 }
196
197 fn wake(&self) {
198 let wake = self.wake.borrow().clone();
199 if let Some(wake) = wake {
200 wake.wake();
201 }
202 }
203
204 #[doc(hidden)]
205 #[must_use]
206 pub fn new_for_test(capabilities: Arc<RwLock<PlatformCapabilities>>) -> Self {
207 Self::new(capabilities)
208 }
209
210 #[must_use]
211 pub fn capabilities(&self) -> incular_platform::ApplicationServiceCapabilities {
212 self.capabilities
213 .read()
214 .expect("platform capabilities lock")
215 .application_services
216 }
217
218 pub fn create_tray_item(
219 &self,
220 presentation: TrayItemPresentation,
221 menus: &[PlatformMenu],
222 ) -> Result<TrayItemHandle, ApplicationShellError> {
223 self.require_support(
224 self.capabilities().tray_or_status_item,
225 ApplicationShellFeature::TrayOrStatusItem,
226 )?;
227 let menu = PlatformMenuCommandModel::compile(menus).map_err(menu_build_error)?;
228 let mut state = self.state.borrow_mut();
229 ensure_running(&state)?;
230 let id = reserve_tray(
231 &mut state.trays,
232 TrayRecord {
233 phase: ResourcePhase::Queued,
234 presentation: presentation.clone(),
235 menu: menu.clone(),
236 on_activated: None,
237 },
238 );
239 enqueue(
240 &mut state,
241 NativeApplicationShellOperation::CreateTray {
242 id,
243 presentation,
244 menu: menu.snapshot().clone(),
245 },
246 );
247 self.wake();
248 Ok(TrayItemHandle {
249 id,
250 service: self.clone(),
251 released: false,
252 })
253 }
254
255 pub fn show_notification(
256 &self,
257 presentation: NotificationPresentation,
258 ) -> Result<NotificationHandle, ApplicationShellError> {
259 let capabilities = self.capabilities();
260 self.require_support(
261 capabilities.notifications,
262 ApplicationShellFeature::Notifications,
263 )?;
264 if !presentation.actions.is_empty() {
265 self.require_support(
266 capabilities.notification_actions,
267 ApplicationShellFeature::NotificationActions,
268 )?;
269 }
270 let mut state = self.state.borrow_mut();
271 ensure_running(&state)?;
272 let id = reserve_notification(
273 &mut state.notifications,
274 NotificationRecord {
275 phase: ResourcePhase::Queued,
276 presentation: presentation.clone(),
277 on_activated: None,
278 on_action: None,
279 on_dismissed: None,
280 },
281 );
282 enqueue(
283 &mut state,
284 NativeApplicationShellOperation::ShowNotification { id, presentation },
285 );
286 self.wake();
287 Ok(NotificationHandle {
288 id,
289 service: self.clone(),
290 released: false,
291 })
292 }
293
294 pub fn set_notification_identity(
298 &self,
299 identity: impl Into<String>,
300 ) -> Result<(), ApplicationShellError> {
301 let identity = identity.into();
302 if identity.trim().is_empty() || identity.chars().any(char::is_control) {
303 return Err(ApplicationShellError::PlatformConfigurationRequired(
304 "notification identity must be a non-empty printable installed application identity".to_owned(),
305 ));
306 }
307 let mut state = self.state.borrow_mut();
308 ensure_running(&state)?;
309 state.notification_identity = Some(identity);
310 Ok(())
311 }
312
313 #[doc(hidden)]
314 #[must_use]
315 pub fn notification_identity(&self) -> Option<String> {
316 self.state.borrow().notification_identity.clone()
317 }
318
319 pub fn set_taskbar_dock_state(
320 &self,
321 state_value: TaskbarDockState,
322 ) -> Result<(), ApplicationShellError> {
323 let capabilities = self.capabilities();
324 if state_value.progress != incular_platform::TaskbarProgress::None {
325 self.require_support(
326 capabilities.taskbar_progress,
327 ApplicationShellFeature::TaskbarProgress,
328 )?;
329 }
330 if state_value.badge != incular_platform::ApplicationBadge::None {
331 self.require_support(
332 capabilities.application_badge,
333 ApplicationShellFeature::ApplicationBadge,
334 )?;
335 }
336 if state_value.overlay_icon.is_some() {
337 self.require_support(
338 capabilities.taskbar_overlay_icon,
339 ApplicationShellFeature::TaskbarOverlayIcon,
340 )?;
341 }
342 let mut state = self.state.borrow_mut();
343 ensure_running(&state)?;
344 enqueue(
345 &mut state,
346 NativeApplicationShellOperation::SetTaskbarDockState(state_value),
347 );
348 self.wake();
349 Ok(())
350 }
351
352 fn require_support(
353 &self,
354 support: CapabilitySupport,
355 feature: ApplicationShellFeature,
356 ) -> Result<(), ApplicationShellError> {
357 if support == CapabilitySupport::Unsupported {
358 Err(ApplicationShellError::Unsupported(feature))
359 } else {
360 Ok(())
361 }
362 }
363
364 pub(crate) fn take_native_requests(&self) -> Vec<NativeApplicationShellRequest> {
365 let mut state = self.state.borrow_mut();
366 let requests: Vec<_> = state.requests.drain(..).collect();
367 for request in &requests {
368 state.pending.dispatch(&request.request_id);
369 if let Some(resource) = created_resource(&request.operation) {
370 set_resource_phase(&mut state, resource, ResourcePhase::Dispatched);
371 }
372 }
373 requests
374 }
375
376 #[doc(hidden)]
377 pub fn take_native_requests_for_test(&self) -> Vec<NativeApplicationShellRequest> {
378 self.take_native_requests()
379 }
380
381 pub(crate) fn complete(&self, completion: NativeApplicationShellCompletion) {
382 let mut state = self.state.borrow_mut();
383 if let Ok(operation) = state.pending.complete(&completion.request_id, |_| true) {
384 if let Some(resource) = created_resource(&operation) {
385 let phase = if completion.result.is_ok() {
386 ResourcePhase::Live
387 } else {
388 ResourcePhase::Failed
389 };
390 set_resource_phase(&mut state, resource, phase);
391 if phase == ResourcePhase::Failed {
392 cancel_queued_resource(&mut state, resource);
393 }
394 }
395 state.completions.push_back(completion);
396 }
397 }
398
399 #[doc(hidden)]
400 pub fn complete_for_test(&self, completion: NativeApplicationShellCompletion) {
401 self.complete(completion);
402 }
403
404 #[must_use]
405 pub fn take_completions(&self) -> Vec<NativeApplicationShellCompletion> {
406 self.state.borrow_mut().completions.drain(..).collect()
407 }
408
409 pub(crate) fn handle_native_event(&self, event: NativeApplicationShellEvent) -> bool {
410 match event {
411 NativeApplicationShellEvent::TrayMenu { id, event } => {
412 let callback = {
413 let state = self.state.borrow_mut();
414 tray_record(&state.trays, id).map(|record| record.menu.clone())
415 };
416 callback.is_some_and(|menu| {
417 matches!(
418 menu.dispatch(&event),
419 incular_widgets::MenuDispatchResult::Handled
420 )
421 })
422 }
423 NativeApplicationShellEvent::TrayActivated { id } => {
424 let callback = {
425 let state = self.state.borrow();
426 tray_record(&state.trays, id).and_then(|record| record.on_activated.clone())
427 };
428 callback.is_some_and(|callback| {
429 callback();
430 true
431 })
432 }
433 NativeApplicationShellEvent::NotificationActivated { id } => {
434 notification_callback(&self.state, id, |record| record.on_activated.clone())
435 }
436 NativeApplicationShellEvent::NotificationAction { id, action } => {
437 let callback = {
438 let state = self.state.borrow_mut();
439 notification_record(&state.notifications, id).and_then(|record| {
440 record
441 .presentation
442 .actions
443 .iter()
444 .any(|known| known.id == action)
445 .then(|| record.on_action.clone())
446 .flatten()
447 })
448 };
449 callback.is_some_and(|callback| {
450 callback(action);
451 true
452 })
453 }
454 NativeApplicationShellEvent::NotificationDismissed { id } => {
455 let handled =
456 notification_callback(&self.state, id, |record| record.on_dismissed.clone());
457 let dismiss_native =
458 self.capabilities().notification_dismiss != CapabilitySupport::Unsupported;
459 let mut state = self.state.borrow_mut();
460 let phase =
461 notification_record(&state.notifications, id).map(|record| record.phase);
462 let released = release_notification(&mut state.notifications, id);
463 if let Some(phase) = phase {
464 retire_resource(
465 &mut state,
466 ShellResource::Notification(id),
467 phase,
468 dismiss_native,
469 );
470 }
471 drop(state);
472 if released && dismiss_native {
473 self.wake();
474 }
475 handled || released
476 }
477 }
478 }
479
480 #[doc(hidden)]
481 pub fn handle_native_event_for_test(&self, event: NativeApplicationShellEvent) -> bool {
482 self.handle_native_event(event)
483 }
484
485 pub(crate) fn stop(&self) {
486 let mut state = self.state.borrow_mut();
487 state.stopped = true;
488 state.requests.clear();
489 let pending: Vec<_> = state
490 .pending
491 .drain()
492 .map(|(request_id, _)| NativeApplicationShellCompletion {
493 request_id,
494 result: Err(ApplicationShellError::ApplicationStopped),
495 })
496 .collect();
497 state.completions.extend(pending);
498 for slot in &mut state.trays {
499 retire_slot(slot);
500 }
501 for slot in &mut state.notifications {
502 retire_slot(slot);
503 }
504 }
505}
506
507fn notification_callback<T>(
508 state: &Rc<RefCell<ShellState>>,
509 id: NotificationId,
510 select: impl FnOnce(&NotificationRecord) -> Option<Arc<T>>,
511) -> bool
512where
513 T: Fn() + Send + Sync + ?Sized,
514{
515 let callback = {
516 let state = state.borrow();
517 notification_record(&state.notifications, id).and_then(select)
518 };
519 callback.is_some_and(|callback| {
520 callback();
521 true
522 })
523}
524
525fn operation_resource(operation: &NativeApplicationShellOperation) -> Option<ShellResource> {
526 match operation {
527 NativeApplicationShellOperation::CreateTray { id, .. }
528 | NativeApplicationShellOperation::UpdateTray { id, .. }
529 | NativeApplicationShellOperation::RemoveTray { id } => Some(ShellResource::Tray(*id)),
530 NativeApplicationShellOperation::ShowNotification { id, .. }
531 | NativeApplicationShellOperation::UpdateNotification { id, .. }
532 | NativeApplicationShellOperation::CloseNotification { id } => {
533 Some(ShellResource::Notification(*id))
534 }
535 NativeApplicationShellOperation::SetTaskbarDockState(_) => None,
536 }
537}
538fn created_resource(operation: &NativeApplicationShellOperation) -> Option<ShellResource> {
539 match operation {
540 NativeApplicationShellOperation::CreateTray { .. }
541 | NativeApplicationShellOperation::ShowNotification { .. } => operation_resource(operation),
542 _ => None,
543 }
544}
545fn set_resource_phase(state: &mut ShellState, resource: ShellResource, phase: ResourcePhase) {
546 match resource {
547 ShellResource::Tray(id) => {
548 if let Some(record) = tray_record_mut(&mut state.trays, id) {
549 record.phase = phase;
550 }
551 }
552 ShellResource::Notification(id) => {
553 if let Some(record) = notification_record_mut(&mut state.notifications, id) {
554 record.phase = phase;
555 }
556 }
557 }
558}
559fn cancel_queued_resource(state: &mut ShellState, resource: ShellResource) {
560 let ids: Vec<_> = state
561 .requests
562 .iter()
563 .filter(|request| operation_resource(&request.operation) == Some(resource))
564 .map(|request| request.request_id)
565 .collect();
566 state
567 .requests
568 .retain(|request| operation_resource(&request.operation) != Some(resource));
569 for request_id in ids {
570 state.pending.remove(&request_id);
571 state
572 .completions
573 .push_back(NativeApplicationShellCompletion {
574 request_id,
575 result: Err(ApplicationShellError::StaleResource),
576 });
577 }
578}
579fn retire_resource(
580 state: &mut ShellState,
581 resource: ShellResource,
582 phase: ResourcePhase,
583 native_cleanup: bool,
584) {
585 cancel_queued_resource(state, resource);
586 if native_cleanup && matches!(phase, ResourcePhase::Dispatched | ResourcePhase::Live) {
587 enqueue(
588 state,
589 match resource {
590 ShellResource::Tray(id) => NativeApplicationShellOperation::RemoveTray { id },
591 ShellResource::Notification(id) => {
592 NativeApplicationShellOperation::CloseNotification { id }
593 }
594 },
595 );
596 }
597}
598
599fn menu_build_error(error: PlatformMenuBuildError) -> ApplicationShellError {
600 ApplicationShellError::NativeFailure(error.to_string())
601}
602
603fn ensure_running(state: &ShellState) -> Result<(), ApplicationShellError> {
604 (!state.stopped)
605 .then_some(())
606 .ok_or(ApplicationShellError::ApplicationStopped)
607}
608
609fn enqueue(state: &mut ShellState, operation: NativeApplicationShellOperation) {
610 let request_id = ApplicationShellRequestId(state.next_request);
611 state.next_request = state
612 .next_request
613 .checked_add(1)
614 .expect("shell request identity exhausted");
615 state.pending.insert(request_id, operation.clone());
616 state.requests.push_back(NativeApplicationShellRequest {
617 request_id,
618 operation,
619 });
620}
621
622fn reserve_tray(slots: &mut Vec<Slot<TrayRecord>>, value: TrayRecord) -> TrayItemId {
623 if let Some((index, slot)) = slots
624 .iter_mut()
625 .enumerate()
626 .find(|(_, slot)| !slot.retired && slot.value.is_none())
627 {
628 slot.value = Some(value);
629 return TrayItemId::from_parts(
630 u32::try_from(index).expect("tray index was validated on insertion"),
631 slot.generation,
632 );
633 }
634 let index = u32::try_from(slots.len()).expect("tray identity space exhausted");
635 slots.push(Slot {
636 generation: 0,
637 retired: false,
638 value: Some(value),
639 });
640 TrayItemId::from_parts(index, 0)
641}
642
643fn reserve_notification(
644 slots: &mut Vec<Slot<NotificationRecord>>,
645 value: NotificationRecord,
646) -> NotificationId {
647 if let Some((index, slot)) = slots
648 .iter_mut()
649 .enumerate()
650 .find(|(_, slot)| !slot.retired && slot.value.is_none())
651 {
652 slot.value = Some(value);
653 return NotificationId::from_parts(
654 u32::try_from(index).expect("notification index was validated on insertion"),
655 slot.generation,
656 );
657 }
658 let index = u32::try_from(slots.len()).expect("notification identity space exhausted");
659 slots.push(Slot {
660 generation: 0,
661 retired: false,
662 value: Some(value),
663 });
664 NotificationId::from_parts(index, 0)
665}
666
667fn tray_record(slots: &[Slot<TrayRecord>], id: TrayItemId) -> Option<&TrayRecord> {
668 slots
669 .get(id.index() as usize)
670 .filter(|slot| slot.generation == id.generation())?
671 .value
672 .as_ref()
673}
674
675fn tray_record_mut(slots: &mut [Slot<TrayRecord>], id: TrayItemId) -> Option<&mut TrayRecord> {
676 slots
677 .get_mut(id.index() as usize)
678 .filter(|slot| slot.generation == id.generation())?
679 .value
680 .as_mut()
681}
682
683fn notification_record(
684 slots: &[Slot<NotificationRecord>],
685 id: NotificationId,
686) -> Option<&NotificationRecord> {
687 slots
688 .get(id.index() as usize)
689 .filter(|slot| slot.generation == id.generation())?
690 .value
691 .as_ref()
692}
693
694fn notification_record_mut(
695 slots: &mut [Slot<NotificationRecord>],
696 id: NotificationId,
697) -> Option<&mut NotificationRecord> {
698 slots
699 .get_mut(id.index() as usize)
700 .filter(|slot| slot.generation == id.generation())?
701 .value
702 .as_mut()
703}
704
705fn release_notification(slots: &mut [Slot<NotificationRecord>], id: NotificationId) -> bool {
706 let Some(slot) = slots
707 .get_mut(id.index() as usize)
708 .filter(|slot| slot.generation == id.generation())
709 else {
710 return false;
711 };
712 if slot.value.is_none() {
713 return false;
714 }
715 retire_slot(slot);
716 true
717}
718
719fn retire_slot<T>(slot: &mut Slot<T>) {
720 if slot.value.take().is_none() {
721 return;
722 }
723 if let Some(next_generation) = slot.generation.checked_add(1) {
724 slot.generation = next_generation;
725 } else {
726 slot.retired = true;
727 }
728}
729
730pub struct TrayItemHandle {
731 id: TrayItemId,
732 service: ApplicationShellService,
733 released: bool,
734}
735
736impl TrayItemHandle {
737 #[must_use]
738 pub const fn id(&self) -> TrayItemId {
739 self.id
740 }
741
742 pub fn update(
743 &self,
744 presentation: TrayItemPresentation,
745 menus: &[PlatformMenu],
746 ) -> Result<(), ApplicationShellError> {
747 self.service.require_support(
748 self.service.capabilities().tray_or_status_item,
749 ApplicationShellFeature::TrayOrStatusItem,
750 )?;
751 let menu = PlatformMenuCommandModel::compile(menus).map_err(menu_build_error)?;
752 let mut state = self.service.state.borrow_mut();
753 ensure_running(&state)?;
754 let record = tray_record_mut(&mut state.trays, self.id)
755 .ok_or(ApplicationShellError::StaleResource)?;
756 if record.phase == ResourcePhase::Failed {
757 return Err(ApplicationShellError::StaleResource);
758 }
759 record.presentation = presentation.clone();
760 record.menu = menu.clone();
761 enqueue(
762 &mut state,
763 NativeApplicationShellOperation::UpdateTray {
764 id: self.id,
765 presentation,
766 menu: menu.snapshot().clone(),
767 },
768 );
769 drop(state);
770 self.service.wake();
771 Ok(())
772 }
773
774 pub fn on_activated(
775 &self,
776 callback: impl Fn() + Send + Sync + 'static,
777 ) -> Result<(), ApplicationShellError> {
778 let mut state = self.service.state.borrow_mut();
779 ensure_running(&state)?;
780 let record = tray_record_mut(&mut state.trays, self.id)
781 .ok_or(ApplicationShellError::StaleResource)?;
782 record.on_activated = Some(Arc::new(callback));
783 Ok(())
784 }
785
786 pub fn remove(mut self) -> Result<(), ApplicationShellError> {
787 self.service.remove_tray(self.id)?;
788 self.released = true;
789 Ok(())
790 }
791}
792
793impl Drop for TrayItemHandle {
794 fn drop(&mut self) {
795 if !self.released {
796 let _ = self.service.remove_tray(self.id);
797 }
798 }
799}
800
801impl ApplicationShellService {
802 fn remove_tray(&self, id: TrayItemId) -> Result<(), ApplicationShellError> {
803 let mut state = self.state.borrow_mut();
804 ensure_running(&state)?;
805 let slot = state
806 .trays
807 .get_mut(id.index() as usize)
808 .filter(|slot| slot.generation == id.generation())
809 .ok_or(ApplicationShellError::StaleResource)?;
810 let record = slot
811 .value
812 .take()
813 .ok_or(ApplicationShellError::StaleResource)?;
814 if let Some(next_generation) = slot.generation.checked_add(1) {
815 slot.generation = next_generation;
816 } else {
817 slot.retired = true;
818 }
819 retire_resource(&mut state, ShellResource::Tray(id), record.phase, true);
820 drop(state);
821 self.wake();
822 Ok(())
823 }
824}
825
826pub struct NotificationHandle {
827 id: NotificationId,
828 service: ApplicationShellService,
829 released: bool,
830}
831
832impl NotificationHandle {
833 #[must_use]
834 pub const fn id(&self) -> NotificationId {
835 self.id
836 }
837
838 pub fn on_activated(
839 &self,
840 callback: impl Fn() + Send + Sync + 'static,
841 ) -> Result<(), ApplicationShellError> {
842 self.with_record(|record| record.on_activated = Some(Arc::new(callback)))
843 }
844
845 pub fn on_action(
846 &self,
847 callback: impl Fn(NotificationActionId) + Send + Sync + 'static,
848 ) -> Result<(), ApplicationShellError> {
849 self.with_record(|record| record.on_action = Some(Arc::new(callback)))
850 }
851
852 pub fn on_dismissed(
853 &self,
854 callback: impl Fn() + Send + Sync + 'static,
855 ) -> Result<(), ApplicationShellError> {
856 self.with_record(|record| record.on_dismissed = Some(Arc::new(callback)))
857 }
858
859 pub fn update(
860 &self,
861 presentation: NotificationPresentation,
862 ) -> Result<(), ApplicationShellError> {
863 self.service.require_support(
864 self.service.capabilities().notification_update,
865 ApplicationShellFeature::NotificationUpdate,
866 )?;
867 if !presentation.actions.is_empty() {
868 self.service.require_support(
869 self.service.capabilities().notification_actions,
870 ApplicationShellFeature::NotificationActions,
871 )?;
872 }
873 let mut state = self.service.state.borrow_mut();
874 ensure_running(&state)?;
875 let record = notification_record_mut(&mut state.notifications, self.id)
876 .ok_or(ApplicationShellError::StaleResource)?;
877 if record.phase == ResourcePhase::Failed {
878 return Err(ApplicationShellError::StaleResource);
879 }
880 record.presentation = presentation.clone();
881 enqueue(
882 &mut state,
883 NativeApplicationShellOperation::UpdateNotification {
884 id: self.id,
885 presentation,
886 },
887 );
888 drop(state);
889 self.service.wake();
890 Ok(())
891 }
892
893 fn with_record(
894 &self,
895 update: impl FnOnce(&mut NotificationRecord),
896 ) -> Result<(), ApplicationShellError> {
897 let mut state = self.service.state.borrow_mut();
898 ensure_running(&state)?;
899 let record = notification_record_mut(&mut state.notifications, self.id)
900 .ok_or(ApplicationShellError::StaleResource)?;
901 update(record);
902 Ok(())
903 }
904
905 pub fn close(mut self) -> Result<(), ApplicationShellError> {
906 self.service.require_support(
907 self.service.capabilities().notification_dismiss,
908 ApplicationShellFeature::NotificationDismiss,
909 )?;
910 self.service.release_notification_handle(self.id, true)?;
911 self.released = true;
912 Ok(())
913 }
914}
915
916impl Drop for NotificationHandle {
917 fn drop(&mut self) {
918 if self.released {
919 return;
920 }
921 let dismiss_native =
928 self.service.capabilities().notification_dismiss != CapabilitySupport::Unsupported;
929 let _ = self
930 .service
931 .release_notification_handle(self.id, dismiss_native);
932 }
933}
934
935impl ApplicationShellService {
936 fn release_notification_handle(
937 &self,
938 id: NotificationId,
939 dismiss_native: bool,
940 ) -> Result<(), ApplicationShellError> {
941 let mut state = self.state.borrow_mut();
942 ensure_running(&state)?;
943 let phase = notification_record(&state.notifications, id)
944 .ok_or(ApplicationShellError::StaleResource)?
945 .phase;
946 release_notification(&mut state.notifications, id);
947 retire_resource(
948 &mut state,
949 ShellResource::Notification(id),
950 phase,
951 dismiss_native,
952 );
953 drop(state);
954 if dismiss_native {
955 self.wake();
956 }
957 Ok(())
958 }
959}
960
961#[derive(Clone, Default)]
962pub struct MemoryApplicationShellAdapter {
963 operations: Arc<Mutex<Vec<NativeApplicationShellOperation>>>,
964}
965
966impl MemoryApplicationShellAdapter {
967 pub fn apply(
968 &self,
969 request: NativeApplicationShellRequest,
970 ) -> NativeApplicationShellCompletion {
971 self.operations
972 .lock()
973 .expect("memory shell adapter lock")
974 .push(request.operation);
975 NativeApplicationShellCompletion {
976 request_id: request.request_id,
977 result: Ok(()),
978 }
979 }
980
981 #[must_use]
982 pub fn operations(&self) -> Vec<NativeApplicationShellOperation> {
983 self.operations
984 .lock()
985 .expect("memory shell adapter lock")
986 .clone()
987 }
988}