feat(save): native OS dialog for Save As, keep in-app format picker

Replace the custom in-app file-browser save dialog with the native OS
save dialog for choosing the destination path — folder browsing and
overwrite confirmation now come from the OS. A compact in-app modal
still selects the DWG/DXF format and version (a native dialog cannot
carry the version year) and appears only for Save As. Plain Save on an
unsaved drawing defaults to DWG 2018 with no picker. The file-name field
is shown only on web, where there is no native dialog.

Remove the hand-rolled directory listing, folder navigation, and
overwrite modal.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-11 09:12:36 +03:00
1 parent f3482e8b3e
commit 6032a024d2
8 files changed
+212 -526

No files matched your search

+4 -23
View File
@@ -435,8 +435,6 @@ pub(super) struct OpenCADStudio {
aec_drop_acknowledged: bool,
/// Number of unsupported objects shown in the AEC-drop warning modal.
aec_drop_count: usize,
/// Set once the user confirms overwriting an existing file on Save-As.
overwrite_acknowledged: bool,
/// Layers awaiting a "delete non-empty layer(s)" confirmation: `(names,
/// total object count)`. Set when the user deletes one or more layers that
/// still have objects; the warning modal reads it, and confirming erases
@@ -677,12 +675,9 @@ pub(super) struct OpenCADStudio {
/// OS window for the custom Save As dialog.
/// Currently selected format string, e.g. "DWG 2013".
save_dialog_format: String,
/// Editable filename (without path), e.g. "drawing.dwg".
/// Editable filename (without path), e.g. "drawing.dwg" — seeds the native
/// OS save dialog's default name (native) or the download name (web).
save_dialog_filename: String,
/// Currently browsed folder (PathBuf for reliable fs ops).
save_dialog_folder: std::path::PathBuf,
/// Cached directory listing: (display_name, is_dir, full_path).
save_dialog_entries: Vec<(String, bool, std::path::PathBuf)>,
/// True when triggered from the unsaved-changes flow.
save_dialog_for_unsaved: bool,
@@ -1097,7 +1092,6 @@ pub enum ModalKind {
Unsaved,
SaveDialog,
AecDropWarning,
OverwriteWarning,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
AssocPrompt,
PointStyle,
@@ -1234,12 +1228,10 @@ pub enum Message {
// ── Custom Save-As dialog ─────────────────────────────────────────────
SaveDialogFormatChanged(String),
SaveDialogFilenameChanged(String),
/// Navigate the file-chooser to the given directory.
SaveDialogNavigate(std::path::PathBuf),
/// User clicked a file entry (fill filename) or directory entry (navigate).
SaveDialogEntryClicked(std::path::PathBuf, bool),
SaveDialogConfirm,
SaveDialogCancel,
/// Destination picked in the native OS save dialog (`None` = cancelled).
SaveDialogPathPicked(Option<std::path::PathBuf>),
ClearScene,
SetWireframe(bool),
/// Set the active tab's render mode (one of acadrust's seven visual
@@ -1285,11 +1277,6 @@ pub enum Message {
AecDropProceed,
/// Go back to the Save dialog from the AEC-drop warning.
AecDropBack,
// ── Overwrite confirmation (Save-As over an existing file) ────────────
/// Replace the existing file and save.
OverwriteConfirm,
/// Go back to the Save dialog from the overwrite warning.
OverwriteCancel,
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
AutoSave,
/// Save-as path picked for the unsaved-changes → save → close flow.
@@ -2170,7 +2157,6 @@ impl OpenCADStudio {
active_modal: None,
aec_drop_acknowledged: false,
aec_drop_count: 0,
overwrite_acknowledged: false,
layer_delete_pending: None,
modal_offset: iced::Vector::ZERO,
modal_drag_last: None,
@@ -2218,11 +2204,6 @@ impl OpenCADStudio {
pending_close: None,
save_dialog_format: "DWG 2018".to_string(),
save_dialog_filename: "drawing.dwg".to_string(),
save_dialog_folder: std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string())
.into(),
save_dialog_entries: Vec::new(),
save_dialog_for_unsaved: false,
// Plot style
active_plot_style: None,
+10 -11
View File
@@ -36,21 +36,17 @@ impl OpenCADStudio {
self.save_dialog_format =
crate::io::format_for_version(acadrust::DxfVersion::AC1032, is_dxf);
// Pre-fill filename and folder from current path or defaults.
// Pre-fill the default file name from the current path or the tab name;
// the destination folder comes from the native OS dialog that follows.
if let Some(p) = &self.tabs[tab_idx].current_path.clone() {
if let Some(name) = p.file_name() {
self.save_dialog_filename = name.to_string_lossy().into_owned();
}
if let Some(dir) = p.parent() {
self.save_dialog_folder = dir.to_path_buf();
}
} else {
let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format);
self.save_dialog_filename = format!("{}.{ext}", self.tabs[tab_idx].tab_display_name());
}
self.save_dialog_entries = crate::io::read_dir_entries(&self.save_dialog_folder.clone());
self.aec_drop_acknowledged = false;
self.overwrite_acknowledged = false;
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
Task::none()
}
@@ -58,7 +54,6 @@ impl OpenCADStudio {
pub(in crate::app) fn close_save_dialog_window(&mut self) -> Task<Message> {
self.aec_drop_acknowledged = false;
self.overwrite_acknowledged = false;
if self.active_modal == Some(crate::app::ModalKind::SaveDialog) {
self.active_modal = None;
}
@@ -221,11 +216,13 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
}
} else {
// No path — close unsaved dialog, open custom Save As dialog.
// No path — close unsaved dialog and save a default
// DWG 2018 via the native destination dialog (no
// version picker; that is reserved for Save As).
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
self.save_dialog_for_unsaved = true;
let close_win = self.close_unsaved_dialog_window();
let open_save = self.open_save_dialog_window(idx);
let open_save = self.save_default_dwg2018(idx);
return Task::batch([close_win, open_save]);
}
}
@@ -245,12 +242,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
}
} else {
// No path — close unsaved dialog, open custom Save As dialog.
// No path — close unsaved dialog and save a
// default DWG 2018 via the native destination
// dialog (version picker is Save-As only).
self.active_tab = idx;
self.pending_close = Some(crate::app::PendingClose::Quit);
self.save_dialog_for_unsaved = true;
let close_win = self.close_unsaved_dialog_window();
let open_save = self.open_save_dialog_window(idx);
let open_save = self.save_default_dwg2018(idx);
return Task::batch([close_win, open_save]);
}
}
+98 -46
View File
@@ -723,7 +723,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none();
}
self.save_dialog_for_unsaved = false;
self.open_save_dialog_window(i)
self.save_default_dwg2018(i)
}
/// Save without the version picker: default to DWG 2018 and go straight to
/// the native destination dialog (native) or the browser download (web).
/// Used by plain Save (QSAVE) on an as-yet-unsaved drawing and by the
/// save-before-close flow — the version picker is reserved for Save As.
pub(in crate::app) fn save_default_dwg2018(&mut self, tab_idx: usize) -> Task<Message> {
self.active_tab = tab_idx;
self.save_dialog_format = "DWG 2018".to_string();
self.save_dialog_filename = self.tabs[tab_idx]
.current_path
.as_ref()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("{}.dwg", self.tabs[tab_idx].tab_display_name()));
self.aec_drop_acknowledged = false;
self.on_save_dialog_confirm()
}
pub(super) fn on_save_dialog_confirm(&mut self) -> Task<Message> {
@@ -756,52 +773,49 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
name.to_string()
};
self.save_dialog_filename = filename.clone();
// Confirm before overwriting an existing file in the folder.
#[cfg(not(target_arch = "wasm32"))]
if !self.overwrite_acknowledged
&& self.save_dialog_folder.join(&filename).exists()
{
self.active_modal = Some(crate::app::ModalKind::OverwriteWarning);
return Task::none();
}
let close = self.close_save_dialog_window();
let i = self.active_tab;
sync_annotation_scale_header(&mut self.tabs[i].scene);
// Persist Ortho / running OSNAP into the header (Save-As path).
self.stamp_header_sysvars(i);
self.sync_truck_solids_to_acis(i);
self.stamp_thumbnail(i);
// Native: write to the chosen path. Web: download the bytes
// under the chosen name (no filesystem).
// Native: hand the destination choice to the OS save dialog —
// it provides folder browsing and overwrite confirmation. The
// chosen version rides in `save_dialog_format` and the write
// happens in `on_save_dialog_path_picked` once a path returns.
#[cfg(not(target_arch = "wasm32"))]
{
let path = self.save_dialog_folder.join(&filename);
if self.backup_on_save {
crate::io::write_backup(&path);
}
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
Ok(()) => {
self.command_line
.push_output(&format!("Saved: {}", path.display()));
// Drop any prior autosave copy — including the temp
// one used while the drawing was still unsaved —
// before the tab takes on its new path.
let _ = std::fs::remove_file(self.autosave_target(i));
self.tabs[i].current_path = Some(path.clone());
self.tabs[i].dirty = false;
let _ = std::fs::remove_file(path.with_extension("sv$"));
if self.save_dialog_for_unsaved {
let next = self.update(Message::UnsavedPickedSavePath(Some(path)));
return Task::batch([close, next]);
let seed_dir = self.tabs[i]
.current_path
.as_ref()
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
let default_name = filename;
let (filter_label, filter_ext): (&str, &str) =
if ext.eq_ignore_ascii_case("dxf") {
("DXF Files", "dxf")
} else {
("DWG Files", "dwg")
};
let close = self.close_save_dialog_window();
let pick = Task::perform(
async move {
let mut dlg = rfd::AsyncFileDialog::new()
.set_title("Save Drawing As")
.set_file_name(default_name)
.add_filter(filter_label, &[filter_ext]);
if let Some(dir) = seed_dir {
dlg = dlg.set_directory(dir);
}
}
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
}
close
dlg.save_file().await.map(|h| crate::sys::handle_path(&h))
},
Message::SaveDialogPathPicked,
);
Task::batch([close, pick])
}
// Web: no filesystem — serialize and hand the browser a download.
#[cfg(target_arch = "wasm32")]
{
let close = self.close_save_dialog_window();
sync_annotation_scale_header(&mut self.tabs[i].scene);
self.stamp_header_sysvars(i);
self.sync_truck_solids_to_acis(i);
self.stamp_thumbnail(i);
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) {
Ok(bytes) => {
crate::sys::download_bytes(&filename, &bytes);
@@ -821,6 +835,51 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
}
/// Native: a destination path came back from the OS save dialog — write the
/// file there in the chosen version. `None` means the user cancelled.
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn on_save_dialog_path_picked(
&mut self,
picked: Option<std::path::PathBuf>,
) -> Task<Message> {
let Some(path) = picked else {
// Cancelled the OS dialog — if this came from the unsaved-changes →
// save → close flow, re-show the confirmation so the tab isn't lost.
if self.save_dialog_for_unsaved && self.pending_close.is_some() {
return self.open_unsaved_dialog_window();
}
return Task::none();
};
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
let i = self.active_tab;
sync_annotation_scale_header(&mut self.tabs[i].scene);
// Persist Ortho / running OSNAP into the header (Save-As path).
self.stamp_header_sysvars(i);
self.sync_truck_solids_to_acis(i);
self.stamp_thumbnail(i);
if self.backup_on_save {
crate::io::write_backup(&path);
}
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
Ok(()) => {
self.command_line
.push_output(&format!("Saved: {}", path.display()));
// Drop any prior autosave copy — including the temp one used
// while the drawing was still unsaved — before the tab takes on
// its new path.
let _ = std::fs::remove_file(self.autosave_target(i));
self.tabs[i].current_path = Some(path.clone());
self.tabs[i].dirty = false;
let _ = std::fs::remove_file(path.with_extension("sv$"));
if self.save_dialog_for_unsaved {
return self.update(Message::UnsavedPickedSavePath(Some(path)));
}
}
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
}
Task::none()
}
/// AEC-drop warning → "Save anyway": accept the loss and proceed with the
/// format the user already chose.
pub(super) fn on_aec_drop_proceed(&mut self) -> Task<Message> {
@@ -846,13 +905,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.on_save_dialog_confirm()
}
/// Overwrite warning → "Replace": save over the existing file.
pub(super) fn on_overwrite_confirm(&mut self) -> Task<Message> {
self.overwrite_acknowledged = true;
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
self.on_save_dialog_confirm()
}
/// Where the autosave recovery copy for tab `i` lives: beside a saved
/// drawing as `<file>.sv$`, or — for an unsaved drawing with no path yet —
/// under the system temp dir keyed by the tab's display name.
+6 -24
View File
@@ -558,29 +558,16 @@ impl OpenCADStudio {
Task::none()
}
Message::SaveDialogNavigate(path) => {
self.save_dialog_folder = path.clone();
self.save_dialog_entries = crate::io::read_dir_entries(&path);
Task::none()
}
Message::SaveDialogEntryClicked(path, is_dir) => {
if is_dir {
self.save_dialog_folder = path.clone();
self.save_dialog_entries = crate::io::read_dir_entries(&path);
} else {
// Fill filename from clicked file.
if let Some(name) = path.file_name() {
self.save_dialog_filename = name.to_string_lossy().into_owned();
}
}
Task::none()
}
Message::SaveDialogConfirm => self.on_save_dialog_confirm(),
Message::SaveDialogCancel => self.close_save_dialog_window(),
#[cfg(not(target_arch = "wasm32"))]
Message::SaveDialogPathPicked(picked) => self.on_save_dialog_path_picked(picked),
#[cfg(target_arch = "wasm32")]
Message::SaveDialogPathPicked(_) => Task::none(),
Message::ClearScene => {
let i = self.active_tab;
self.push_undo_snapshot(i, "CLEAR");
@@ -3139,11 +3126,6 @@ impl OpenCADStudio {
Task::none()
}
Message::OverwriteConfirm => self.on_overwrite_confirm(),
Message::OverwriteCancel => {
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
Task::none()
}
Message::AutoSave => self.on_autosave(),
Message::UnsavedPickedSavePath(Some(path)) => {
+4 -2
View File
@@ -1565,8 +1565,10 @@ impl OpenCADStudio {
AssocPrompt => (440, 210),
Unsaved => (420, 160),
AecDropWarning => (480, 230),
OverwriteWarning => (420, 180),
SaveDialog => (560, 480),
#[cfg(target_arch = "wasm32")]
SaveDialog => (420, 200),
#[cfg(not(target_arch = "wasm32"))]
SaveDialog => (420, 150),
PointStyle => (360, 470),
AttributeEditor => (640, 500),
LayerDeleteWarning => (440, 200),
+90 -261
View File
@@ -1,8 +1,5 @@
use super::super::{Message, OpenCADStudio};
use iced::widget::{
button, column, container, mouse_area, pick_list, row, text, text_input,
Space,
};
use iced::widget::{button, column, container, pick_list, row, text, Space};
use iced::{Background, Border, Color, Element, Fill, Theme};
impl OpenCADStudio {
@@ -474,9 +471,6 @@ impl OpenCADStudio {
230,
)
}
super::super::ModalKind::OverwriteWarning => {
sized(overwrite_dialog_window(&self.save_dialog_filename), 420, 180)
}
super::super::ModalKind::LayerDeleteWarning => {
let (names, count) = self
.layer_delete_pending
@@ -540,16 +534,19 @@ impl OpenCADStudio {
500,
)
}
super::super::ModalKind::SaveDialog => sized(
save_as_dialog_window(
&self.save_dialog_filename,
&self.save_dialog_folder,
&self.save_dialog_entries,
&self.save_dialog_format,
),
560,
480,
),
super::super::ModalKind::SaveDialog => {
// Native omits the file-name field (the OS dialog collects it),
// so it stands shorter than the web build.
#[cfg(target_arch = "wasm32")]
let h = 200;
#[cfg(not(target_arch = "wasm32"))]
let h = 150;
sized(
save_as_dialog_window(&self.save_dialog_filename, &self.save_dialog_format),
420,
h,
)
}
})
}
}
@@ -559,24 +556,16 @@ const SAVE_FORMAT_OPTIONS: &[&str] = &[
"DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
];
fn save_as_dialog_window<'a>(
filename: &'a str,
folder: &'a std::path::Path,
entries: &'a [(String, bool, std::path::PathBuf)],
format: &'a str,
) -> Element<'a, Message> {
/// Compact Save-As options dialog: pick the format/version and a default file
/// name. The destination folder and overwrite confirmation come from the
/// native OS save dialog (native) or the browser download (web) that follows.
fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, Message> {
const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.17,
a: 1.0,
};
const LIST_BG: Color = Color {
r: 0.11,
g: 0.11,
b: 0.13,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.32,
g: 0.32,
@@ -595,12 +584,6 @@ fn save_as_dialog_window<'a>(
b: 0.62,
a: 1.0,
};
const INPUT_BG: Color = Color {
r: 0.10,
g: 0.10,
b: 0.12,
a: 1.0,
};
const BTN_OK: Color = Color {
r: 0.20,
g: 0.46,
@@ -625,38 +608,6 @@ fn save_as_dialog_window<'a>(
b: 0.38,
a: 1.0,
};
const DIR_COL: Color = Color {
r: 0.75,
g: 0.85,
b: 1.00,
a: 1.0,
};
const FILE_COL: Color = TEXT;
const ROW_HOV: Color = Color {
r: 0.22,
g: 0.24,
b: 0.28,
a: 1.0,
};
let input_sty =
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
background: Background::Color(INPUT_BG),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 0.45,
},
};
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
button(text(lbl).size(12).color(TEXT))
@@ -680,163 +631,94 @@ fn save_as_dialog_window<'a>(
.padding([4, 12])
};
// ── Path bar ─────────────────────────────────────────────────────────
let path_str = if crate::io::is_drives_root(folder) {
crate::io::drives_root_label().to_string()
} else {
folder.to_string_lossy().into_owned()
};
let up_path = crate::io::parent_folder(folder);
let path_bar = row![
{
let up_msg = up_path.map(Message::SaveDialogNavigate);
let b = button(crate::ui::icons::tinted(crate::ui::icons::UP, 14.0, TEXT))
.style(|_: &Theme, st| button::Style {
background: Some(Background::Color(
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
BTN_GHOV
} else {
BTN_GREY
},
)),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([3, 10]);
if let Some(msg) = up_msg {
b.on_press(msg)
} else {
b
}
},
Space::new().width(8),
container(text(path_str.clone()).size(12).color(DIM))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(INPUT_BG)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into()
},
..Default::default()
})
.padding([4, 8])
.width(Fill),
]
.align_y(iced::Alignment::Center);
// ── File list ─────────────────────────────────────────────────────────
let file_list: Element<'_, Message> = {
let rows: Vec<Element<'_, Message>> = entries
.iter()
.map(|(name, is_dir, path)| {
let icon_bytes = if *is_dir {
crate::ui::icons::FOLDER
} else {
crate::ui::icons::DOC
};
let color = if *is_dir { DIR_COL } else { FILE_COL };
let p = path.clone();
let d = *is_dir;
mouse_area(
container(
row![
crate::ui::icons::tinted(icon_bytes, 13.0, color),
Space::new().width(6),
text(crate::ui::text_util::elide(name.as_str(), 48))
.size(13)
.color(color),
]
.align_y(iced::Alignment::Center),
)
.style(|_: &Theme| container::Style {
..Default::default()
})
.padding([3, 8])
.width(Fill),
)
.on_press(Message::SaveDialogEntryClicked(p, d))
.into()
})
.collect();
container(iced::widget::scrollable(
column(rows).spacing(1).width(Fill),
))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(LIST_BG)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.width(Fill)
.height(Fill)
.into()
};
let _ = ROW_HOV; // used conceptually, suppress warning
let sel_fmt = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
let label = |s: &'static str| text(s).size(11).color(DIM);
// ── Bottom controls ───────────────────────────────────────────────────
let bottom = column![
let mut items: Vec<Element<'a, Message>> = Vec::new();
items.push(text("Save Drawing As").size(14).color(TEXT).into());
items.push(Space::new().height(12).into());
// Web has no native file dialog, so the file name is typed here. On native
// the OS save dialog collects the name, so this field is omitted.
#[cfg(target_arch = "wasm32")]
{
const INPUT_BG: Color = Color {
r: 0.10,
g: 0.10,
b: 0.12,
a: 1.0,
};
let input_sty =
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
background: Background::Color(INPUT_BG),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: Color {
r: 0.20,
g: 0.46,
b: 0.80,
a: 0.45,
},
};
items.push(
row![
label("File name:").width(70),
iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
.style(input_sty)
.size(13)
.padding([5, 8])
.width(Fill),
]
.align_y(iced::Alignment::Center)
.spacing(6)
.into(),
);
items.push(Space::new().height(8).into());
}
#[cfg(not(target_arch = "wasm32"))]
let _ = filename;
items.push(
row![
label("File name:").width(90),
text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
.style(input_sty)
.size(13)
.padding([5, 8])
.width(Fill),
]
.align_y(iced::Alignment::Center)
.spacing(6),
Space::new().height(6),
row![
label("Format:").width(90),
label("Format:").width(70),
pick_list(SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| {
Message::SaveDialogFormatChanged(s.to_string())
})
.width(Fill),
]
.align_y(iced::Alignment::Center)
.spacing(6),
Space::new().height(12),
.spacing(6)
.into(),
);
items.push(Space::new().height(16).into());
items.push(
row![
Space::new().width(Fill),
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
Space::new().width(8),
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
],
]
.spacing(0);
container(
column![
path_bar,
Space::new().height(8),
file_list,
Space::new().height(10),
bottom,
]
.spacing(0),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([14, 16])
.width(Fill)
.height(Fill)
.into()
.into(),
);
let body = column(items).spacing(0);
container(body)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.padding([14, 16])
.width(Fill)
.height(Fill)
.into()
}
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
@@ -992,59 +874,6 @@ fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Elem
/// Confirmation shown when the chosen Save-As filename already exists in the
/// target folder. "Replace" overwrites; "Cancel" returns to the Save dialog.
fn overwrite_dialog_window(filename: &str) -> Element<'static, Message> {
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
const BTN_SAVE: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
const BTN_HOVER: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
let body_text =
format!("\"{filename}\" already exists in this folder.\n\nDo you want to replace it?");
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
button(text(label).size(13).color(TEXT_COL))
.on_press(msg)
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => hov,
_ => base,
})),
text_color: TEXT_COL,
border: Border {
color: BORDER_COL,
width: 1.0,
radius: 4.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
})
.padding([6, 18])
};
container(
column![
text(body_text).size(13).color(TEXT_COL),
iced::widget::Space::new().height(20),
row![
btn("Replace", Message::OverwriteConfirm, BTN_SAVE, BTN_HOVER),
iced::widget::Space::new().width(8),
btn("Cancel", Message::OverwriteCancel, BTN_DISC, BTN_DHOV),
],
]
.spacing(0),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.center(Fill)
.padding([24, 28])
.into()
}
/// Confirm deleting layer(s) that still have objects on them. "Delete Objects"
/// erases them and removes the layers; "Cancel" leaves everything.
fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'static, Message> {