corundum_framework_gui/settings/
mod.rs1mod appearance;
24#[cfg(all(
25 feature = "package",
26 not(any(target_os = "ios", target_os = "android"))
27))]
28mod cli;
29mod launcher;
30mod layout;
31mod rail;
32mod scope;
33#[cfg(all(
34 feature = "service",
35 not(any(target_os = "ios", target_os = "android"))
36))]
37mod service;
38
39pub use appearance::{AppearanceChange, AppearanceCtx, AppearancePage, AppearanceScope};
40#[cfg(all(
41 feature = "package",
42 not(any(target_os = "ios", target_os = "android"))
43))]
44pub use cli::CliRow;
45pub use launcher::LauncherRow;
46pub use layout::{ListItem, SettingsLayout};
47pub use rail::RailPage;
48pub use scope::{ScopeTab, scope_tabs};
49#[cfg(all(
50 feature = "service",
51 not(any(target_os = "ios", target_os = "android"))
52))]
53pub use service::ServiceRow;
54
55pub trait SettingsSection {
57 fn label(&self, ctx: &egui::Context) -> String;
60
61 fn ui(&mut self, ui: &mut egui::Ui);
63
64 fn dirty(&self) -> bool {
66 false
67 }
68}
69
70enum Heading {
72 None,
74 Message(&'static str),
76 Text(String),
78}
79
80pub struct SettingsGroup {
82 heading: Heading,
83 sections: Vec<Box<dyn SettingsSection>>,
84}
85
86impl SettingsGroup {
87 pub fn new(sections: Vec<Box<dyn SettingsSection>>) -> SettingsGroup {
89 SettingsGroup {
90 heading: Heading::None,
91 sections,
92 }
93 }
94
95 pub fn titled(mut self, id: &'static str) -> SettingsGroup {
97 self.heading = Heading::Message(id);
98 self
99 }
100
101 pub fn named(mut self, name: impl Into<String>) -> SettingsGroup {
103 self.heading = Heading::Text(name.into());
104 self
105 }
106}
107
108#[derive(Default)]
121pub struct Settings {
122 groups: Vec<SettingsGroup>,
123 layout: SettingsLayout<(usize, usize)>,
125}
126
127impl Settings {
128 pub fn new() -> Settings {
130 Settings::default()
131 }
132
133 pub fn with_group(mut self, group: SettingsGroup) -> Settings {
135 self.groups.push(group);
136 self
137 }
138
139 pub fn push_group(&mut self, group: SettingsGroup) {
142 self.groups.push(group);
143 }
144
145 pub fn open(&mut self, group: usize, section: usize) {
147 self.layout.open(Some((group, section)));
148 }
149
150 pub fn current(&self) -> Option<(usize, usize)> {
152 self.layout.current().copied()
153 }
154
155 pub fn ui(&mut self, ui: &mut egui::Ui) {
157 let items = self.items(ui.ctx());
158 let Settings { groups, layout } = self;
159 layout.ui(ui, &items, |ui, (g, s)| {
160 if let Some(section) = groups.get_mut(g).and_then(|g| g.sections.get_mut(s)) {
161 section.ui(ui);
162 }
163 });
164 }
165
166 fn items(&self, ctx: &egui::Context) -> Vec<ListItem<(usize, usize)>> {
168 let mut out = Vec::new();
169 for (g, group) in self.groups.iter().enumerate() {
170 match &group.heading {
171 Heading::None if g > 0 => out.push(ListItem::Gap),
172 Heading::None => {}
173 Heading::Message(id) => out.push(ListItem::Heading(crate::i18n::tr(ctx, id))),
174 Heading::Text(text) => out.push(ListItem::Heading(text.clone())),
175 }
176 for (s, section) in group.sections.iter().enumerate() {
177 out.push(ListItem::Entry {
178 page: (g, s),
179 label: section.label(ctx),
180 dirty: section.dirty(),
181 });
182 }
183 }
184 out
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::test_support::render;
192
193 struct Named(&'static str, bool);
194
195 impl SettingsSection for Named {
196 fn label(&self, _ctx: &egui::Context) -> String {
197 self.0.to_owned()
198 }
199
200 fn ui(&mut self, ui: &mut egui::Ui) {
201 ui.label(self.0);
202 }
203
204 fn dirty(&self) -> bool {
205 self.1
206 }
207 }
208
209 fn settings() -> Settings {
210 Settings::new()
211 .with_group(
212 SettingsGroup::new(vec![
213 Box::new(Named("Look", false)),
214 Box::new(Named("Sync", true)),
215 ])
216 .named("Example"),
217 )
218 .with_group(
219 SettingsGroup::new(vec![Box::new(Named("Appearance", false))])
220 .titled("corundum-settings"),
221 )
222 .with_group(SettingsGroup::new(vec![Box::new(Named("About", false))]))
223 }
224
225 fn labels(settings: &Settings) -> Vec<String> {
226 let ctx = egui::Context::default();
227 crate::i18n::install(
228 &ctx,
229 &crate::i18n::LanguageSettings {
230 language: crate::i18n::Language::Explicit("en".into()),
231 },
232 );
233 settings
234 .items(&ctx)
235 .into_iter()
236 .map(|item| match item {
237 ListItem::Heading(h) => format!("# {h}"),
238 ListItem::Entry { label, dirty, .. } => layout::marked(&label, dirty),
239 ListItem::Gap => "-".into(),
240 })
241 .collect()
242 }
243
244 #[test]
245 fn groups_are_listed_in_order() {
246 let mut settings = settings();
247 settings
248 .push_group(SettingsGroup::new(vec![Box::new(Named("Later", false))]).named("Late"));
249 let labels = labels(&settings);
250 assert_eq!(
251 labels,
252 [
253 "# Example",
254 "Look",
255 "Sync •",
256 "# Settings",
257 "Appearance",
258 "-",
259 "About",
260 "# Late",
261 "Later",
262 ]
263 );
264 }
265
266 #[test]
267 fn wide_it_opens_the_first_page() {
268 let mut settings = settings();
269 render([900.0, 600.0], |ui| settings.ui(ui));
270 assert_eq!(settings.current(), Some((0, 0)));
271 }
272
273 #[test]
274 fn compact_it_shows_the_list_then_the_page_opened() {
275 let mut settings = settings();
276 render([400.0, 700.0], |ui| settings.ui(ui));
277 assert_eq!(settings.current(), None);
278 settings.open(2, 0);
279 render([400.0, 700.0], |ui| settings.ui(ui));
280 assert_eq!(settings.current(), Some((2, 0)));
281 }
282}