diff --git a/locales/cleancore.pot b/locales/cleancore.pot index 7b54833..29fa1d4 100644 --- a/locales/cleancore.pot +++ b/locales/cleancore.pot @@ -5027,3 +5027,9 @@ msgstr "" msgid "No startup measurements for this entry" msgstr "" + +msgid "General" +msgstr "" + +msgid "Lists" +msgstr "" diff --git a/locales/de.po b/locales/de.po index 8b4cfe9..7f86e2f 100644 --- a/locales/de.po +++ b/locales/de.po @@ -5028,3 +5028,9 @@ msgstr "Geschätzt anhand der Programmgröße ({})" msgid "No startup measurements for this entry" msgstr "Keine Startmessungen für diesen Eintrag" + +msgid "General" +msgstr "Allgemein" + +msgid "Lists" +msgstr "Listen" diff --git a/locales/es.po b/locales/es.po index 31c0b11..cf9a8bd 100644 --- a/locales/es.po +++ b/locales/es.po @@ -5028,3 +5028,9 @@ msgstr "Estimado según el tamaño del programa ({})" msgid "No startup measurements for this entry" msgstr "No hay mediciones de inicio para esta entrada" + +msgid "General" +msgstr "General" + +msgid "Lists" +msgstr "Listas" diff --git a/locales/fr.po b/locales/fr.po index ef5547e..8219bd7 100644 --- a/locales/fr.po +++ b/locales/fr.po @@ -5028,3 +5028,9 @@ msgstr "Estimé d'après la taille du programme ({})" msgid "No startup measurements for this entry" msgstr "Aucune mesure de démarrage pour cette entrée" + +msgid "General" +msgstr "Général" + +msgid "Lists" +msgstr "Listes" diff --git a/locales/he.po b/locales/he.po index 8131d15..1a0eab2 100644 --- a/locales/he.po +++ b/locales/he.po @@ -5029,3 +5029,9 @@ msgstr "הערכה לפי גודל התוכנה ({})" msgid "No startup measurements for this entry" msgstr "אין מדידות הפעלה לרשומה זו" + +msgid "General" +msgstr "כללי" + +msgid "Lists" +msgstr "רשימות" diff --git a/locales/ru.po b/locales/ru.po index fe104ed..2af58af 100644 --- a/locales/ru.po +++ b/locales/ru.po @@ -5028,3 +5028,9 @@ msgstr "Оценка по размеру программы ({})" msgid "No startup measurements for this entry" msgstr "Нет измерений запуска для этой записи" + +msgid "General" +msgstr "Общие" + +msgid "Lists" +msgstr "Списки" diff --git a/locales/uk.po b/locales/uk.po index f8c6b7e..7da68ca 100644 --- a/locales/uk.po +++ b/locales/uk.po @@ -5028,3 +5028,9 @@ msgstr "Оцінка за розміром програми ({})" msgid "No startup measurements for this entry" msgstr "Немає вимірювань запуску для цього запису" + +msgid "General" +msgstr "Загальні" + +msgid "Lists" +msgstr "Списки" diff --git a/src/config.rs b/src/config.rs index 4fae543..eae8392 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,8 +67,6 @@ pub struct Settings { pub logs_db_url: Option, /// Autorun Manager: also list entries that belong to the operating system. pub autorun_show_system: bool, - /// Autorun Manager: show the full professional view (source, command, location). - pub autorun_details: bool, /// Junk Files Cleaner: file name patterns (`*.tmp`, `~*`, …) that are junk. pub junk_patterns: Vec, /// Junk Files Cleaner: where to look (empty: the home folder on Linux, all fixed drives on Windows). @@ -160,7 +158,6 @@ impl Default for Settings { logs_db_folder: None, logs_db_url: None, autorun_show_system: false, - autorun_details: false, junk_patterns: crate::junk::DEFAULT_PATTERNS.iter().map(|p| Toggle { value: p.to_string(), enabled: true }).collect(), junk_locations: Vec::new(), junk_exclude: Vec::new(), diff --git a/src/gui/autorun.rs b/src/gui/autorun.rs index d0fd631..2a7a774 100644 --- a/src/gui/autorun.rs +++ b/src/gui/autorun.rs @@ -37,6 +37,8 @@ pub struct AutorunView { add: Option, export_path: String, icons: super::appicons::IconCache, + /// The full view (source, command, location). Off each time the page opens: it starts simple. + details: bool, } impl AutorunView { @@ -52,11 +54,13 @@ impl AutorunView { add: None, export_path: super::export_dir().join("autorun.html").display().to_string(), icons: Default::default(), + details: false, } } pub fn refresh(&mut self) { self.loaded = false; + self.details = false; } fn load(&mut self, ctx: &egui::Context, cx: &Cx) { @@ -103,7 +107,7 @@ impl AutorunView { } self.icons.poll(ui.ctx()); let p = Palette::get(ui.ctx()); - let details = cx.settings.autorun_details; + let details = self.details; let enabled = self.list.iter().filter(|e| e.enabled).count(); let heavy = self.list.iter().filter(|e| e.enabled && e.startup.impact == Impact::High).count(); let subtitle = if self.loading.is_some() { @@ -163,23 +167,8 @@ impl AutorunView { } crate::gui::style::row(ui, |ui| { style::search_field(ui, &mut self.search, if details { "Search name, command or publisher" } else { "Search name or publisher" }, 250.0); - if details { - ui.add_space(6.0); - if style::segment(ui, self.source.is_none(), "All").clicked() { - self.source = None; - } - for s in sources { - let n = self.list.iter().filter(|e| e.source == s).count(); - if style::segment(ui, self.source == Some(s), &crate::tf!("{} {n}", tr(s.label()))).clicked() { - self.source = Some(s); - } - } - } ui.with_layout(crate::gui::style::far_layout(egui::Align::Center), |ui| { - let r = style::toggle(ui, &mut cx.settings.autorun_details, "Details"); - if r.on_hover_text(style::vis(tr("Show where each entry comes from, its command and more actions"))).changed() { - cx.save_settings(); - } + style::toggle(ui, &mut self.details, "Details").on_hover_text(style::vis(tr("Show where each entry comes from, its command and more actions"))); ui.add(style::Label::new(style::muted(ui, tr("Details")))); if details { ui.add_space(12.0); @@ -195,6 +184,21 @@ impl AutorunView { } }); }); + // Details: the sources on a line of their own, so they never run into the switches. + if details { + ui.add_space(6.0); + crate::gui::style::row_wrapped(ui, |ui| { + if style::segment(ui, self.source.is_none(), "All").clicked() { + self.source = None; + } + for s in sources { + let n = self.list.iter().filter(|e| e.source == s).count(); + if style::segment(ui, self.source == Some(s), &crate::tf!("{} {n}", tr(s.label()))).clicked() { + self.source = Some(s); + } + } + }); + } ui.add_space(8.0); if let Some((l, _)) = &self.loading { @@ -407,14 +411,7 @@ impl AutorunView { Box::new(|ui: &mut Ui| { ui.add(style::Label::new(style::muted(ui, tr(e.source.label()))).truncate()).on_hover_text(style::vis(tr(e.source.label()))); }), - Box::new(|ui: &mut Ui| { - crate::gui::style::column(ui, |ui| { - ui.spacing_mut().item_spacing.y = 2.0; - ui.spacing_mut().interact_size.y = 18.0; - impact_badge(ui, e, p); - ui.add(style::Label::new(style::muted(ui, impact_detail(e)).size(11.5)).truncate()); - }); - }), + Box::new(|ui: &mut Ui| impact_badge(ui, e, p)), Box::new(|ui: &mut Ui| { ui.add(style::Label::new(style::muted(ui, e.publisher.as_deref().unwrap_or(""))).truncate()); }), diff --git a/src/gui/mod.rs b/src/gui/mod.rs index b74872a..6588fc0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -255,6 +255,8 @@ pub struct App { daily: Option>>>, updates: updates::Updates, options: options::SettingsView, + /// The Settings dialog is open over the page. + settings_open: bool, /// The page drawn last frame and when it appeared, for the page fade-in. shown: Option, page_since: f64, @@ -276,6 +278,7 @@ impl App { updates.startup(&cc.egui_ctx, &settings); Self { options: options::SettingsView::new(&settings), + settings_open: false, settings, // `cleancore --hunter` (or /hunter) opens Hunter Mode. tool: if Tool::Hunter.supported() && std::env::args().skip(1).any(|a| a == "--hunter" || a.eq_ignore_ascii_case("/hunter")) { Tool::Hunter } else { Tool::Uninstaller }, @@ -380,13 +383,14 @@ impl App { }); ui.add_space(4.0); // Settings: a full-width entry like the ones above, so it lines up with them. - let r = style::nav_item(ui, Tool::Settings.icon(), Tool::Settings.label(), self.tool == Tool::Settings); - if self.tool == Tool::Settings { + let r = style::nav_item(ui, Tool::Settings.icon(), Tool::Settings.label(), self.settings_open); + if self.settings_open { selected_rect = Some(r.rect); selected_clip = ui.clip_rect(); } if r.clicked() { - self.switch(Tool::Settings); + self.options = options::SettingsView::new(&self.settings); + self.settings_open = true; } ui.add_space(8.0); }); @@ -402,8 +406,8 @@ impl App { }); ui.add_space(4.0); for t in tools { - let r = style::nav_item(ui, t.icon(), t.label(), self.tool == t); - if self.tool == t { + let r = style::nav_item(ui, t.icon(), t.label(), self.tool == t && !self.settings_open); + if self.tool == t && !self.settings_open { selected_rect = Some(r.rect); selected_clip = ui.clip_rect(); } @@ -527,7 +531,7 @@ impl eframe::App for App { Tool::Watcher => self.watcher.main(ui, &mut cx), Tool::Backups => self.backups.main(ui, &mut cx), Tool::Tools => self.tools.main(ui, &mut cx), - Tool::Settings => self.options.main(ui, &mut cx, &mut self.updates), + Tool::Settings => {} } self.updates.poll(&mut cx); }); @@ -542,6 +546,10 @@ impl eframe::App for App { self.tool = Tool::Hunter; } } + if self.settings_open { + let mut cx = Cx { settings: &mut self.settings, notices: &mut self.notices }; + self.settings_open = self.options.dialog(&ctx, &mut cx, &mut self.updates); + } self.toasts(&ctx); if self.settings.gui_theme.is_none() { first_run(&ctx, &mut self.settings); diff --git a/src/gui/options.rs b/src/gui/options.rs index 209a815..e0097f8 100644 --- a/src/gui/options.rs +++ b/src/gui/options.rs @@ -5,7 +5,7 @@ use super::{apply_theme, Cx}; use crate::config::{ListEntry, Settings}; use crate::i18n::tr; use crate::model::ScanMode; -use eframe::egui::{self, Ui, Vec2}; +use eframe::egui::{self, Color32, Ui, Vec2}; pub struct SettingsView { draft: Settings, @@ -20,6 +20,8 @@ pub struct SettingsView { db_url: String, /// The settings the form was last in step with, to notice changes made on other pages. synced: String, + /// The dialog was just opened: the click that opened it must not close it again. + fresh: bool, } impl SettingsView { @@ -36,6 +38,7 @@ impl SettingsView { db_folder: crate::tracer::db_folder(s).display().to_string(), db_url: s.logs_db_url.clone().unwrap_or_default(), synced: serde_json::to_string(s).unwrap_or_default(), + fresh: true, } } @@ -52,7 +55,9 @@ impl SettingsView { d } - pub fn main(&mut self, ui: &mut Ui, cx: &mut Cx, updates: &mut super::updates::Updates) { + /// The Settings dialog over the app. Returns false once it is closed (the close button, + /// Escape or a click outside it). + pub fn dialog(&mut self, ctx: &egui::Context, cx: &mut Cx, updates: &mut super::updates::Updates) -> bool { // Settings changed elsewhere (another page, a newer copy) are taken in first, so the form // never writes back stale values. if serde_json::to_string(&*cx.settings).unwrap_or_default() != self.synced { @@ -60,58 +65,125 @@ impl SettingsView { *self = SettingsView { new_exclude, new_include, ..SettingsView::new(cx.settings) }; } let before = serde_json::to_string(&self.finished()).ok(); - style::page_header(ui, "Settings", "Appearance, safety options, lists and storage", |_| {}); - - egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { - self.appearance(ui, cx); - ui.add_space(14.0); - self.uninstaller(ui, cx); - ui.add_space(14.0); - self.cleaner(ui); - ui.add_space(14.0); - style::card(ui, |ui| { - list_editor(ui, &mut self.draft.exclude_list, &mut self.new_exclude, "Exclude list", "Never reported or deleted. Subkeys / subfolders can be included."); + let p = Palette::get(ctx); + let screen = ctx.content_rect(); + let size = Vec2::new((screen.width() - 64.0).clamp(520.0, 980.0), (screen.height() - 150.0).clamp(320.0, 620.0)); + let mut close = false; + let frame = egui::Frame::new().fill(p.surface).stroke(egui::Stroke::new(1.0, p.border)).corner_radius(14); + let shown = egui::Modal::new(egui::Id::unique("settings-dialog")).frame(frame).show(ctx, |ui| { + ui.set_width(size.x); + ui.spacing_mut().item_spacing.y = 0.0; + // Title bar: the name in the middle, the close button at the far end. + egui::Frame::new().fill(p.bg).corner_radius(egui::CornerRadius { nw: 14, ne: 14, sw: 0, se: 0 }).show(ui, |ui| { + let (rect, _) = ui.allocate_exact_size(Vec2::new(size.x, 56.0), egui::Sense::hover()); + let title = crate::tf!("{} {}", icon::GEAR, tr("Settings")); + ui.painter().text(rect.center(), egui::Align2::CENTER_CENTER, style::vstr(title), egui::FontId::new(19.0, style::semibold()), p.text); + let m = style::Mirror::new(rect); + let x = m.rect(size.x - 52.0, rect.center().y - 16.0, Vec2::splat(32.0)); + if ui.put(x, |ui: &mut Ui| style::icon_button(ui, icon::X, "Close")).clicked() { + close = true; + } }); - ui.add_space(14.0); - style::card(ui, |ui| { - list_editor( - ui, - &mut self.draft.include_list, - &mut self.new_include, - "Include list", - "Contents are always deleted by a Custom Scan with the \"Include list\" category. Be careful.", - ); + ui.add(egui::Separator::default().spacing(0.0)); + egui::Frame::new().inner_margin(egui::Margin::same(16)).show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 8.0; + ui.set_height(size.y); + self.body(ui, cx, updates, p); }); - ui.add_space(14.0); - self.backups(ui); - ui.add_space(14.0); - updates.card(ui, cx, &mut self.draft.check_updates); - ui.add_space(14.0); - style::card(ui, |ui| { - style::section_title(ui, "Watcher and notifications"); - style::setting_row(ui, "Maximum log folder size", "When the logs reach this size, all watches pause. 0 means no limit.", |ui| { - ui.add(egui::DragValue::new(&mut self.draft.watcher_max_log_mb).range(0..=100_000).suffix(" MB")); - }); - style::divider(ui); - style::setting_row(ui, "Notify after Auto Clean", "Failures always notify.", |ui| { - style::toggle(ui, &mut self.draft.notify_auto_clean, "Notify after Auto Clean"); - }); - style::setting_row(ui, "Notify after Auto Backup", "", |ui| { - style::toggle(ui, &mut self.draft.notify_auto_backup, "Notify after Auto Backup"); - }); - style::setting_row(ui, "Skip confirmation dialogs", "Clean without asking. Backups are still made.", |ui| { - style::toggle(ui, &mut self.draft.skip_warnings, "Skip confirmation dialogs"); - }); - }); - ui.add_space(10.0); - ui.add(style::Label::new(style::muted(ui, crate::tf!("Data folder: {}", crate::config::data_dir().display())).size(12.0))); }); + if shown.should_close() && !self.fresh { + close = true; + } + self.fresh = false; let after = self.finished(); if serde_json::to_string(&after).ok() != before { *cx.settings = after; cx.save_settings(); } self.synced = serde_json::to_string(&*cx.settings).unwrap_or_default(); + !close + } + + /// The category list on the side, the chosen category's settings next to it. The choice + /// outlives the dialog, which is rebuilt each time it is opened. + fn body(&mut self, ui: &mut Ui, cx: &mut Cx, updates: &mut super::updates::Updates, p: Palette) { + let id = egui::Id::unique("settings-section"); + let mut section = ui.ctx().data(|d| d.get_temp::
(id)).unwrap_or(Section::General); + let side = if ui.available_width() < 700.0 { 168.0 } else { 210.0 }; + let page_h = ui.available_height(); + crate::gui::style::row_top(ui, |ui| { + ui.set_min_height(page_h); + crate::gui::style::column(ui, |ui| { + ui.set_width(side); + ui.spacing_mut().item_spacing.y = 4.0; + for s in Section::ALL { + if category(ui, s, s == section, p).clicked() { + section = s; + } + } + }); + ui.add(egui::Separator::default().vertical().spacing(20.0)); + egui::ScrollArea::vertical().id_salt(section.title()).auto_shrink([false, false]).min_scrolled_height(page_h).max_height(page_h).show(ui, |ui| { + crate::gui::style::column(ui, |ui| self.section(ui, cx, updates, section)); + }); + }); + ui.ctx().data_mut(|d| d.insert_temp(id, section)); + } + + /// One category's heading and settings. + fn section(&mut self, ui: &mut Ui, cx: &mut Cx, updates: &mut super::updates::Updates, section: Section) { + ui.set_width(ui.available_width()); + ui.add(style::Label::new(style::strong(tr(section.title())).size(20.0))); + ui.add_space(10.0); + match section { + Section::General => self.general(ui, cx), + Section::Uninstaller => self.uninstaller(ui, cx), + Section::Cleaner => self.cleaner(ui), + Section::Lists => self.lists(ui), + Section::Backups => self.backups(ui), + Section::Updates => updates.card(ui, cx, &mut self.draft.check_updates), + } + } + + /// Appearance, notifications and where CleanCore keeps its data. + fn general(&mut self, ui: &mut Ui, cx: &mut Cx) { + self.appearance(ui, cx); + ui.add_space(14.0); + style::card(ui, |ui| { + style::section_title(ui, "Watcher and notifications"); + style::setting_row(ui, "Maximum log folder size", "When the logs reach this size, all watches pause. 0 means no limit.", |ui| { + ui.add(egui::DragValue::new(&mut self.draft.watcher_max_log_mb).range(0..=100_000).suffix(" MB")); + }); + style::divider(ui); + style::setting_row(ui, "Notify after Auto Clean", "Failures always notify.", |ui| { + style::toggle(ui, &mut self.draft.notify_auto_clean, "Notify after Auto Clean"); + }); + style::setting_row(ui, "Notify after Auto Backup", "", |ui| { + style::toggle(ui, &mut self.draft.notify_auto_backup, "Notify after Auto Backup"); + }); + style::setting_row(ui, "Skip confirmation dialogs", "Clean without asking. Backups are still made.", |ui| { + style::toggle(ui, &mut self.draft.skip_warnings, "Skip confirmation dialogs"); + }); + }); + ui.add_space(10.0); + ui.add(style::Label::new(style::muted(ui, crate::tf!("Data folder: {}", crate::config::data_dir().display())).size(12.0))); + } + + /// The exclude and include lists. + fn lists(&mut self, ui: &mut Ui) { + style::card(ui, |ui| { + list_editor(ui, &mut self.draft.exclude_list, &mut self.new_exclude, "Exclude list", "Never reported or deleted. Subkeys / subfolders can be included."); + }); + ui.add_space(14.0); + style::card(ui, |ui| { + list_editor( + ui, + &mut self.draft.include_list, + &mut self.new_include, + "Include list", + "Contents are always deleted by a Custom Scan with the \"Include list\" category. Be careful.", + ); + }); } fn appearance(&mut self, ui: &mut Ui, cx: &mut Cx) { @@ -191,7 +263,6 @@ impl SettingsView { fn uninstaller(&mut self, ui: &mut Ui, cx: &mut Cx) { let d = &mut self.draft; style::card(ui, |ui| { - style::section_title(ui, "Uninstaller"); style::setting_row( ui, if cfg!(windows) { tr("Add to Explorer's right-click menu") } else { tr("Add to the file manager's right-click menu") }, @@ -275,7 +346,6 @@ impl SettingsView { fn cleaner(&mut self, ui: &mut Ui) { let d = &mut self.draft; style::card(ui, |ui| { - style::section_title(ui, "Cleaner"); style::setting_row(ui, "Skip paths on external drives", "Items on drives that are not plugged in are never reported.", |ui| { style::toggle(ui, &mut d.exclude_external_drives, "Skip paths on external drives"); }); @@ -317,7 +387,6 @@ impl SettingsView { fn backups(&mut self, ui: &mut Ui) { style::card(ui, |ui| { - style::section_title(ui, "Backups"); ui.add(style::Label::new(style::muted(ui, tr("Backup folder")))); style::path_input(ui, "backup-dir", &mut self.backup_dir, "", ui.available_width(), style::Pick::Folder); ui.add_space(6.0); @@ -392,3 +461,66 @@ fn list_editor(ui: &mut Ui, list: &mut Vec, new: &mut String, title: fn tf_icon(icon: &str, text: &str) -> String { format!("{icon} {text}") } + +/// A group of settings in the side list. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Section { + General, + Uninstaller, + Cleaner, + Lists, + Backups, + Updates, +} + +impl Section { + const ALL: [Section; 6] = [Self::General, Self::Uninstaller, Self::Cleaner, Self::Lists, Self::Backups, Self::Updates]; + + fn title(self) -> &'static str { + match self { + Self::General => "General", + Self::Uninstaller => "Uninstaller", + Self::Cleaner => "Cleaner", + Self::Lists => "Lists", + Self::Backups => "Backups", + Self::Updates => "Updates", + } + } + + fn icon(self) -> (&'static str, Color32) { + match self { + Self::General => (icon::SLIDERS, Color32::from_rgb(37, 99, 235)), + Self::Uninstaller => (icon::TRASH, Color32::from_rgb(225, 29, 72)), + Self::Cleaner => (icon::BROOM, Color32::from_rgb(22, 163, 74)), + Self::Lists => (icon::FUNNEL, Color32::from_rgb(217, 119, 6)), + Self::Backups => (icon::ARCHIVE, Color32::from_rgb(124, 58, 237)), + Self::Updates => (icon::DOWNLOAD, Color32::from_rgb(13, 148, 136)), + } + } +} + +/// A side-list entry: a coloured icon tile and the name, highlighted when chosen. +fn category(ui: &mut Ui, s: Section, selected: bool, p: Palette) -> egui::Response { + let label = tr(s.title()); + let (glyph, color) = s.icon(); + let (rect, resp) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 46.0), egui::Sense::click()); + // Tabs, so assistive tools (and the translation audit) see them as one group. + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Tab, true, selected, label)); + if selected { + ui.painter().rect_filled(rect, 10.0, p.accent_soft); + } else if resp.hovered() { + ui.painter().rect_filled(rect, 10.0, p.hover); + } + let m = style::Mirror::new(rect); + let tile = m.rect(10.0, rect.center().y - 15.0, Vec2::splat(30.0)); + let fill = if selected { color } else { Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), if p.dark { 52 } else { 32 }) }; + ui.painter().rect_filled(tile, 8.0, fill); + let fg = if selected { Color32::WHITE } else { color }; + ui.painter().text(tile.center(), egui::Align2::CENTER_CENTER, style::vstr(glyph), egui::FontId::new(16.0, style::icon_fill()), fg); + let font = egui::FontId::new(15.0, if selected { style::semibold() } else { egui::FontFamily::Proportional }); + ui.painter().text(m.pos(52.0, rect.center().y), m.start(egui::Align::Center), style::vstr(label), font, if selected { p.text } else { p.muted.lerp_to_gamma(p.text, 0.5) }); + if resp.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + } + resp +} diff --git a/src/gui/updates.rs b/src/gui/updates.rs index f5a7827..ccda64c 100644 --- a/src/gui/updates.rs +++ b/src/gui/updates.rs @@ -120,7 +120,6 @@ impl Updates { pub fn card(&mut self, ui: &mut Ui, cx: &mut Cx, check_updates: &mut bool) { let p = Palette::get(ui.ctx()); style::card(ui, |ui| { - style::section_title(ui, "Updates"); crate::gui::style::row(ui, |ui| { crate::gui::style::column(ui, |ui| { ui.add(style::Label::new(crate::tf!("CleanCore {}", update::CURRENT))); diff --git a/tests/gui.rs b/tests/gui.rs index 05eba57..b3a4988 100644 --- a/tests/gui.rs +++ b/tests/gui.rs @@ -457,10 +457,18 @@ fn walk_through_every_tool() { shot(&mut h, "11-settings"); h.get_by_label("Violet").click(); h.run_steps(3); + // Settings is a dialog: close it to reach the sidebar again. + h.query_all_by_role_and_label(Role::Button, "Close").next().unwrap().click(); + h.run_steps(3); h.get_by_label("Switch to dark").click(); h.run_steps(3); + h.query_all_by_role_and_label(Role::Button, "Settings").next().unwrap().click(); + h.run_steps(3); shot(&mut h, "12-settings-dark-violet"); h.get_by_label("Indigo").click(); + h.run_steps(3); + h.query_all_by_role_and_label(Role::Button, "Close").next().unwrap().click(); + h.run_steps(3); h.get_by_role_and_label(Role::Button, "Cleaner").click(); h.run_steps(3); shot(&mut h, "13-dark-cleaner");