3 Commits
Author SHA1 Message Date
molluskandClaude Opus 4.8 c0c1969332 style(music): theme-colored skip glyphs in player bar + drawer
windows-build / windows-build (push) Has been cancelled
cargo-deny / cargo-deny (push) Has been cancelled
The skip-back/forward buttons rendered orange in every theme because the
emoji glyphs ⏮/⏭ (U+23EE/U+23ED) are drawn by the system color-emoji font,
which ignores the button's text color. Replace them with |◀ / ▶| built from
the text-presentation triangles ◀/▶ (U+25C0/U+25B6) — the same family the
play button already uses — so they honor .color() and follow the active
theme like the play button does. Applies to both the now-playing player bar
and the full music drawer panel. Pure visual change; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:33:32 -04:00
d155091eed style(music): center the transport controls on the player bar
The transport buttons were pushed to the far right because the
now-playing label had width(Fill). Regroup the bar into three sections
— left(Fill) identity+label, centered transport, right(Fill) position +
expand — so the controls sit in the middle. Pure widget regrouping; no
message, config, or behavior change.

Implemented by Codex (gpt-5.5), reviewed + gates re-run by Claude.

Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:07:59 -04:00
b553a94875 feat(music): relocate playlist into a now-playing bar + slide-out drawer
The full music panel was rendered inline at all times (its own card in
the 3-column layout, stuffed into the Controls panel otherwise), which
crowded every layout. Move it behind two toggles:

- A room-only ♪ button in the top bar shows/hides a slim 56px
  now-playing player bar (track + ⏮ ⏸/▶ ⏭ + position + expand). The
  preference persists (AppConfig.show_player_bar).
- The bar's ⤢ button opens the full panel in a resizable right-edge
  drawer (DividerKind::PlaylistDrawer, mirrors the Chat drawer). When
  open, body_w shrinks so the layouts' fixed panels don't overflow.

Removes all inline playlist placement (3-col card + ThreeColPlaylist
divider, ctrl_music block) and the now-dead clamp/consts. Pure
now_playing_label seam + drawer-width clamp test. 474 lib tests, clippy
-D warnings clean, release build green.

Implemented by Codex (gpt-5.5), reviewed + gates re-run by Claude.

Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:59:49 -04:00
2 changed files with 251 additions and 77 deletions
+233 -77
View File
@@ -237,15 +237,15 @@ pub enum DividerKind {
/// Horizontal divider between the main row and the Chat dock (resizes the
/// Chat dock height).
Chat,
/// Horizontal divider between Chat and the standalone Playlist card in the
/// 3-column layout (resizes the Playlist card height).
ThreeColPlaylist,
/// Vertical divider between Chat and Controls in the 3-column layout (resizes
/// the Controls panel width).
Controls,
/// Vertical divider on the left edge of the Chat drawer (resizes the drawer
/// width) in the drawer layout.
ChatDrawer,
/// Vertical divider on the left edge of the Playlist drawer (resizes the
/// drawer width).
PlaylistDrawer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -264,11 +264,6 @@ const CHAT_MIN_H: f32 = 110.0;
/// Minimum height reserved above the Chat dock (header + main row) when resizing
/// the dock (px).
const ABOVE_CHAT_MIN_H: f32 = 300.0;
/// Minimum height of the standalone Playlist card in the 3-column layout (px).
const THREECOL_PLAYLIST_MIN_H: f32 = 150.0;
/// Minimum height reserved for the Chat above the Playlist card in the 3-column
/// layout when resizing the card (px).
const THREECOL_CHAT_MIN_H: f32 = 160.0;
/// Thickness of a draggable divider (px).
const DIVIDER_THICKNESS: f32 = 8.0;
/// Upper bound for waiting on orderly core shutdown before letting the window exit.
@@ -290,13 +285,6 @@ fn clamp_chat_height(height: f32, window_h: f32) -> f32 {
height.clamp(CHAT_MIN_H, max)
}
/// Clamp the 3-column Playlist card height so neither it nor the Chat above it
/// drops below its minimum, given the current window height.
fn clamp_threecol_playlist_height(height: f32, window_h: f32) -> f32 {
let max = (window_h - THREECOL_CHAT_MIN_H).max(THREECOL_PLAYLIST_MIN_H);
height.clamp(THREECOL_PLAYLIST_MIN_H, max)
}
/// Minimum width of the Chat column / drawer (px).
const CHAT_MIN_W: f32 = 200.0;
@@ -315,6 +303,13 @@ fn clamp_chat_drawer_width(width: f32, window_w: f32) -> f32 {
width.clamp(CHAT_MIN_W, max)
}
/// Clamp the Playlist drawer width so neither it nor the room body drops below
/// its minimum, given the current window width.
fn clamp_playlist_drawer_width(width: f32, window_w: f32) -> f32 {
let max = (window_w - PARTICIPANTS_MIN_W - CONTROLS_MIN_W).max(CHAT_MIN_W);
width.clamp(CHAT_MIN_W, max)
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum AppMessage {
@@ -444,6 +439,10 @@ pub enum AppMessage {
MusicSetVolume(f32),
/// Set the tuned-in source's music playback volume (and persist it).
MusicSetSourceVolume(f32),
/// Show / hide the room now-playing player bar (persisted).
TogglePlayerBar,
/// Open / close the full playlist drawer.
TogglePlaylistDrawer,
/// Remove the playlist track at this index.
MusicRemove(usize),
/// Move a personal playlist track one slot up/down.
@@ -754,6 +753,8 @@ pub struct AppState {
clock_skew_warning: Option<ClockSkewBanner>,
/// Whether the Chat drawer is open (drawer layout only).
drawer_chat_open: bool,
/// Whether the Playlist drawer is open beside the room body.
playlist_drawer_open: bool,
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
mic_level: f32,
/// Whether the standalone (off-call) mic test stream is running.
@@ -871,10 +872,10 @@ impl Default for AppState {
config.participants_width =
clamp_participants_width(config.participants_width, ww);
config.chat_height = clamp_chat_height(config.chat_height, wh);
config.threecol_playlist_height =
clamp_threecol_playlist_height(config.threecol_playlist_height, wh);
config.controls_width = clamp_controls_width(config.controls_width, ww);
config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, ww);
config.playlist_drawer_width =
clamp_playlist_drawer_width(config.playlist_drawer_width, ww);
notify::set_enabled(config.notifications_enabled);
for sound in Sound::ALL {
notify::set_sound_enabled(sound, config.sound_enabled(sound));
@@ -1003,6 +1004,7 @@ impl Default for AppState {
share_app_audio_supported: true,
clock_skew_warning: None,
drawer_chat_open: false,
playlist_drawer_open: false,
mic_level: 0.0,
mic_test_active: false,
connecting: HashSet::new(),
@@ -2056,14 +2058,6 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.window_size.height,
);
}
DividerKind::ThreeColPlaylist => {
// The Playlist card sits at the bottom of the middle column; dragging the
// divider down (positive delta) gives Chat more room and shrinks the card.
state.config.threecol_playlist_height = clamp_threecol_playlist_height(
state.config.threecol_playlist_height - delta,
state.window_size.height,
);
}
DividerKind::Controls => {
// Controls sits on the right; dragging the divider right (positive
// delta) gives Chat more room and shrinks Controls.
@@ -2080,6 +2074,14 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.window_size.width,
);
}
DividerKind::PlaylistDrawer => {
// The drawer sits on the right; dragging its left-edge divider
// left (negative delta) widens the drawer.
state.config.playlist_drawer_width = clamp_playlist_drawer_width(
state.config.playlist_drawer_width - delta,
state.window_size.width,
);
}
}
}
AppMessage::OpenLayoutPicker => {
@@ -2350,6 +2352,16 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
AppMessage::ToggleDrawerChat => {
state.drawer_chat_open = !state.drawer_chat_open;
}
AppMessage::TogglePlayerBar => {
state.config.show_player_bar = !state.config.show_player_bar;
if !state.config.show_player_bar {
state.playlist_drawer_open = false;
}
state.config.save();
}
AppMessage::TogglePlaylistDrawer => {
state.playlist_drawer_open = !state.playlist_drawer_open;
}
AppMessage::ChatSubmit => {
let text = sanitize_chat(&state.chat_input);
if !text.is_empty() {
@@ -2783,12 +2795,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
clamp_participants_width(state.config.participants_width, size.width);
state.config.chat_height =
clamp_chat_height(state.config.chat_height, size.height);
state.config.threecol_playlist_height =
clamp_threecol_playlist_height(state.config.threecol_playlist_height, size.height);
state.config.controls_width =
clamp_controls_width(state.config.controls_width, size.width);
state.config.chat_drawer_width =
clamp_chat_drawer_width(state.config.chat_drawer_width, size.width);
state.config.playlist_drawer_width =
clamp_playlist_drawer_width(state.config.playlist_drawer_width, size.width);
}
AppMessage::EventOccurred(Event::Window(iced::window::Event::Moved(position))) => {
// Remember the position in-memory; written to disk once on close.
@@ -3121,6 +3133,15 @@ fn can_broadcast_music(state: &AppState) -> bool {
state.music_broadcasting && state.music_listening_to.is_none()
}
/// Text shown on the now-playing player bar. Pure for testing.
fn now_playing_label(listening_to: Option<&str>, current_track: Option<&str>) -> String {
match (listening_to, current_track) {
(Some(peer), _) => format!("Tuned in to {peer}"),
(None, Some(track)) => track.to_string(),
(None, None) => "Nothing playing".to_string(),
}
}
fn stop_music_broadcast(state: &mut AppState) {
let old_current = state.music_broadcast_id.take();
let old_next = state.music_broadcast_next.take().map(|(_, id, _)| id);
@@ -4015,7 +4036,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
};
// The Hotkeys info button is always available (hotkeys are app-wide). The
// room-layout button is hidden on the Home screen, leaving only it + Settings.
// room-only player button is hidden off-room; the layout button is hidden on Home.
let info_button = tooltip(
button(icon(IconKind::Info, 18.0, color_text))
.on_press(AppMessage::OpenHotkeyInfo)
@@ -4028,6 +4049,27 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
)
.gap(8);
let player_bar_button: Element<'_, AppMessage> =
if state.current_screen == Screen::Room {
let active = state.config.show_player_bar;
let bg = if active { color_blue } else { color_surface };
let fg = if active { color_crust } else { color_text };
tooltip(
button(text("").size(15))
.on_press(AppMessage::TogglePlayerBar)
.style(b_style(bg, color_blue, fg, 6.0))
.padding(8),
container(text("Player bar").size(11).color(color_text))
.padding(8)
.style(c_style(color_crust, color_surface, 6.0)),
iced::widget::tooltip::Position::Bottom,
)
.gap(8)
.into()
} else {
iced::widget::Space::new().width(0.0).height(0.0).into()
};
let layout_button: Element<'_, AppMessage> = if state.current_screen == Screen::Home {
iced::widget::Space::new().width(0.0).height(0.0).into()
} else {
@@ -4051,6 +4093,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
let top_bar = row![
horizontal_space(),
player_bar_button,
info_button,
layout_button,
button(
@@ -5524,7 +5567,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.unwrap_or_else(|| "--:--".to_string());
column![
row![
button(text("").size(13))
button(text("|◀").size(13))
.on_press(AppMessage::MusicPrev)
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding(7),
@@ -5532,7 +5575,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.on_press(AppMessage::MusicPlayPause)
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
.padding(7),
button(text("").size(13))
button(text("▶|").size(13))
.on_press(AppMessage::MusicNext)
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding(7),
@@ -5664,37 +5707,83 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.width(iced::Length::Fill)
.into()
};
// W22: in the 3-column layout the Playlist gets its own card stacked under Chat
// (built below in the ThreeColumn body arm); in every other layout it stays in
// the Controls panel. `music_panel` is consumed by exactly one of these.
let three_col = matches!(state.config.room_layout, RoomLayout::ThreeColumn);
let (ctrl_music, playlist_card): (Element<'_, AppMessage>, Option<Element<'_, AppMessage>>) =
if three_col {
let card = container(
column![
text("Playlist").size(18).color(color_blue),
vertical_space(8.0),
scrollable(music_panel)
.width(iced::Length::Fill)
.height(iced::Length::Fill),
let music_bar_status = status_snapshot(&state.music_status);
let music_bar_playing = music_bar_status.playing_id.is_some();
let music_bar_play_label = if music_bar_playing && music_bar_status.paused {
""
} else if music_bar_playing {
""
} else {
""
};
let listening_peer_name = state
.music_listening_to
.and_then(|peer| state.peers.get(&peer).map(|p| p.name.as_str()));
let current_track_name = state
.music_current
.and_then(|i| state.music_playlist.get(i).map(|track| track.name.as_str()));
let elapsed = format_clip_time(music_bar_status.position);
let duration = music_bar_status
.total
.map(format_clip_time)
.unwrap_or_else(|| "--:--".to_string());
let player_bar = container(
row![
container(
row![
text("").size(18).color(color_blue),
text(now_playing_label(listening_peer_name, current_track_name))
.size(13)
.color(color_text),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center)
)
.width(iced::Length::Fill),
row![
button(text("|◀").size(13))
.on_press(AppMessage::MusicPrev)
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding(7),
button(text(music_bar_play_label).size(13))
.on_press(AppMessage::MusicPlayPause)
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
.padding(7),
button(text("▶|").size(13))
.on_press(AppMessage::MusicNext)
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding(7),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center),
container(
row![
text(format!("{elapsed} / {duration}"))
.size(11)
.color(color_subtext),
button(text(if state.playlist_drawer_open { "" } else { "" }).size(13))
.on_press(AppMessage::TogglePlaylistDrawer)
.style(b_style(
if state.playlist_drawer_open { color_blue } else { color_surface },
color_blue,
if state.playlist_drawer_open { color_crust } else { color_text },
6.0,
))
.padding(7),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center)
)
.style(c_style(color_mantle, color_surface, 8.0))
.padding(12)
.width(iced::Length::Fill)
.height(iced::Length::Fill);
(column![].into(), Some(card.into()))
} else {
(
column![
vertical_space(20.0),
text("Playlist").size(18).color(color_blue),
music_panel,
]
.into(),
None,
)
};
.align_x(iced::alignment::Horizontal::Right),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center)
)
.style(c_style(color_mantle, color_surface, 8.0))
.padding(10)
.width(iced::Length::Fill)
.height(iced::Length::Fixed(56.0));
let ctrl_buttons = column![
button(btn_content(mute_kind, mute_text, mute_fg))
.on_press(AppMessage::ToggleMutePressed)
@@ -5798,7 +5887,6 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.padding(14)
.width(iced::Length::Fill)
},
ctrl_music,
];
// Leave is the exit control, so it's pinned below the scrolling controls
@@ -6112,13 +6200,35 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.height(iced::Length::Fixed(DIVIDER_THICKNESS))
};
let playlist_drawer = container(
column![
text("Playlist").size(18).color(color_blue),
vertical_space(8.0),
scrollable(music_panel)
.width(iced::Length::Fill)
.height(iced::Length::Fill),
]
.spacing(0)
)
.style(c_style(color_mantle, color_surface, 8.0))
.padding(12)
.width(iced::Length::Fixed(state.config.playlist_drawer_width))
.height(iced::Length::Fill);
let drawer_open = state.playlist_drawer_open && state.config.show_player_bar;
let body_w = if drawer_open {
state.window_size.width - state.config.playlist_drawer_width - DIVIDER_THICKNESS
} else {
state.window_size.width
};
// Assemble the body per the chosen room layout. `chat_inner` is moved into
// exactly one arm (allowed across mutually-exclusive match arms).
let pw = state.config.participants_width;
let body: Element<'_, AppMessage> = match state.config.room_layout {
RoomLayout::BottomDock => {
// Cap Participants so the Fill Controls panel keeps its minimum.
let avail = state.window_size.width - 30.0;
let avail = body_w - 30.0;
let pwb = pw.min((avail - CONTROLS_MIN_W - DIVIDER_THICKNESS).max(PARTICIPANTS_MIN_W));
let main = row![
peers_panel.width(iced::Length::Fixed(pwb)),
@@ -6135,7 +6245,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
column![main, hdiv(DividerKind::Chat), chat].into()
}
RoomLayout::ThreeColumn => {
let avail = state.window_size.width - 30.0; // outer padding
let avail = body_w - 30.0; // outer padding
let pw3 = pw.min(
(avail - state.config.controls_width - CHAT_MIN_W - 2.0 * DIVIDER_THICKNESS)
.max(PARTICIPANTS_MIN_W),
@@ -6145,19 +6255,10 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.padding(12)
.width(iced::Length::Fill)
.height(iced::Length::Fill);
// The Playlist card was built above iff this is the 3-column layout.
let card = playlist_card.expect("playlist_card is Some for ThreeColumn");
let middle = column![
chat,
hdiv(DividerKind::ThreeColPlaylist),
container(card).height(iced::Length::Fixed(state.config.threecol_playlist_height)),
]
.width(iced::Length::Fill)
.height(iced::Length::Fill);
row![
peers_panel.width(iced::Length::Fixed(pw3)),
vdiv(DividerKind::Panels),
middle,
chat,
vdiv(DividerKind::Controls),
control_panel.width(iced::Length::Fixed(state.config.controls_width)),
]
@@ -6166,7 +6267,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.into()
}
RoomLayout::Drawer => {
let avail = state.window_size.width - 30.0;
let avail = body_w - 30.0;
if state.drawer_chat_open {
// Participants + Chat drawer are both fixed; cap Participants so
// the Fill Controls panel between them keeps its minimum.
@@ -6238,9 +6339,30 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
iced::widget::Space::new().width(0.0).height(0.0).into()
};
let room = container(
column![top_bar, header_container, clock_skew_banner, vertical_space(12.0), body]
)
let main_area = column![
header_container,
clock_skew_banner,
vertical_space(12.0),
body,
];
let main_with_bar: Element<'_, AppMessage> = if state.config.show_player_bar {
column![main_area.height(iced::Length::Fill), player_bar].into()
} else {
main_area.into()
};
let inner: Element<'_, AppMessage> = if drawer_open {
row![
column![main_with_bar].width(iced::Length::Fill),
vdiv(DividerKind::PlaylistDrawer),
playlist_drawer,
]
.height(iced::Length::Fill)
.into()
} else {
main_with_bar
};
let room = container(column![top_bar, inner])
.padding(15)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
@@ -7653,8 +7775,8 @@ impl Program<AppMessage> for Icon {
mod tests {
use super::{
attachment_default_name, clear_expired_clock_skew_warning, format_clock_skew_duration,
format_duration, format_relative_ago, initial_window_position, reconnect_attempt_chime, reconnected_chime,
set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, update, AppConfig,
format_duration, format_relative_ago, initial_window_position, now_playing_label,
reconnect_attempt_chime, reconnected_chime, set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, update, AppConfig,
AppMessage, AppState, AttachmentCache, AttachmentState, ChatEntry, ClockSkewBanner,
GateMeter, METER_MAX, UiEvent, CLOCK_SKEW_WARNING_VISIBLE_SECS,
};
@@ -8330,6 +8452,40 @@ mod tests {
assert!(d.is_finite() && d >= CHAT_MIN_W);
}
#[test]
fn playlist_drawer_width_clamps() {
use super::{clamp_playlist_drawer_width, CHAT_MIN_W, CONTROLS_MIN_W, PARTICIPANTS_MIN_W};
let window_w = 1000.0;
let max = window_w - PARTICIPANTS_MIN_W - CONTROLS_MIN_W;
// Mid-range passes through.
assert_eq!(clamp_playlist_drawer_width(320.0, window_w), 320.0);
// Below minimum snaps up.
assert_eq!(clamp_playlist_drawer_width(10.0, window_w), CHAT_MIN_W);
// Above maximum leaves the rest of the room at its reserved width.
assert_eq!(clamp_playlist_drawer_width(800.0, window_w), max);
// Tiny window stays finite and at/above the minimum (no inverted range).
let d = clamp_playlist_drawer_width(400.0, 100.0);
assert!(d.is_finite() && d >= CHAT_MIN_W);
}
#[test]
fn now_playing_label_prefers_tuned_peer() {
assert_eq!(
now_playing_label(Some("Alice"), Some("local.flac")),
"Tuned in to Alice"
);
}
#[test]
fn now_playing_label_uses_current_track_when_local() {
assert_eq!(now_playing_label(None, Some("local.flac")), "local.flac");
}
#[test]
fn now_playing_label_handles_empty_player() {
assert_eq!(now_playing_label(None, None), "Nothing playing");
}
use crate::notify::Sound;
use iroh::EndpointId;
use std::collections::HashSet;
+18
View File
@@ -138,6 +138,10 @@ fn default_chat_drawer_width() -> f32 {
320.0
}
fn default_playlist_drawer_width() -> f32 {
320.0
}
fn default_window_width() -> f32 {
900.0
}
@@ -174,6 +178,9 @@ pub struct AppConfig {
/// W22 music: opt-in shared listening broadcast toggle. Local preference.
#[serde(default)]
pub music_broadcast: bool,
/// Show the slim now-playing player bar in the room screen.
#[serde(default = "default_true")]
pub show_player_bar: bool,
/// When true, `clip_volume` governs every clip. When false, each clip keeps
/// its own (in-memory) level and the universal slider is inactive.
#[serde(default = "default_true")]
@@ -207,6 +214,9 @@ pub struct AppConfig {
/// Chat drawer width for the drawer layout (px).
#[serde(default = "default_chat_drawer_width")]
pub chat_drawer_width: f32,
/// Playlist drawer width for the room-screen right-edge music panel (px).
#[serde(default = "default_playlist_drawer_width")]
pub playlist_drawer_width: f32,
/// Chosen arrangement of the in-call room screen.
#[serde(default)]
pub room_layout: RoomLayout,
@@ -348,6 +358,7 @@ impl Default for AppConfig {
music_playlist: Vec::new(),
music_volume: 1.0,
music_broadcast: false,
show_player_bar: true,
clip_volume_universal: true,
network_mode: NetworkMode::default(),
presence_mode: crate::presence::PresenceMode::default(),
@@ -358,6 +369,7 @@ impl Default for AppConfig {
threecol_playlist_height: default_threecol_playlist_height(),
controls_width: default_controls_width(),
chat_drawer_width: default_chat_drawer_width(),
playlist_drawer_width: default_playlist_drawer_width(),
room_layout: RoomLayout::default(),
theme: AppTheme::default(),
avatar: crate::avatar::Avatar::default(),
@@ -517,6 +529,8 @@ mod tests {
assert_eq!(deserialized.room_layout, RoomLayout::BottomDock);
assert_eq!(deserialized.controls_width, 280.0);
assert_eq!(deserialized.chat_drawer_width, 320.0);
assert_eq!(deserialized.playlist_drawer_width, 320.0);
assert!(deserialized.show_player_bar);
assert!(deserialized.custom_sound_self_join.is_none());
assert!(deserialized.custom_sound_peer_join.is_none());
assert!(deserialized.custom_sound_peer_leave.is_none());
@@ -701,6 +715,7 @@ mod tests {
assert!(def.music_playlist.is_empty());
assert_eq!(def.music_volume, 1.0);
assert!(!def.music_broadcast);
assert!(def.show_player_bar);
assert!(def.clip_volume_universal);
// Missing in JSON → unity (serde default).
@@ -712,6 +727,7 @@ mod tests {
assert!(cfg_missing.music_playlist.is_empty());
assert_eq!(cfg_missing.music_volume, 1.0);
assert!(!cfg_missing.music_broadcast);
assert!(cfg_missing.show_player_bar);
// Configs predating the toggle default to universal mode.
assert!(cfg_missing.clip_volume_universal);
@@ -723,6 +739,7 @@ mod tests {
music_playlist: vec!["/tmp/song.ogg".to_string()],
music_volume: 0.6,
music_broadcast: true,
show_player_bar: false,
clip_volume_universal: false,
..AppConfig::default()
};
@@ -734,6 +751,7 @@ mod tests {
assert_eq!(round_tripped.music_playlist, vec!["/tmp/song.ogg".to_string()]);
assert_eq!(round_tripped.music_volume, 0.6);
assert!(round_tripped.music_broadcast);
assert!(!round_tripped.show_player_bar);
assert!(!round_tripped.clip_volume_universal);
}