forked from 1b-agent/opencadstudio-upstream
fix(start): refine responsive page layout
This commit is contained in:
1 parent
13123d5b34
commit
a30f72f627
3 files changed
+40
-32
No files matched your search
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user