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 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 22:33:06 -04:00
co-authored by Claude Opus 4.8
parent 2b52a48efe
commit 3edac3f8d1
+78 -51
View File
@@ -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<Element<'_, AppMessage>> =
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)
)