From a30f72f6270a49230fa2bb242f07f16ef4cb4c98 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 12:48:39 +0300 Subject: [PATCH] fix(start): refine responsive page layout --- src/app/mod.rs | 4 ++++ src/app/view/mod.rs | 54 ++++++++++++++++++--------------------------- src/ui/wrap_bar.rs | 14 ++++++++++++ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 001ae8760..7edbb0709 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -271,6 +271,9 @@ pub(super) struct OpenCADStudio { /// Which Start-page section is shown when the page is too narrow for all /// three side by side and falls back to a tab bar. start_section: StartSection, + /// Widest natural single-row width of the Start-page action buttons, + /// measured by `WrapFlow` so side lists collapse before those buttons wrap. + start_action_w: std::sync::Arc, /// When the window is too narrow the properties panel collapses to a /// vertical bar; this is the user's toggle to expand it back out. props_expanded: bool, @@ -2494,6 +2497,7 @@ impl OpenCADStudio { discussions_loading: false, props_asym_scale: std::collections::HashSet::new(), start_section: StartSection::default(), + start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)), props_expanded: false, history_content: iced::widget::text_editor::Content::new(), status_bar: StatusBar::new(), diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index f3a4e0f3b..59e3d7c16 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -135,6 +135,7 @@ impl OpenCADStudio { self.recent_limit, &self.recent_limit_input, size.width, + self.start_action_w.clone(), self.start_section, ) }) @@ -2191,6 +2192,7 @@ pub(super) fn start_page_view<'a>( recent_limit: usize, recent_limit_input: &'a str, avail_w: f32, + action_width_out: std::sync::Arc, active: super::StartSection, ) -> Element<'a, Message> { let headline = text("Open CAD Studio").size(40).style(start_primary_style); @@ -2243,7 +2245,8 @@ pub(super) fn start_page_view<'a>( donate_btn.into(), ]) .spacing_x(12.0) - .row_h(48.0); + .row_h(48.0) + .report_natural_width(action_width_out.clone()); #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] let mut secondary_items: Vec> = vec![ @@ -2276,34 +2279,14 @@ pub(super) fn start_page_view<'a>( } let secondary_row = WrapFlow::new(secondary_items) .spacing_x(12.0) - .row_h(44.0); - - // Buttons sit on a transparent container with a large, primary-tinted - // ambient shadow (offset = 0, big blur) — produces a soft halo behind - // the action row, matching the Thunderbird coloured-glow look against - // the dark page. - let primary_glow = container(primary_row) - .padding(iced::Padding { - top: 4.0, - right: 8.0, - bottom: 4.0, - left: 8.0, - }) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - shadow: iced::Shadow { - color: theme.extended_palette().primary.base.color.scale_alpha(0.45), - offset: iced::Vector::ZERO, - blur_radius: 80.0, - }, - ..Default::default() - }); + .row_h(44.0) + .report_natural_width(action_width_out.clone()); let content = column![ Space::new().height(iced::Length::Fixed(28.0)), container(headline).center_x(Fill), Space::new().height(iced::Length::Fixed(22.0)), - container(primary_glow).center_x(Fill), + container(primary_row).center_x(Fill), Space::new().height(iced::Length::Fixed(10.0)), container(secondary_row).center_x(Fill), Space::new().height(Fill), @@ -2323,7 +2306,12 @@ pub(super) fn start_page_view<'a>( Compact, } let panel_w = 280.0f32; - let welcome_wide_min = 360.0f32; + const VIDEO_PANEL_PADDING: f32 = 16.0; + const VIDEO_SCROLL_GUTTER: f32 = 14.0; + let measured_action_w = f32::from_bits( + action_width_out.load(std::sync::atomic::Ordering::Relaxed) + ); + let welcome_wide_min = measured_action_w.max(360.0); let avail = (avail_w - 16.0).max(0.0); // minus the page's l/r padding let panel_widths = [panel_w; 4]; let mut panel_visible = [true, true, true, true]; @@ -2371,14 +2359,16 @@ pub(super) fn start_page_view<'a>( // Tutorial-videos rail: the official playlist, fetched at boot (cached on // disk) — thumbnail card + title per video, click opens the browser. let videos_panel: Element<'a, Message> = { - // Inner width = panel 300 − padding 2×16 − scrollbar gutter 14. - const THUMB_H: f32 = (300.0 - 32.0 - 14.0) * 9.0 / 16.0; + // Derive the 16:9 cover box from the actual shared list width so the + // whole thumbnail remains visible when that width changes. + let thumb_h = + (panel_w - VIDEO_PANEL_PADDING * 2.0 - VIDEO_SCROLL_GUTTER) * 9.0 / 16.0; let mut list = column![text("Tutorials").size(15)] .spacing(10) .width(Fill) // Keep the scrollbar off the thumbnails. .padding(iced::Padding { - right: 14.0, + right: VIDEO_SCROLL_GUTTER, ..iced::Padding::ZERO }); for v in videos { @@ -2388,11 +2378,11 @@ pub(super) fn start_page_view<'a>( container( iced::widget::image(handle.clone()) .width(Fill) - .height(iced::Length::Fixed(THUMB_H)) - .content_fit(iced::ContentFit::Cover), + .height(iced::Length::Fixed(thumb_h)) + .content_fit(iced::ContentFit::Contain), ) .width(Fill) - .height(iced::Length::Fixed(THUMB_H)) + .height(iced::Length::Fixed(thumb_h)) .style(|theme: &Theme| container::Style { border: Border { color: theme.extended_palette().background.neutral.color, @@ -2453,7 +2443,7 @@ pub(super) fn start_page_view<'a>( | StartLayout::Compact => iced::Length::Fill, }) .height(Fill) - .padding(16) + .padding(VIDEO_PANEL_PADDING) .style(|theme: &Theme| { let palette = theme.extended_palette(); container::Style { diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index 1ae753095..32dae9b65 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -482,6 +482,9 @@ pub struct WrapFlow<'a> { row_h: f32, /// Align every wrapped row to the right edge of the available width. justify_end: bool, + /// Receives the natural single-row width (as `f32` bits). Multiple flows + /// may share one output; the widest measured value wins. + natural_width_out: Option>, } impl<'a> WrapFlow<'a> { @@ -493,6 +496,7 @@ impl<'a> WrapFlow<'a> { spacing_y: 0.0, row_h: 28.0, justify_end: false, + natural_width_out: None, } } @@ -517,6 +521,11 @@ impl<'a> WrapFlow<'a> { self.justify_end = justify; self } + + pub fn report_natural_width(mut self, out: Arc) -> Self { + self.natural_width_out = Some(out); + self + } } impl<'a> Widget for WrapFlow<'a> { @@ -557,6 +566,11 @@ impl<'a> Widget for WrapFlow<'a> { // bit closer before wrapping. Only fires when max_w is a real bound // (i.e. the flow is already width-constrained, e.g. overflowing tabs). let n = measured.len(); + if let Some(out) = &self.natural_width_out { + let natural_width = + sum_w + n.saturating_sub(1) as f32 * self.spacing_x; + out.fetch_max(natural_width.to_bits(), Ordering::Relaxed); + } let mut eff_gap = self.spacing_x; if self.min_spacing_x < self.spacing_x && max_w.is_finite() && n > 1 { let line_w = sum_w + (n - 1) as f32 * self.spacing_x;