fix(start): complete Start page workflow

Track Save As paths, persist the selected section, and size tabbed
panels from their actual available width.

Refs #443
This commit is contained in:
Hakan Seven 2026-07-24 17:44:07 +03:00
commit 658aa4a4a5
5 changed files with 61 additions and 19 deletions

View file

@ -20,6 +20,8 @@ pub struct AppConfig {
pub settings: UserSettings, pub settings: UserSettings,
/// Recent-files list + retained count. /// Recent-files list + retained count.
pub recent: RecentConfig, pub recent: RecentConfig,
/// Last selected section on the tabbed Start page.
pub start: StartConfig,
/// Which status-bar pills the user has hidden. /// Which status-bar pills the user has hidden.
pub statusbar: StatusBarConfig, pub statusbar: StatusBarConfig,
/// Ribbon collapse density. /// Ribbon collapse density.
@ -34,6 +36,7 @@ impl Default for AppConfig {
Self { Self {
settings: UserSettings::default(), settings: UserSettings::default(),
recent: RecentConfig::default(), recent: RecentConfig::default(),
start: StartConfig::default(),
statusbar: StatusBarConfig::default(), statusbar: StatusBarConfig::default(),
ribbon: RibbonConfig::default(), ribbon: RibbonConfig::default(),
plot: PlotDialogState::default(), plot: PlotDialogState::default(),
@ -59,6 +62,12 @@ impl Default for RecentConfig {
} }
} }
#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct StartConfig {
pub section: super::StartSection,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Default)] #[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)] #[serde(default)]
pub struct RibbonConfig { pub struct RibbonConfig {

View file

@ -211,7 +211,9 @@ struct AddSelectedRestore {
} }
/// Which Start-page section a narrow (tabbed) Start page is showing. /// Which Start-page section a narrow (tabbed) Start page is showing.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] #[derive(
Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum StartSection { pub enum StartSection {
Recent, Recent,
Videos, Videos,

View file

@ -292,6 +292,9 @@ impl OpenCADStudio {
.collect(), .collect(),
limit: self.recent_limit, limit: self.recent_limit,
}, },
start: crate::app::config::StartConfig {
section: self.start_section,
},
statusbar: self.statusbar_config.clone(), statusbar: self.statusbar_config.clone(),
ribbon: crate::app::config::RibbonConfig { ribbon: crate::app::config::RibbonConfig {
collapse: self.ribbon.collapse_mode(), collapse: self.ribbon.collapse_mode(),
@ -317,6 +320,7 @@ impl OpenCADStudio {
self.recent_limit_input = self.recent_limit.to_string(); self.recent_limit_input = self.recent_limit.to_string();
// Thumbnails are decoded by a background task queued at boot // Thumbnails are decoded by a background task queued at boot
// (`refresh_recent_thumbs`) — never here on the boot path. // (`refresh_recent_thumbs`) — never here on the boot path.
self.start_section = cfg.start.section;
self.statusbar_config = cfg.statusbar; self.statusbar_config = cfg.statusbar;
self.ribbon.set_collapse_mode(cfg.ribbon.collapse); self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot; self.plot_dialog = cfg.plot;
@ -952,9 +956,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].current_path = Some(path.clone()); self.tabs[i].current_path = Some(path.clone());
self.tabs[i].dirty = false; self.tabs[i].dirty = false;
let _ = std::fs::remove_file(path.with_extension("sv$")); let _ = std::fs::remove_file(path.with_extension("sv$"));
let recent = self.push_recent(path.clone());
if self.save_dialog_for_unsaved { if self.save_dialog_for_unsaved {
return self.update(Message::UnsavedPickedSavePath(Some(path))); return Task::batch([
recent,
self.update(Message::UnsavedPickedSavePath(Some(path))),
]);
} }
return recent;
} }
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
} }

View file

@ -224,6 +224,7 @@ impl OpenCADStudio {
Message::StartSectionSelect(section) => { Message::StartSectionSelect(section) => {
self.start_section = section; self.start_section = section;
self.save_config();
Task::none() Task::none()
} }

View file

@ -115,18 +115,21 @@ impl OpenCADStudio {
// viewport draws the layout's own geometry (white sheet + entities + // viewport draws the layout's own geometry (white sheet + entities +
// borders) and the floating content viewports blit on top. // borders) and the floating content viewports blit on top.
let viewport_3d: Element<'_, Message> = if tab.is_start { let viewport_3d: Element<'_, Message> = if tab.is_start {
start_page_view( responsive(|size| {
&self.patrons, start_page_view(
&self.videos, &self.patrons,
self.videos_loading, &self.videos,
&self.video_thumbs, self.videos_loading,
&self.recent_files, &self.video_thumbs,
&self.recent_thumbs, &self.recent_files,
self.recent_limit, &self.recent_thumbs,
&self.recent_limit_input, self.recent_limit,
self.win_size.0, &self.recent_limit_input,
self.start_section, size.width,
) self.start_section,
)
})
.into()
} else if is_paper { } else if is_paper {
shader(ViewportPane::model( shader(ViewportPane::model(
&tab.scene, &tab.scene,
@ -2504,7 +2507,17 @@ pub(super) fn start_page_view<'a>(
let fits_all = avail let fits_all = avail
>= recent_w + sup_w + 2.0 * 16.0 + welcome_min + 2.0 * (videos_w + 16.0); >= recent_w + sup_w + 2.0 * 16.0 + welcome_min + 2.0 * (videos_w + 16.0);
let recent = recent_files_panel(recents, thumbs, recent_limit, recent_limit_input); let recent = recent_files_panel(
recents,
thumbs,
recent_limit,
recent_limit_input,
if fits_all {
iced::Length::Fixed(recent_w)
} else {
iced::Length::Fill
},
);
let welcome = container(content).width(Fill).height(Fill); let welcome = container(content).width(Fill).height(Fill);
// Tutorial-videos rail: the official playlist, fetched at boot (cached on // Tutorial-videos rail: the official playlist, fetched at boot (cached on
@ -2593,7 +2606,11 @@ pub(super) fn start_page_view<'a>(
Space::new().height(iced::Length::Fixed(12.0)), Space::new().height(iced::Length::Fixed(12.0)),
playlist_btn, playlist_btn,
]) ])
.width(iced::Length::Fixed(videos_w)) .width(if fits_all {
iced::Length::Fixed(videos_w)
} else {
iced::Length::Fill
})
.height(Fill) .height(Fill)
.padding(16) .padding(16)
.style(|_: &Theme| container::Style { .style(|_: &Theme| container::Style {
@ -2672,7 +2689,11 @@ pub(super) fn start_page_view<'a>(
Space::new().height(iced::Length::Fixed(12.0)), Space::new().height(iced::Length::Fixed(12.0)),
support_btn, support_btn,
]) ])
.width(iced::Length::Fixed(240.0)) .width(if fits_all {
iced::Length::Fixed(sup_w)
} else {
iced::Length::Fill
})
.height(Fill) .height(Fill)
.padding(20) .padding(20)
.style(|_: &Theme| container::Style { .style(|_: &Theme| container::Style {
@ -2804,6 +2825,7 @@ pub(super) fn recent_files_panel<'a>(
>, >,
limit: usize, limit: usize,
limit_input: &'a str, limit_input: &'a str,
width: iced::Length,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
// Card chrome matches the Supporters rail (the canonical start-page card). // Card chrome matches the Supporters rail (the canonical start-page card).
const PANEL_BG: Color = Color { const PANEL_BG: Color = Color {
@ -2993,7 +3015,7 @@ pub(super) fn recent_files_panel<'a>(
] ]
.width(Fill), .width(Fill),
) )
.width(iced::Length::Fixed(280.0)) .width(width)
.height(Fill) .height(Fill)
.padding(20) .padding(20)
.style(|_: &Theme| container::Style { .style(|_: &Theme| container::Style {
@ -3007,4 +3029,3 @@ pub(super) fn recent_files_panel<'a>(
}) })
.into() .into()
} }