fix(start): refine responsive page layout

This commit is contained in:
Hakan Seven committed 2026-07-28 12:48:39 +03:00
1 parent 13123d5b34
commit a30f72f627
3 files changed
+40 -32

No files matched your search

+4
View File
@@ -271,6 +271,9 @@ pub(super) struct OpenCADStudio {
/// Which Start-page section is shown when the page is too narrow for all /// 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. /// three side by side and falls back to a tab bar.
start_section: StartSection, 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<std::sync::atomic::AtomicU32>,
/// When the window is too narrow the properties panel collapses to a /// 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. /// vertical bar; this is the user's toggle to expand it back out.
props_expanded: bool, props_expanded: bool,
@@ -2494,6 +2497,7 @@ impl OpenCADStudio {
discussions_loading: false, discussions_loading: false,
props_asym_scale: std::collections::HashSet::new(), props_asym_scale: std::collections::HashSet::new(),
start_section: StartSection::default(), start_section: StartSection::default(),
start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
props_expanded: false, props_expanded: false,
history_content: iced::widget::text_editor::Content::new(), history_content: iced::widget::text_editor::Content::new(),
status_bar: StatusBar::new(), status_bar: StatusBar::new(),
+22 -32
View File
@@ -135,6 +135,7 @@ impl OpenCADStudio {
self.recent_limit, self.recent_limit,
&self.recent_limit_input, &self.recent_limit_input,
size.width, size.width,
self.start_action_w.clone(),
self.start_section, self.start_section,
) )
}) })
@@ -2191,6 +2192,7 @@ pub(super) fn start_page_view<'a>(
recent_limit: usize, recent_limit: usize,
recent_limit_input: &'a str, recent_limit_input: &'a str,
avail_w: f32, avail_w: f32,
action_width_out: std::sync::Arc<std::sync::atomic::AtomicU32>,
active: super::StartSection, active: super::StartSection,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let headline = text("Open CAD Studio").size(40).style(start_primary_style); 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(), donate_btn.into(),
]) ])
.spacing_x(12.0) .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))] #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut secondary_items: Vec<Element<'a, Message>> = vec![ let mut secondary_items: Vec<Element<'a, Message>> = vec![
@@ -2276,34 +2279,14 @@ pub(super) fn start_page_view<'a>(
} }
let secondary_row = WrapFlow::new(secondary_items) let secondary_row = WrapFlow::new(secondary_items)
.spacing_x(12.0) .spacing_x(12.0)
.row_h(44.0); .row_h(44.0)
.report_natural_width(action_width_out.clone());
// 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()
});
let content = column![ let content = column![
Space::new().height(iced::Length::Fixed(28.0)), Space::new().height(iced::Length::Fixed(28.0)),
container(headline).center_x(Fill), container(headline).center_x(Fill),
Space::new().height(iced::Length::Fixed(22.0)), 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)), Space::new().height(iced::Length::Fixed(10.0)),
container(secondary_row).center_x(Fill), container(secondary_row).center_x(Fill),
Space::new().height(Fill), Space::new().height(Fill),
@@ -2323,7 +2306,12 @@ pub(super) fn start_page_view<'a>(
Compact, Compact,
} }
let panel_w = 280.0f32; 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 avail = (avail_w - 16.0).max(0.0); // minus the page's l/r padding
let panel_widths = [panel_w; 4]; let panel_widths = [panel_w; 4];
let mut panel_visible = [true, true, true, true]; 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 // Tutorial-videos rail: the official playlist, fetched at boot (cached on
// disk) — thumbnail card + title per video, click opens the browser. // disk) — thumbnail card + title per video, click opens the browser.
let videos_panel: Element<'a, Message> = { let videos_panel: Element<'a, Message> = {
// Inner width = panel 300 − padding 2×16 − scrollbar gutter 14. // Derive the 16:9 cover box from the actual shared list width so the
const THUMB_H: f32 = (300.0 - 32.0 - 14.0) * 9.0 / 16.0; // 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)] let mut list = column![text("Tutorials").size(15)]
.spacing(10) .spacing(10)
.width(Fill) .width(Fill)
// Keep the scrollbar off the thumbnails. // Keep the scrollbar off the thumbnails.
.padding(iced::Padding { .padding(iced::Padding {
right: 14.0, right: VIDEO_SCROLL_GUTTER,
..iced::Padding::ZERO ..iced::Padding::ZERO
}); });
for v in videos { for v in videos {
@@ -2388,11 +2378,11 @@ pub(super) fn start_page_view<'a>(
container( container(
iced::widget::image(handle.clone()) iced::widget::image(handle.clone())
.width(Fill) .width(Fill)
.height(iced::Length::Fixed(THUMB_H)) .height(iced::Length::Fixed(thumb_h))
.content_fit(iced::ContentFit::Cover), .content_fit(iced::ContentFit::Contain),
) )
.width(Fill) .width(Fill)
.height(iced::Length::Fixed(THUMB_H)) .height(iced::Length::Fixed(thumb_h))
.style(|theme: &Theme| container::Style { .style(|theme: &Theme| container::Style {
border: Border { border: Border {
color: theme.extended_palette().background.neutral.color, color: theme.extended_palette().background.neutral.color,
@@ -2453,7 +2443,7 @@ pub(super) fn start_page_view<'a>(
| StartLayout::Compact => iced::Length::Fill, | StartLayout::Compact => iced::Length::Fill,
}) })
.height(Fill) .height(Fill)
.padding(16) .padding(VIDEO_PANEL_PADDING)
.style(|theme: &Theme| { .style(|theme: &Theme| {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Style { container::Style {
+14
View File
@@ -482,6 +482,9 @@ pub struct WrapFlow<'a> {
row_h: f32, row_h: f32,
/// Align every wrapped row to the right edge of the available width. /// Align every wrapped row to the right edge of the available width.
justify_end: bool, 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<Arc<AtomicU32>>,
} }
impl<'a> WrapFlow<'a> { impl<'a> WrapFlow<'a> {
@@ -493,6 +496,7 @@ impl<'a> WrapFlow<'a> {
spacing_y: 0.0, spacing_y: 0.0,
row_h: 28.0, row_h: 28.0,
justify_end: false, justify_end: false,
natural_width_out: None,
} }
} }
@@ -517,6 +521,11 @@ impl<'a> WrapFlow<'a> {
self.justify_end = justify; self.justify_end = justify;
self self
} }
pub fn report_natural_width(mut self, out: Arc<AtomicU32>) -> Self {
self.natural_width_out = Some(out);
self
}
} }
impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> { impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
@@ -557,6 +566,11 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
// bit closer before wrapping. Only fires when max_w is a real bound // 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). // (i.e. the flow is already width-constrained, e.g. overflowing tabs).
let n = measured.len(); 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; let mut eff_gap = self.spacing_x;
if self.min_spacing_x < self.spacing_x && max_w.is_finite() && n > 1 { 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; let line_w = sum_w + (n - 1) as f32 * self.spacing_x;