fix: discard unsaved theme edits when settings closes

This commit is contained in:
Tomás Romero committed 2026-10-21 02:09:50 +02:00
1 parent 9edbaee030
commit 3400f02acb
10 files changed
+2820 -2756

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+408 -408
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+45
View File
@@ -680,3 +680,48 @@ fn save_oauth_settings(
});
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hiding_settings_discards_an_unsaved_palette() {
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};
struct Headless(Rc<MinimalSoftwareWindow>);
impl slint::platform::Platform for Headless {
fn create_window_adapter(
&self,
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
Ok(self.0.clone())
}
}
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
slint::platform::set_platform(Box::new(Headless(window))).unwrap();
let app = AppWindow::new().unwrap();
let theme = app.global::<AppTheme>();
let draft = app.global::<ThemeDraft>();
let saved = theme.get_custom_light_primary();
app.set_settings_open(true);
slint::platform::update_timers_and_animations();
// The editor previews a color before it is saved.
draft.invoke_begin();
theme.set_custom_light_primary(slint::Color::from_rgb_u8(0xff, 0, 0));
app.set_settings_open(false);
slint::platform::update_timers_and_animations();
assert_eq!(theme.get_custom_light_primary(), saved);
assert!(!draft.get_dirty());
// A saved palette stays.
app.set_settings_open(true);
slint::platform::update_timers_and_animations();
draft.invoke_begin();
let chosen = slint::Color::from_rgb_u8(0, 0x80, 0);
theme.set_custom_light_primary(chosen);
draft.set_dirty(false);
app.set_settings_open(false);
slint::platform::update_timers_and_animations();
assert_eq!(theme.get_custom_light_primary(), chosen);
}
}
+8 -1
View File
@@ -34,6 +34,8 @@ import { ConfirmationDialog } from "components/confirmation-dialog.slint";
import { SettingsDialog } from "dialogs/settings-dialog.slint";
import { AccountImport, ImportDialog } from "dialogs/import-dialog.slint";
export { AccountImport, ImportAccountRow, ImportFailureRow, ImportFindingRow, ImportProfileRow } from "dialogs/import-dialog.slint";
export { ThemeDraft } from "components/theme-editor.slint";
import { ThemeDraft } from "components/theme-editor.slint";
import { MailView } from "views/mail-view.slint";
export { MessageColors } from "message-colors.slint";
export { FilesUi, FileRow, OperationRow } from "views/files-view.slint";
@@ -791,7 +793,12 @@ export component AppWindow inherits Window {
in-out property <length> email-scroll-document-height: 0px;
in-out property <length> email-scroll-y: 0px;
in-out property <bool> settings_open: false;
changed settings_open => { root.product-changed(root.active_view); }
changed settings_open => {
root.product-changed(root.active_view);
// Hiding Settings destroys the dialog before its own change handlers
// run, so an unsaved palette is discarded here.
if !root.settings_open { ThemeDraft.revert(); }
}
in-out property <string> active_view: "mail";
callback product-changed(string);
private property <string> mail-compact-page: "list";
+61 -49
View File
@@ -386,6 +386,57 @@ struct ThemeContrastCheck {
ratio: float,
}
// The custom palette as last saved while the editor previews unsaved
// changes live. It outlives the editor, which closing Settings or switching
// to another tab destroys, so the window can still discard the draft.
export global ThemeDraft {
in-out property <bool> dirty: false;
in-out property <color> light-primary;
in-out property <color> light-page-bg;
in-out property <color> light-surface;
in-out property <color> light-text;
in-out property <color> light-border;
in-out property <color> dark-primary;
in-out property <color> dark-page-bg;
in-out property <color> dark-surface;
in-out property <color> dark-text;
in-out property <color> dark-border;
// Remember the saved palette before the first unsaved change.
public function begin() {
if !root.dirty {
root.light-primary = AppTheme.custom-light-primary;
root.light-page-bg = AppTheme.custom-light-page-bg;
root.light-surface = AppTheme.custom-light-surface;
root.light-text = AppTheme.custom-light-text;
root.light-border = AppTheme.custom-light-border;
root.dark-primary = AppTheme.custom-dark-primary;
root.dark-page-bg = AppTheme.custom-dark-page-bg;
root.dark-surface = AppTheme.custom-dark-surface;
root.dark-text = AppTheme.custom-dark-text;
root.dark-border = AppTheme.custom-dark-border;
root.dirty = true;
}
}
// Put the saved palette back.
public function revert() {
if root.dirty {
AppTheme.custom-light-primary = root.light-primary;
AppTheme.custom-light-page-bg = root.light-page-bg;
AppTheme.custom-light-surface = root.light-surface;
AppTheme.custom-light-text = root.light-text;
AppTheme.custom-light-border = root.light-border;
AppTheme.custom-dark-primary = root.dark-primary;
AppTheme.custom-dark-page-bg = root.dark-page-bg;
AppTheme.custom-dark-surface = root.dark-surface;
AppTheme.custom-dark-text = root.dark-text;
AppTheme.custom-dark-border = root.dark-border;
root.dirty = false;
}
}
}
export component ThemeEditor inherits Rectangle {
in property <bool> compact: false;
in-out property <string> theme-mode: "system";
@@ -396,17 +447,6 @@ export component ThemeEditor inherits Rectangle {
private property <string> selected-token: "primary";
private property <bool> editing-dark: AppTheme.dark;
private property <bool> dirty: false;
private property <color> saved-light-primary;
private property <color> saved-light-page-bg;
private property <color> saved-light-surface;
private property <color> saved-light-text;
private property <color> saved-light-border;
private property <color> saved-dark-primary;
private property <color> saved-dark-page-bg;
private property <color> saved-dark-surface;
private property <color> saved-dark-text;
private property <color> saved-dark-border;
private property <color> current-primary: root.editing-dark ? AppTheme.custom-dark-primary : AppTheme.custom-light-primary;
private property <color> current-page-bg: root.editing-dark ? AppTheme.custom-dark-page-bg : AppTheme.custom-light-page-bg;
@@ -467,40 +507,12 @@ export component ThemeEditor inherits Rectangle {
border-color: AppTheme.border-subtle;
background: AppTheme.surface;
function snapshot() {
root.saved-light-primary = AppTheme.custom-light-primary;
root.saved-light-page-bg = AppTheme.custom-light-page-bg;
root.saved-light-surface = AppTheme.custom-light-surface;
root.saved-light-text = AppTheme.custom-light-text;
root.saved-light-border = AppTheme.custom-light-border;
root.saved-dark-primary = AppTheme.custom-dark-primary;
root.saved-dark-page-bg = AppTheme.custom-dark-page-bg;
root.saved-dark-surface = AppTheme.custom-dark-surface;
root.saved-dark-text = AppTheme.custom-dark-text;
root.saved-dark-border = AppTheme.custom-dark-border;
}
function begin-edit() {
if !root.dirty {
root.snapshot();
root.dirty = true;
}
ThemeDraft.begin();
}
function revert() {
if root.dirty {
AppTheme.custom-light-primary = root.saved-light-primary;
AppTheme.custom-light-page-bg = root.saved-light-page-bg;
AppTheme.custom-light-surface = root.saved-light-surface;
AppTheme.custom-light-text = root.saved-light-text;
AppTheme.custom-light-border = root.saved-light-border;
AppTheme.custom-dark-primary = root.saved-dark-primary;
AppTheme.custom-dark-page-bg = root.saved-dark-page-bg;
AppTheme.custom-dark-surface = root.saved-dark-surface;
AppTheme.custom-dark-text = root.saved-dark-text;
AppTheme.custom-dark-border = root.saved-dark-border;
root.dirty = false;
}
ThemeDraft.revert();
}
function choose-mode(mode: string) {
@@ -554,7 +566,7 @@ export component ThemeEditor inherits Rectangle {
}
function persist() {
if root.dirty {
if ThemeDraft.dirty {
let saved = root.save-custom-theme(
AppTheme.custom-light-primary,
AppTheme.custom-light-page-bg,
@@ -568,7 +580,7 @@ export component ThemeEditor inherits Rectangle {
AppTheme.custom-dark-border,
);
if saved {
root.dirty = false;
ThemeDraft.dirty = false;
}
}
}
@@ -673,15 +685,15 @@ export component ThemeEditor inherits Rectangle {
}
Rectangle { horizontal-stretch: 1; }
if !root.compact : Text {
text: root.dirty ? @tr("Unsaved changes") : @tr("Saved");
color: root.dirty ? AppTheme.accent : AppTheme.secondary;
text: ThemeDraft.dirty ? @tr("Unsaved changes") : @tr("Saved");
color: ThemeDraft.dirty ? AppTheme.accent : AppTheme.secondary;
font-size: TypeScale.caption;
vertical-alignment: center;
}
}
if AppTheme.preset == "custom" && root.compact : Text {
text: root.dirty ? @tr("Unsaved changes") : @tr("Saved");
color: root.dirty ? AppTheme.accent : AppTheme.secondary;
text: ThemeDraft.dirty ? @tr("Unsaved changes") : @tr("Saved");
color: ThemeDraft.dirty ? AppTheme.accent : AppTheme.secondary;
font-size: TypeScale.caption;
wrap: word-wrap;
}
@@ -769,8 +781,8 @@ export component ThemeEditor inherits Rectangle {
spacing: 8px;
Button { text: root.compact ? @tr("Reset") : @tr("Reset this palette"); label-overflow: elide; clicked => { root.reset-current-palette(); } }
Rectangle { horizontal-stretch: 1; }
Button { text: @tr("Revert"); label-overflow: elide; enabled: root.dirty; clicked => { root.revert(); } }
Button { text: root.compact ? @tr("Save") : @tr("Save palette"); label-overflow: elide; primary: true; enabled: root.dirty; clicked => { root.persist(); } }
Button { text: @tr("Revert"); label-overflow: elide; enabled: ThemeDraft.dirty; clicked => { root.revert(); } }
Button { text: root.compact ? @tr("Save") : @tr("Save palette"); label-overflow: elide; primary: true; enabled: ThemeDraft.dirty; clicked => { root.persist(); } }
}
}
}