fix(start): collapse panels by available space

Collapse Tutorials, Supporters, then Recent Documents as the panels stop
fitting.

Refs #519
This commit is contained in:
Hakan Seven 2026-07-27 17:57:46 +03:00
commit dce2533767

View file

@ -2607,32 +2607,59 @@ pub(super) fn start_page_view<'a>(
b: 0.085, b: 0.085,
a: 1.0, a: 1.0,
}; };
// Three parts — Recent Files · Welcome · Supporters. They sit side by side // Collapse side panels one at a time as width shrinks: Tutorials first,
// while they fit; when the page is too narrow it becomes a tab bar with the // then Supporters, and Recent Documents last. The previous all-or-nothing
// Welcome tab open by default. // threshold reserved a videos-sized empty margin on both sides of the
// centred Welcome block and hid every panel around 1664 px.
#[derive(Clone, Copy, PartialEq, Eq)]
enum StartLayout {
AllPanels,
WithoutVideos,
RecentAndWelcome,
Compact,
}
let recent_w = 280.0f32; let recent_w = 280.0f32;
let videos_w = 300.0f32; let videos_w = 300.0f32;
let sup_w = 240.0f32; let sup_w = 240.0f32;
let welcome_min = 480.0f32; let welcome_wide_min = 360.0f32;
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
// The videos rail overlays the welcome column's left side, and the welcome let panel_widths = [recent_w, videos_w, sup_w];
// content is CENTRED in that column — so side-by-side only works while the let mut panel_visible = [true, true, true];
// centred `welcome_min` block still leaves a rail-sized gap on its left: let required_width = |visible: &[bool; 3]| {
// the column must fit welcome_min plus a videos_w + spacing margin on BOTH let visible_panels = visible.iter().filter(|&&shown| shown).count();
// sides (centring is symmetric). Below that, the centre content would welcome_wide_min
// slide over the rail — switch to the tabbed layout instead. + panel_widths
let fits_all = avail .iter()
>= recent_w + sup_w + 2.0 * 16.0 + welcome_min + 2.0 * (videos_w + 16.0); .zip(visible)
.filter_map(|(width, shown)| shown.then_some(*width))
.sum::<f32>()
+ visible_panels as f32 * 16.0
};
// Re-measure after every collapse. There are no independent breakpoints:
// the available width and the panels' preferred widths decide the state.
for panel in [1usize, 2, 0] {
if required_width(&panel_visible) <= avail {
break;
}
panel_visible[panel] = false;
}
let start_layout = match panel_visible {
[true, true, true] => StartLayout::AllPanels,
[true, false, true] => StartLayout::WithoutVideos,
[true, false, false] => StartLayout::RecentAndWelcome,
_ => StartLayout::Compact,
};
let recent = recent_files_panel( let recent = recent_files_panel(
recents, recents,
thumbs, thumbs,
recent_limit, recent_limit,
recent_limit_input, recent_limit_input,
if fits_all { match start_layout {
iced::Length::Fixed(recent_w) StartLayout::AllPanels
} else { | StartLayout::WithoutVideos
iced::Length::Fill | StartLayout::RecentAndWelcome => iced::Length::Fixed(recent_w),
StartLayout::Compact => iced::Length::Fill,
}, },
); );
let welcome = container(content).width(Fill).height(Fill); let welcome = container(content).width(Fill).height(Fill);
@ -2723,10 +2750,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(if fits_all { .width(match start_layout {
iced::Length::Fixed(videos_w) StartLayout::AllPanels => iced::Length::Fixed(videos_w),
} else { StartLayout::WithoutVideos
iced::Length::Fill | StartLayout::RecentAndWelcome
| StartLayout::Compact => iced::Length::Fill,
}) })
.height(Fill) .height(Fill)
.padding(16) .padding(16)
@ -2806,10 +2834,12 @@ 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(if fits_all { .width(match start_layout {
iced::Length::Fixed(sup_w) StartLayout::AllPanels | StartLayout::WithoutVideos => {
} else { iced::Length::Fixed(sup_w)
iced::Length::Fill }
StartLayout::RecentAndWelcome
| StartLayout::Compact => iced::Length::Fill,
}) })
.height(Fill) .height(Fill)
.padding(20) .padding(20)
@ -2825,91 +2855,101 @@ pub(super) fn start_page_view<'a>(
.into() .into()
}; };
let body: Element<'a, Message> = if fits_all { let body: Element<'a, Message> = match start_layout {
// The videos rail OVERLAYS the welcome column's empty left side StartLayout::AllPanels => iced::widget::row![
// instead of participating in the row: the welcome content keeps the recent,
// same width and centre as before, so the action buttons don't move. videos_panel,
let base = iced::widget::row![recent, welcome, supporters].spacing(16); welcome,
let rail = container(videos_panel) supporters,
.padding(iced::Padding {
left: recent_w + 16.0,
..iced::Padding::ZERO
})
.height(Fill);
iced::widget::stack![base, rail].into()
} else {
let tab_btn = |label: &'static str, section: super::StartSection| {
let is_active = active == section;
button(text(label).size(14))
.on_press(Message::StartSectionSelect(section))
.padding([8, 18])
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
},
(false, button::Status::Hovered) => Color {
r: 0.14,
g: 0.14,
b: 0.16,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
text_color: if is_active {
TEXT
} else {
Color {
r: 0.62,
g: 0.62,
b: 0.62,
a: 1.0,
}
},
border: Border {
color: if is_active { BRAND } else { Color::TRANSPARENT },
width: if is_active { 1.0 } else { 0.0 },
radius: 6.0.into(),
},
..Default::default()
})
};
let tab_bar = iced::widget::row![
tab_btn("Recent Files", super::StartSection::Recent),
tab_btn("Videos", super::StartSection::Videos),
tab_btn("Welcome", super::StartSection::Welcome),
tab_btn("Supporters", super::StartSection::Supporters),
] ]
.spacing(6); .spacing(16)
let section_body: Element<'a, Message> = match active {
super::StartSection::Recent => container(recent)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
super::StartSection::Videos => container(videos_panel)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
super::StartSection::Welcome => welcome.into(),
super::StartSection::Supporters => container(supporters)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
};
column![
container(tab_bar).center_x(Fill),
Space::new().height(iced::Length::Fixed(12.0)),
section_body,
]
.width(Fill)
.height(Fill) .height(Fill)
.into() .into(),
StartLayout::WithoutVideos => {
iced::widget::row![recent, welcome, supporters]
.spacing(16)
.height(Fill)
.into()
}
StartLayout::RecentAndWelcome => iced::widget::row![recent, welcome]
.spacing(16)
.height(Fill)
.into(),
StartLayout::Compact => {
let tab_btn = |label: &'static str, section: super::StartSection| {
let is_active = active == section;
button(text(label).size(14))
.on_press(Message::StartSectionSelect(section))
.padding([8, 18])
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => Color {
r: 0.18,
g: 0.18,
b: 0.20,
a: 1.0,
},
(false, button::Status::Hovered) => Color {
r: 0.14,
g: 0.14,
b: 0.16,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
text_color: if is_active {
TEXT
} else {
Color {
r: 0.62,
g: 0.62,
b: 0.62,
a: 1.0,
}
},
border: Border {
color: if is_active { BRAND } else { Color::TRANSPARENT },
width: if is_active { 1.0 } else { 0.0 },
radius: 6.0.into(),
},
..Default::default()
})
};
let tab_bar = WrapFlow::new(vec![
tab_btn("Recent Files", super::StartSection::Recent).into(),
tab_btn("Videos", super::StartSection::Videos).into(),
tab_btn("Welcome", super::StartSection::Welcome).into(),
tab_btn("Supporters", super::StartSection::Supporters).into(),
])
.spacing_x(6.0)
.row_h(38.0);
let section_body: Element<'a, Message> = match active {
super::StartSection::Recent => container(recent)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
super::StartSection::Videos => container(videos_panel)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
super::StartSection::Welcome => welcome.into(),
super::StartSection::Supporters => container(supporters)
.width(Fill)
.height(Fill)
.center_x(Fill)
.into(),
};
column![
container(tab_bar).center_x(Fill),
Space::new().height(iced::Length::Fixed(12.0)),
section_body,
]
.width(Fill)
.height(Fill)
.into()
}
}; };
container(body) container(body)