feat: replace rfd Save As with native iced file chooser
Implements a full in-app file browser window for Save As, replacing the rfd dialog that could not expose the selected filter index on Linux. The new chooser includes directory navigation, a scrollable file list, filename input, and a format drop-down (DWG/DXF × 2018/2013/2010/2007/2004/2000/R14). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9cd8eff77f
commit
7e8fc8ef88
4 changed files with 224 additions and 146 deletions
206
src/app/view.rs
206
src/app/view.rs
|
|
@ -119,6 +119,7 @@ impl H7CAD {
|
|||
return save_as_dialog_window(
|
||||
&self.save_dialog_filename,
|
||||
&self.save_dialog_folder,
|
||||
&self.save_dialog_entries,
|
||||
&self.save_dialog_format,
|
||||
);
|
||||
}
|
||||
|
|
@ -856,95 +857,170 @@ const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
|||
|
||||
fn save_as_dialog_window<'a>(
|
||||
filename: &'a str,
|
||||
folder: &'a str,
|
||||
folder: &'a std::path::Path,
|
||||
entries: &'a [(String, bool, std::path::PathBuf)],
|
||||
format: &'a str,
|
||||
) -> Element<'a, 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 INPUT_BG: Color = Color { r: 0.12, g: 0.12, b: 0.14, a: 1.0 };
|
||||
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, b: 0.36, a: 1.0 };
|
||||
const TEXT: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
|
||||
const DIM: Color = Color { r: 0.58, g: 0.58, 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, b: 0.80, a: 1.0 };
|
||||
const BTN_HOV: 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 };
|
||||
const BTN_GREY: Color = Color { r: 0.26, g: 0.26, b: 0.29, a: 1.0 };
|
||||
const BTN_GHOV: Color = Color { r: 0.34, g: 0.34, 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_style = |_: &Theme, _status: iced::widget::text_input::Status| {
|
||||
let input_sty = |_: &Theme, _: iced::widget::text_input::Status| {
|
||||
iced::widget::text_input::Style {
|
||||
background: Background::Color(INPUT_BG),
|
||||
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
|
||||
icon: TEXT_COL,
|
||||
placeholder: Color { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
|
||||
value: TEXT_COL,
|
||||
selection: Color { r: 0.20, g: 0.46, b: 0.80, a: 0.5 },
|
||||
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 = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
||||
button(text(label).size(13).color(TEXT_COL))
|
||||
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
||||
button(text(lbl).size(12).color(TEXT))
|
||||
.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,
|
||||
.style(move |_: &Theme, st| button::Style {
|
||||
background: Some(Background::Color(
|
||||
if matches!(st, button::Status::Hovered | button::Status::Pressed) { hov } else { base }
|
||||
)),
|
||||
text_color: TEXT,
|
||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.padding([5, 14])
|
||||
.padding([4, 12])
|
||||
};
|
||||
|
||||
let sel_static = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
||||
// ── Path bar ─────────────────────────────────────────────────────────
|
||||
let path_str = folder.to_string_lossy().into_owned();
|
||||
let up_path = folder.parent().map(|p| p.to_path_buf());
|
||||
let path_bar = row![
|
||||
{
|
||||
let up_msg = up_path.map(Message::SaveDialogNavigate);
|
||||
let b = button(text("↑").size(14).color(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);
|
||||
|
||||
let label = |s: &'static str| text(s).size(12).color(Color { r: 0.65, g: 0.65, b: 0.68, a: 1.0 });
|
||||
// ── File list ─────────────────────────────────────────────────────────
|
||||
let file_list: Element<'_, Message> = {
|
||||
let rows: Vec<Element<'_, Message>> = entries.iter().map(|(name, is_dir, path)| {
|
||||
let icon = if *is_dir { "📁" } else { "📄" };
|
||||
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
||||
let p = path.clone();
|
||||
let d = *is_dir;
|
||||
mouse_area(
|
||||
container(
|
||||
row![
|
||||
text(icon).size(13),
|
||||
Space::new().width(6),
|
||||
text(name.as_str()).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![
|
||||
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),
|
||||
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),
|
||||
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![
|
||||
label("File name"),
|
||||
iced::widget::Space::new().height(4),
|
||||
iced::widget::text_input("drawing.dwg", filename)
|
||||
.on_input(Message::SaveDialogFilenameChanged)
|
||||
.style(input_style)
|
||||
.size(13)
|
||||
.padding([6, 8]),
|
||||
iced::widget::Space::new().height(10),
|
||||
label("Save in"),
|
||||
iced::widget::Space::new().height(4),
|
||||
row![
|
||||
iced::widget::text_input("/home/...", folder)
|
||||
.on_input(Message::SaveDialogFolderChanged)
|
||||
.style(input_style)
|
||||
.size(13)
|
||||
.padding([6, 8])
|
||||
.width(Fill),
|
||||
iced::widget::Space::new().width(6),
|
||||
btn("Browse…", Message::SaveDialogBrowse, BTN_DISC, BTN_DHOV),
|
||||
],
|
||||
iced::widget::Space::new().height(10),
|
||||
label("Format"),
|
||||
iced::widget::Space::new().height(4),
|
||||
pick_list(
|
||||
SAVE_FORMAT_OPTIONS,
|
||||
sel_static,
|
||||
|s: &str| Message::SaveDialogFormatChanged(s.to_string()),
|
||||
)
|
||||
.width(Fill),
|
||||
iced::widget::Space::new().height(16),
|
||||
row![
|
||||
iced::widget::Space::new().width(Fill),
|
||||
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
||||
iced::widget::Space::new().width(8),
|
||||
btn("Cancel", Message::SaveDialogCancel, BTN_DISC, BTN_DHOV),
|
||||
],
|
||||
path_bar,
|
||||
Space::new().height(8),
|
||||
file_list,
|
||||
Space::new().height(10),
|
||||
bottom,
|
||||
]
|
||||
.spacing(0),
|
||||
)
|
||||
.style(move |_: &Theme| container::Style {
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BG)),
|
||||
..Default::default()
|
||||
})
|
||||
.padding([20, 24])
|
||||
.padding([14, 16])
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
|
|
|
|||
Loading…
Reference in a new issue