corundum_framework_gui/settings/
mod.rs1mod appearance;
21mod launcher;
22mod layout;
23mod rail;
24mod scope;
25
26pub use appearance::{AppearanceChange, AppearanceCtx, AppearancePage, AppearanceScope};
27pub use launcher::LauncherRow;
28pub use layout::{ListItem, SettingsLayout};
29pub use rail::RailPage;
30pub use scope::{ScopeTab, scope_tabs};
31
32pub trait SettingsSection {
34 fn label(&self, ctx: &egui::Context) -> String;
37
38 fn ui(&mut self, ui: &mut egui::Ui);
40
41 fn dirty(&self) -> bool {
43 false
44 }
45}
46
47enum Heading {
49 None,
51 Message(&'static str),
53 Text(String),
55}
56
57pub struct SettingsGroup {
59 heading: Heading,
60 sections: Vec<Box<dyn SettingsSection>>,
61}
62
63impl SettingsGroup {
64 pub fn new(sections: Vec<Box<dyn SettingsSection>>) -> SettingsGroup {
66 SettingsGroup {
67 heading: Heading::None,
68 sections,
69 }
70 }
71
72 pub fn titled(mut self, id: &'static str) -> SettingsGroup {
74 self.heading = Heading::Message(id);
75 self
76 }
77
78 pub fn named(mut self, name: impl Into<String>) -> SettingsGroup {
80 self.heading = Heading::Text(name.into());
81 self
82 }
83}
84
85#[derive(Default)]
98pub struct Settings {
99 groups: Vec<SettingsGroup>,
100 layout: SettingsLayout<(usize, usize)>,
102}
103
104impl Settings {
105 pub fn new() -> Settings {
107 Settings::default()
108 }
109
110 pub fn with_group(mut self, group: SettingsGroup) -> Settings {
112 self.groups.push(group);
113 self
114 }
115
116 pub fn push_group(&mut self, group: SettingsGroup) {
119 self.groups.push(group);
120 }
121
122 pub fn open(&mut self, group: usize, section: usize) {
124 self.layout.open(Some((group, section)));
125 }
126
127 pub fn current(&self) -> Option<(usize, usize)> {
129 self.layout.current().copied()
130 }
131
132 pub fn ui(&mut self, ui: &mut egui::Ui) {
134 let items = self.items(ui.ctx());
135 let Settings { groups, layout } = self;
136 layout.ui(ui, &items, |ui, (g, s)| {
137 if let Some(section) = groups.get_mut(g).and_then(|g| g.sections.get_mut(s)) {
138 section.ui(ui);
139 }
140 });
141 }
142
143 fn items(&self, ctx: &egui::Context) -> Vec<ListItem<(usize, usize)>> {
145 let mut out = Vec::new();
146 for (g, group) in self.groups.iter().enumerate() {
147 match &group.heading {
148 Heading::None if g > 0 => out.push(ListItem::Gap),
149 Heading::None => {}
150 Heading::Message(id) => out.push(ListItem::Heading(crate::i18n::tr(ctx, id))),
151 Heading::Text(text) => out.push(ListItem::Heading(text.clone())),
152 }
153 for (s, section) in group.sections.iter().enumerate() {
154 out.push(ListItem::Entry {
155 page: (g, s),
156 label: section.label(ctx),
157 dirty: section.dirty(),
158 });
159 }
160 }
161 out
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::test_support::render;
169
170 struct Named(&'static str, bool);
171
172 impl SettingsSection for Named {
173 fn label(&self, _ctx: &egui::Context) -> String {
174 self.0.to_owned()
175 }
176
177 fn ui(&mut self, ui: &mut egui::Ui) {
178 ui.label(self.0);
179 }
180
181 fn dirty(&self) -> bool {
182 self.1
183 }
184 }
185
186 fn settings() -> Settings {
187 Settings::new()
188 .with_group(
189 SettingsGroup::new(vec![
190 Box::new(Named("Look", false)),
191 Box::new(Named("Sync", true)),
192 ])
193 .named("Example"),
194 )
195 .with_group(
196 SettingsGroup::new(vec![Box::new(Named("Appearance", false))])
197 .titled("corundum-settings"),
198 )
199 .with_group(SettingsGroup::new(vec![Box::new(Named("About", false))]))
200 }
201
202 fn labels(settings: &Settings) -> Vec<String> {
203 let ctx = egui::Context::default();
204 crate::i18n::install(
205 &ctx,
206 &crate::i18n::LanguageSettings {
207 language: crate::i18n::Language::Explicit("en".into()),
208 },
209 );
210 settings
211 .items(&ctx)
212 .into_iter()
213 .map(|item| match item {
214 ListItem::Heading(h) => format!("# {h}"),
215 ListItem::Entry { label, dirty, .. } => layout::marked(&label, dirty),
216 ListItem::Gap => "-".into(),
217 })
218 .collect()
219 }
220
221 #[test]
222 fn groups_are_listed_in_order() {
223 let mut settings = settings();
224 settings
225 .push_group(SettingsGroup::new(vec![Box::new(Named("Later", false))]).named("Late"));
226 let labels = labels(&settings);
227 assert_eq!(
228 labels,
229 [
230 "# Example",
231 "Look",
232 "Sync •",
233 "# Settings",
234 "Appearance",
235 "-",
236 "About",
237 "# Late",
238 "Later",
239 ]
240 );
241 }
242
243 #[test]
244 fn wide_it_opens_the_first_page() {
245 let mut settings = settings();
246 render([900.0, 600.0], |ui| settings.ui(ui));
247 assert_eq!(settings.current(), Some((0, 0)));
248 }
249
250 #[test]
251 fn compact_it_shows_the_list_then_the_page_opened() {
252 let mut settings = settings();
253 render([400.0, 700.0], |ui| settings.ui(ui));
254 assert_eq!(settings.current(), None);
255 settings.open(2, 0);
256 render([400.0, 700.0], |ui| settings.ui(ui));
257 assert_eq!(settings.current(), Some((2, 0)));
258 }
259}