feat(options): OK / Apply / Close with a discard guard

The Options window used to commit every control the moment it was
touched, with no way back but re-editing. It now works like the Drafting
Settings window: changes still show at once (a theme, a cursor size or a
language is seen immediately), but nothing is committed until Apply or OK.
Close — and the window's × or Esc — puts everything back the way it was
when the window opened or Apply was last pressed, asking first when there
is something to lose.

The commit point is a snapshot of the persisted configuration plus the
drawing variables the 3D Modeling page edits (with the drawing's modified
flag), so every control the window has or gains is covered without being
registered; reverting re-applies the snapshot the way start-up does and
re-bakes text or regenerates solids when those settings moved.

The "unsaved changes" guard is now a shared modal helper used by both
windows.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-18 14:53:54 +03:00
1 parent 74936063f9
commit 1448909b6f
8 files changed
+365 -55

No files matched your search

+54
View File
@@ -244,6 +244,60 @@ pub fn backdrop<'a>(
stack![base.into(), opaque(shield)].into()
}
/// A dialog's "unsaved changes" guard: `main` dimmed behind a small panel
/// that offers to discard the changes and close, or to keep editing. Clicking
/// the dim area keeps editing. Callers show it in place of the plain dialog
/// once the user asked to close with something to lose.
pub fn discard_guard<'a>(
main: impl Into<Element<'a, Message>>,
on_discard: Message,
on_keep: Message,
) -> Element<'a, Message> {
let shield = mouse_area(
container(Space::new())
.width(Length::Fill)
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
)),
..Default::default()
}),
)
.on_press(on_keep.clone());
let panel = container(
column![
iced::widget::text(crate::t!("Unsaved changes will be discarded.")).size(13.5),
Space::new().height(12),
row![
button(iced::widget::text(crate::t!("Discard && close")).size(12))
.on_press(on_discard)
.padding([5, 14])
.style(button::danger),
button(iced::widget::text(crate::t!("Keep editing")).size(12))
.on_press(on_keep)
.padding([5, 14])
.style(button::secondary),
]
.spacing(10),
]
.spacing(0),
)
.padding([18, 22])
.style(container::rounded_box);
stack![
main.into(),
shield,
container(panel).center_x(Length::Fill).center_y(Length::Fill)
]
.into()
}
/// Stack `content` over `base` behind a dimmed backdrop, framed with a title bar
/// (the ✕ close button at its right end). The backdrop only dims and blocks
/// clicks from reaching the view beneath — it does **not** dismiss the dialog;
+5 -46
View File
@@ -2,7 +2,7 @@ use crate::app::settings::IsoPlane;
use crate::app::Message;
use crate::snap::{SnapType, ALL_3D_SNAP_MODES, ALL_SNAP_MODES};
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use iced::{Background, Border, Element, Fill, Theme};
use std::borrow::Cow;
/// Active tab in the Drafting Settings dialog.
@@ -717,51 +717,10 @@ pub fn view_window<'a>(
if !close_confirm {
return main_content.into();
}
let shield = iced::widget::mouse_area(
container(Space::new())
.width(Length::Fill)
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
)),
..Default::default()
}),
)
.on_press(Message::DraftingSettingsCloseKeep);
let warning_panel = container(
column![
text(crate::t!("Unsaved changes will be discarded.")).size(13.5),
Space::new().height(12),
row![
button(text(crate::t!("Discard && close")).size(12))
.on_press(Message::DraftingSettingsCloseDiscard)
.padding([5, 14])
.style(button::danger),
button(text(crate::t!("Keep editing")).size(12))
.on_press(Message::DraftingSettingsCloseKeep)
.padding([5, 14])
.style(button::secondary),
]
.spacing(10),
]
.spacing(0),
)
.padding([18, 22])
.style(container::rounded_box);
iced::widget::stack![
crate::ui::modal::discard_guard(
main_content,
shield,
container(warning_panel).center_x(Fill).center_y(Fill)
]
.into()
Message::DraftingSettingsCloseDiscard,
Message::DraftingSettingsCloseKeep,
)
}
+20 -5
View File
@@ -174,6 +174,8 @@ pub fn view_window<'a>(
model_bg_input: &'a str,
paper_bg_input: &'a str,
desk_bg_input: &'a str,
dirty: bool,
close_confirm: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
@@ -273,8 +275,18 @@ pub fn view_window<'a>(
);
}
// Changes show at once but are committed by OK / Apply; Close puts them
// back (asking first when there is something to lose).
let ok = button(text(crate::t!("OK")).size(12))
.on_press(Message::OptionsOk)
.padding([6, 18])
.style(button::primary);
let apply = button(text(crate::t!("Apply")).size(12))
.on_press_maybe(dirty.then_some(Message::OptionsApply))
.padding([6, 18])
.style(if dirty { button::secondary } else { button::text });
let close = button(text(crate::tr!("action", "close")).size(12))
.on_press(Message::CloseModal)
.on_press(Message::OptionsClose)
.padding([6, 18])
.style(button::secondary);
@@ -1678,7 +1690,7 @@ pub fn view_window<'a>(
// controls at the trailing edge of the Options content.
scrollable(content).spacing(8).height(Fill),
Space::new().height(12),
row![Space::new().width(Fill), close],
row![Space::new().width(Fill), ok, apply, close].spacing(8),
]
.width(Fill)
.height(sizing.height);
@@ -1688,10 +1700,13 @@ pub fn view_window<'a>(
.height(sizing.height);
let intrinsic = sizing.width == crate::ui::modal::ModalSizing::INTRINSIC.width;
container(body)
let main = container(body)
.style(container::rounded_box)
.padding([16, 18])
.width(if intrinsic { iced::Length::Fixed(DIALOG_WIDTH) } else { sizing.width })
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height })
.into()
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height });
if !close_confirm {
return main.into();
}
crate::ui::modal::discard_guard(main, Message::OptionsCloseDiscard, Message::OptionsCloseKeep)
}