From 3edac3f8d12968e9a7c0256ecbbec18a4402b7e1 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sat, 13 Jun 2026 22:33:06 -0400 Subject: [PATCH] feat(ui): category headers on the Settings page Reorganize Settings into six clearly-headed categories with a reusable section_header (color_blue size-16 title + thin full-width divider): Audio Devices, Microphone, Network & Privacy, Room Layout, Theme, Notifications & Sounds. Splits the old shared Mic|Network row, drops the inconsistent inline size-14 sublabels, and removes the hardcoded "Theme" title from theme_section so it's rendered by the same header helper as every other section. Left-aligns the content (was centered) so headers, dividers, and hints line up. Presentational only; no message/logic changes. User-verified live. Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 129 ++++++++++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 9f5b109..42c8e7c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1144,8 +1144,9 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { AppTheme::ALL[0..5].iter().map(|&t| theme_choice(t)).collect(); let theme_row2: Vec> = AppTheme::ALL[5..].iter().map(|&t| theme_choice(t)).collect(); + // Title comes from the "Theme" section header (added below), so this body + // is just the swatch rows + hint. let theme_section = column![ - text("Theme").size(14).color(color_subtext), iced::widget::Row::with_children(theme_row1).spacing(12), iced::widget::Row::with_children(theme_row2).spacing(12), text("Colour theme for the whole UI. Applies live.") @@ -1155,10 +1156,28 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { .spacing(10) .width(iced::Length::Fill); + // Reusable category header: a coloured title with a thin full-width + // divider beneath, so each class of settings reads as its own section. + let section_header = |title: &'static str| -> Element<'_, AppMessage> { + column![ + text(title).size(16).color(color_blue), + container(text("")) + .width(iced::Length::Fill) + .height(iced::Length::Fixed(1.0)) + .style(c_style(color_surface, Color::TRANSPARENT, 0.0)), + ] + .spacing(6) + .width(iced::Length::Fill) + .into() + }; + + // Spacing between one category and the next. + let section_gap = 18.0; + let settings_content = scrollable( column![ - text("Device Settings").size(14).color(color_subtext), - vertical_space(6.0), + // --- Audio Devices --- + section_header("Audio Devices"), row![ column![ text("Input Device").size(12).color(color_subtext), @@ -1185,32 +1204,37 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { .on_release(AppMessage::PersistConfig), ].spacing(4).width(iced::Length::Fill), ].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill), - vertical_space(12.0), - row![ - column![ - text("Mic Level & Noise Gate").size(14).color(color_subtext), - mic_meter, - text("Drag the yellow handle to set the gate. While talking, place it just above your quiet-room level so silence is muted but your voice passes through.").size(11).color(color_subtext), - vertical_space(4.0), - checkbox(state.config.echo_cancellation_enabled) - .label("Echo cancellation") - .on_toggle(AppMessage::ToggleEchoCancellation), - text("Cancels speaker echo + suppresses noise (PipeWire). Takes effect on your next room join.").size(11).color(color_subtext), - ].spacing(8).width(iced::Length::Fill), - column![ - text("Network Privacy").size(14).color(color_subtext), - pick_list( - &NetworkMode::ALL[..], - Some(state.config.network_mode), - AppMessage::NetworkModeSelected, - ).width(iced::Length::Fill), - text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext), - text("Takes effect on your next room join.").size(11).color(color_surface), - ].spacing(4).width(iced::Length::Fill), - ].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill), - vertical_space(12.0), + vertical_space(section_gap), + + // --- Microphone --- + section_header("Microphone"), + column![ + mic_meter, + text("Drag the yellow handle to set the gate. While talking, place it just above your quiet-room level so silence is muted but your voice passes through.").size(11).color(color_subtext), + vertical_space(4.0), + checkbox(state.config.echo_cancellation_enabled) + .label("Echo cancellation") + .on_toggle(AppMessage::ToggleEchoCancellation), + text("Cancels speaker echo + suppresses noise (PipeWire). Takes effect on your next room join.").size(11).color(color_subtext), + ].spacing(8).width(iced::Length::Fill), + vertical_space(section_gap), + + // --- Network & Privacy --- + section_header("Network & Privacy"), + column![ + pick_list( + &NetworkMode::ALL[..], + Some(state.config.network_mode), + AppMessage::NetworkModeSelected, + ).width(iced::Length::Fill), + text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext), + text("Takes effect on your next room join.").size(11).color(color_subtext), + ].spacing(4).width(iced::Length::Fill), + vertical_space(section_gap), + + // --- Room Layout --- + section_header("Room Layout"), column![ - text("Room Layout").size(14).color(color_subtext), row![ layout_choice(RoomLayout::ThreeColumn, "3-Column"), layout_choice(RoomLayout::BottomDock, "Bottom Dock"), @@ -1218,36 +1242,39 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { ].spacing(16), text("How the in-call room is arranged. Applies live.").size(11).color(color_subtext), ].spacing(8).width(iced::Length::Fill), - vertical_space(12.0), + vertical_space(section_gap), + + // --- Theme --- + section_header("Theme"), theme_section, - vertical_space(12.0), + vertical_space(section_gap), + + // --- Notifications & Sounds --- + section_header("Notifications & Sounds"), column![ - text("Notification Chimes").size(14).color(color_subtext), checkbox(state.config.notifications_enabled) .label("Enable sound notifications") .on_toggle(AppMessage::ToggleNotifications), + vertical_space(6.0), + text("Custom chime files (WAV paths) — leave blank for the built-in sounds.").size(12).color(color_subtext), + row![ + path_field("Self Join", Sound::SelfJoin), + path_field("Peer Join", Sound::PeerJoin), + ].spacing(20).width(iced::Length::Fill), + row![ + path_field("Self Leave", Sound::SelfLeave), + path_field("Peer Leave", Sound::PeerLeave), + ].spacing(20).width(iced::Length::Fill), + row![ + path_field("Reconnect Attempt", Sound::ReconnectAttempt), + path_field("Reconnected", Sound::Reconnected), + ].spacing(20).width(iced::Length::Fill), + row![ + path_field("Mic Toggle", Sound::MicToggle), + path_field("Reconnect Failed", Sound::ReconnectFailed), + ].spacing(20).width(iced::Length::Fill), ].spacing(8).width(iced::Length::Fill), - vertical_space(12.0), - text("Custom Chime Files (WAV Paths)").size(14).color(color_subtext), - - row![ - path_field("Self Join", Sound::SelfJoin), - path_field("Peer Join", Sound::PeerJoin), - ].spacing(20).width(iced::Length::Fill), - row![ - path_field("Self Leave", Sound::SelfLeave), - path_field("Peer Leave", Sound::PeerLeave), - ].spacing(20).width(iced::Length::Fill), - row![ - path_field("Reconnect Attempt", Sound::ReconnectAttempt), - path_field("Reconnected", Sound::Reconnected), - ].spacing(20).width(iced::Length::Fill), - row![ - path_field("Mic Toggle", Sound::MicToggle), - path_field("Reconnect Failed", Sound::ReconnectFailed), - ].spacing(20).width(iced::Length::Fill), ] - .align_x(iced::alignment::Horizontal::Center) .spacing(10) .width(iced::Length::Fill) )