corundum_framework_gui/settings/
launcher.rs1use corundum_framework_platform::launcher::{self, LauncherSpec, Status};
4
5use crate::tr;
6
7pub struct LauncherRow {
20 spec: LauncherSpec,
21 status_of: fn(&LauncherSpec) -> Status,
23 status: Option<Status>,
25 error: Option<String>,
27 #[cfg(test)]
29 shown: Option<&'static str>,
30}
31
32#[derive(Clone, Copy)]
34enum Act {
35 Remove,
36 Add,
37}
38
39impl LauncherRow {
40 pub fn new(spec: LauncherSpec) -> LauncherRow {
42 LauncherRow::with_status(spec, launcher::status)
43 }
44
45 pub(crate) fn with_status(
48 spec: LauncherSpec,
49 status_of: fn(&LauncherSpec) -> Status,
50 ) -> LauncherRow {
51 LauncherRow {
52 spec,
53 status_of,
54 status: None,
55 error: None,
56 #[cfg(test)]
57 shown: None,
58 }
59 }
60
61 pub fn ui(&mut self, ui: &mut egui::Ui) {
63 let status = *self
64 .status
65 .get_or_insert_with(|| (self.status_of)(&self.spec));
66 let act = match status {
67 Status::Registered | Status::Stale => Some((Act::Remove, "corundum-launcher-remove")),
68 Status::Absent => Some((Act::Add, "corundum-launcher-add")),
69 Status::Unsupported => None,
70 };
71 #[cfg(test)]
72 {
73 self.shown = act.map(|(_, id)| id);
74 }
75 let mut clicked = None;
76 ui.horizontal(|ui| {
77 ui.label(tr!(ui, "corundum-launcher-in-menu"));
78 if let Some((act, id)) = act
79 && ui.button(tr!(ui, id)).clicked()
80 {
81 clicked = Some(act);
82 }
83 });
84 if let Some(act) = clicked {
85 self.act(ui.ctx(), act);
86 }
87 if let Some(error) = &self.error {
88 ui.colored_label(
89 ui.visuals().error_fg_color,
90 tr!(ui, "corundum-launcher-failed", error = error.as_str()),
91 );
92 }
93 }
94
95 #[cfg_attr(not(feature = "serde"), allow(unused_variables))]
97 fn act(&mut self, ctx: &egui::Context, act: Act) {
98 let done = match act {
99 Act::Remove => launcher::unregister(&self.spec),
100 Act::Add => launcher::register(&self.spec).map(|_| ()),
101 };
102 match done {
103 Ok(()) => {
104 self.error = None;
105 #[cfg(feature = "serde")]
106 crate::store::update(ctx, |s| {
107 s.launcher = Some(matches!(act, Act::Add));
108 });
109 }
110 Err(err) => self.error = Some(err.to_string()),
111 }
112 self.status = None;
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::test_support::render;
120 use std::sync::atomic::{AtomicUsize, Ordering};
121
122 const SPEC: LauncherSpec = LauncherSpec {
123 id: "net.example.Test",
124 name: "Test",
125 description: "A test app",
126 category: "Utility",
127 icon_svg: b"<svg/>",
128 bundle_prefix: "net.example",
129 };
130
131 fn shown(status_of: fn(&LauncherSpec) -> Status) -> Option<&'static str> {
132 let mut row = LauncherRow::with_status(SPEC, status_of);
133 render([600.0, 400.0], |ui| row.ui(ui));
134 row.shown
135 }
136
137 #[test]
138 fn an_absent_entry_offers_add_back() {
139 assert_eq!(shown(|_| Status::Absent), Some("corundum-launcher-add"));
140 }
141
142 #[test]
143 fn a_registered_or_stale_entry_offers_remove() {
144 assert_eq!(
145 shown(|_| Status::Registered),
146 Some("corundum-launcher-remove")
147 );
148 assert_eq!(shown(|_| Status::Stale), Some("corundum-launcher-remove"));
149 }
150
151 #[test]
152 fn without_an_app_menu_there_is_no_button() {
153 assert_eq!(shown(|_| Status::Unsupported), None);
154 }
155
156 #[test]
157 fn the_status_is_looked_up_once() {
158 static CALLS: AtomicUsize = AtomicUsize::new(0);
159 let mut row = LauncherRow::with_status(SPEC, |_| {
160 CALLS.fetch_add(1, Ordering::Relaxed);
161 Status::Absent
162 });
163 render([600.0, 400.0], |ui| row.ui(ui));
164 render([600.0, 400.0], |ui| row.ui(ui));
165 assert_eq!(CALLS.load(Ordering::Relaxed), 1);
166 }
167}