From ddc78f190e5d4c6a21c9d1421b0890932419e7e8 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Tue, 16 Jun 2026 14:07:10 -0400 Subject: [PATCH] fix(home): responsive friends-panel layout (stack vertically when narrow) The home-screen Connect + Friends cards were a fixed-width row, so below ~860px the 380px Connect card squeezed the Friends card until its node-ID field, status, and remove button clipped away. Extract the Connect card into a free `connect_card(state)` fn (mirroring `friends_panel`) and wrap both in `responsive`: side-by-side row at >=900px, stacked column below. Verified at 560/760/1000/1912px. clippy clean, 256 lib tests green. Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 205 +++++++++++++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 75 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 1b91514..7b867a1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,7 +9,7 @@ use crate::theme::{AppTheme, Palette}; use iced::widget::{ container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, radio, tooltip, progress_bar, canvas, Canvas, Column, stack, mouse_area, - rich_text, span, + rich_text, span, responsive, }; use iced::widget::canvas::{Action, Frame, Geometry, Path, Program}; use iced::{ @@ -1206,6 +1206,113 @@ fn vertical_space(height: f32) -> iced::widget::Space { /// the add-by-node-ID form, and the presence posture selector. Self-contained: /// recomputes the palette + the few style helpers it needs so it doesn't depend on /// `view`'s locals. +// The home-screen "Connect" card (left panel): create / join a room. Extracted +// into its own free fn (mirroring `friends_panel`) so the responsive home layout +// can rebuild it per layout pass and place it in either a row or a column. +fn connect_card(state: &AppState) -> Element<'_, AppMessage> { + let pal = state.config.theme.palette(); + let color_crust = pal.crust; + let color_mantle = pal.mantle; + let color_surface = pal.surface; + let color_text = pal.text; + let color_subtext = pal.subtext; + let color_blue = pal.blue; + let color_lavender = pal.lavender; + + let c_style = move |bg: Color, b_color: Color, radius: f32| { + move |_theme: &Theme| container::Style { + text_color: Some(color_text), + background: Some(Background::Color(bg)), + border: Border { + color: b_color, + width: if b_color == Color::TRANSPARENT { 0.0 } else { 1.0 }, + radius: radius.into(), + }, + ..Default::default() + } + }; + let b_style = move |bg: Color, hover_bg: Color, text_c: Color, radius: f32| { + move |_theme: &Theme, status: button::Status| { + let active_bg = match status { + button::Status::Hovered => hover_bg, + _ => bg, + }; + button::Style { + background: Some(Background::Color(active_bg)), + text_color: text_c, + border: Border { color: Color::TRANSPARENT, width: 0.0, radius: radius.into() }, + ..Default::default() + } + } + }; + let t_style = move |_theme: &Theme, _status: text_input::Status| text_input::Style { + background: Background::Color(color_crust), + border: Border { color: color_surface, width: 1.0, radius: 6.0.into() }, + icon: color_subtext, + placeholder: pal.overlay, + value: color_text, + selection: color_blue, + }; + + let logo = text("PEERSPEAK").size(36).color(color_blue); + let subtitle = text("NAT-traversing full-mesh voice chat").size(16).color(color_subtext); + + let nickname_input = column![ + text("Nickname").size(14).color(color_subtext), + vertical_space(4.0), + text_input("Enter nickname...", &state.name) + .on_input(AppMessage::NicknameChanged) + .style(t_style) + .padding(10) + ]; + + let create_btn = button(btn_content(IconKind::Create, "Create New Room", color_crust)) + .on_press(AppMessage::CreatePressed) + .style(b_style(color_blue, color_lavender, color_crust, 8.0)) + .padding(12) + .width(iced::Length::Fill); + + let join_group = column![ + text("Join Existing Room").size(14).color(color_subtext), + vertical_space(4.0), + text_input("Paste room ticket here...", &state.ticket_input) + .on_input(AppMessage::TicketInputChanged) + .style(t_style) + .padding(10), + vertical_space(8.0), + button(text("Join Room").size(16).align_x(iced::alignment::Horizontal::Center)) + .on_press(AppMessage::JoinPressed) + .style(b_style(color_surface, color_blue, color_text, 8.0)) + .padding(12) + .width(iced::Length::Fill) + ]; + + let status = text(&state.status_message).size(14).color(color_subtext); + + container( + column![ + logo, + subtitle, + vertical_space(20.0), + nickname_input, + vertical_space(16.0), + create_btn, + vertical_space(16.0), + text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center), + vertical_space(16.0), + join_group, + vertical_space(10.0), + status + ] + .spacing(10) + .align_x(iced::alignment::Horizontal::Center), + ) + .style(c_style(color_mantle, color_surface, 12.0)) + .padding(30) + .width(380) + .into() +} + fn friends_panel(state: &AppState) -> Element<'_, AppMessage> { let pal = state.config.theme.palette(); let color_crust = pal.crust; @@ -2008,86 +2115,34 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { if state.current_screen == Screen::Home { // --- HOME SCREEN --- - let logo = text("PEERSPEAK") - .size(36) - .color(color_blue); - - let subtitle = text("NAT-traversing full-mesh voice chat") - .size(16) - .color(color_subtext); - - let nickname_input = column![ - text("Nickname").size(14).color(color_subtext), - vertical_space(4.0), - text_input("Enter nickname...", &state.name) - .on_input(AppMessage::NicknameChanged) - .style(t_style) - .padding(10) - ]; - - let create_btn = button(btn_content(IconKind::Create, "Create New Room", color_crust)) - .on_press(AppMessage::CreatePressed) - .style(b_style(color_blue, color_lavender, color_crust, 8.0)) - .padding(12) - .width(iced::Length::Fill); - - let join_group = column![ - text("Join Existing Room").size(14).color(color_subtext), - vertical_space(4.0), - text_input("Paste room ticket here...", &state.ticket_input) - .on_input(AppMessage::TicketInputChanged) - .style(t_style) - .padding(10), - vertical_space(8.0), - button( - text("Join Room") - .size(16) + // Two cards: Connect (left) + the live Friends list (right). They sit + // side-by-side when the window is wide enough, and stack vertically on a + // narrow window so the Friends card never gets crushed — below ~860px the + // fixed-width Connect card would otherwise squeeze it until its node-ID + // field and remove button clip away. `responsive` measures the available + // width each layout pass and picks the orientation accordingly. + let body = responsive(move |size| { + let cards: Element = if size.width < 900.0 { + column![connect_card(state), friends_panel(state)] + .spacing(20) .align_x(iced::alignment::Horizontal::Center) - ) - .on_press(AppMessage::JoinPressed) - .style(b_style(color_surface, color_blue, color_text, 8.0)) - .padding(12) - .width(iced::Length::Fill) - ]; - - let status = text(&state.status_message) - .size(14) - .color(color_subtext); - - let content = container( - column![ - logo, - subtitle, - vertical_space(20.0), - nickname_input, - vertical_space(16.0), - create_btn, - vertical_space(16.0), - text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center), - vertical_space(16.0), - join_group, - vertical_space(10.0), - status - ] - .spacing(10) - .align_x(iced::alignment::Horizontal::Center) - ) - .style(c_style(color_mantle, color_surface, 12.0)) - .padding(30) - .width(380); - - // Two side-by-side cards: connect (left) + the live friends list (right), - // centered together. The friends panel was moved here from Settings. - let panels = row![content, friends_panel(state)] - .spacing(20) - .align_y(iced::alignment::Vertical::Top); - let scroll = scrollable(container(panels).center_x(iced::Length::Fill)); + .into() + } else { + row![connect_card(state), friends_panel(state)] + .spacing(20) + .align_y(iced::alignment::Vertical::Top) + .into() + }; + scrollable(container(cards).center_x(iced::Length::Fill)) + .width(iced::Length::Fill) + .into() + }); let home = container( column![ top_bar, vertical_space(20.0), - scroll + body ].align_x(iced::alignment::Horizontal::Center) ) .width(iced::Length::Fill)