feat(ui): draggable, persisted dividers between room panels
Add a reusable Divider canvas widget and place two in the room screen: a vertical divider between the Participants and Controls panels (drag to resize the Participants width) and a horizontal divider between the main row and the Chat dock (drag to resize the dock height). The Participants panel and Chat dock size from persisted config values; the Controls panel and main row fill the rest. The widget reports drag motion as a pixel delta along its axis (mirroring the GateMeter drag handling, so a drag continues past the thin strip). update() applies the delta and clamps it: clamp_participants_width / clamp_chat_height keep both sides of each divider above a minimum. Sizes are re-clamped on window resize (window size tracked from window::Event::Resized) and clamped again on load (a size saved under a different window could be out of range). Persistence: participants_width / chat_height are new serde-default AppConfig fields; the divider publishes PersistConfig on drag release so the final position is written once (not per pixel). 3 clamp unit tests (incl. a tiny-window degenerate case) + config backward-compat assertions for the new fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+263
-7
@@ -39,6 +39,43 @@ struct ChatEntry {
|
|||||||
/// Cap on retained chat history so a long call can't grow it without bound.
|
/// Cap on retained chat history so a long call can't grow it without bound.
|
||||||
const CHAT_HISTORY_MAX: usize = 300;
|
const CHAT_HISTORY_MAX: usize = 300;
|
||||||
|
|
||||||
|
/// Which room-screen divider a drag is resizing.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DividerKind {
|
||||||
|
/// Vertical divider between the Participants and Controls panels (resizes the
|
||||||
|
/// Participants panel width).
|
||||||
|
Panels,
|
||||||
|
/// Horizontal divider between the main row and the Chat dock (resizes the
|
||||||
|
/// Chat dock height).
|
||||||
|
Chat,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimum width of the Participants panel (px).
|
||||||
|
const PARTICIPANTS_MIN_W: f32 = 200.0;
|
||||||
|
/// Minimum width reserved for the Controls panel when resizing Participants (px).
|
||||||
|
const CONTROLS_MIN_W: f32 = 220.0;
|
||||||
|
/// Minimum height of the Chat dock (px).
|
||||||
|
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;
|
||||||
|
/// Thickness of a draggable divider (px).
|
||||||
|
const DIVIDER_THICKNESS: f32 = 8.0;
|
||||||
|
|
||||||
|
/// Clamp the Participants panel width so neither it nor the Controls panel drops
|
||||||
|
/// below its minimum, given the current window width.
|
||||||
|
fn clamp_participants_width(width: f32, window_w: f32) -> f32 {
|
||||||
|
let max = (window_w - CONTROLS_MIN_W).max(PARTICIPANTS_MIN_W);
|
||||||
|
width.clamp(PARTICIPANTS_MIN_W, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp the Chat dock height so neither it nor the area above it drops below its
|
||||||
|
/// minimum, given the current window height.
|
||||||
|
fn clamp_chat_height(height: f32, window_h: f32) -> f32 {
|
||||||
|
let max = (window_h - ABOVE_CHAT_MIN_H).max(CHAT_MIN_H);
|
||||||
|
height.clamp(CHAT_MIN_H, max)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AppMessage {
|
pub enum AppMessage {
|
||||||
NicknameChanged(String),
|
NicknameChanged(String),
|
||||||
@@ -81,6 +118,9 @@ pub enum AppMessage {
|
|||||||
ChatInputChanged(String),
|
ChatInputChanged(String),
|
||||||
/// Send the current chat input line (Enter or the Send button).
|
/// Send the current chat input line (Enter or the Send button).
|
||||||
ChatSubmit,
|
ChatSubmit,
|
||||||
|
/// A room divider was dragged by the given pixel delta along its drag axis
|
||||||
|
/// (horizontal for the Panels divider, vertical for the Chat divider).
|
||||||
|
DividerDragged(DividerKind, f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
||||||
@@ -128,6 +168,9 @@ pub struct AppState {
|
|||||||
/// Room text-chat history (newest last) and the pending input line.
|
/// Room text-chat history (newest last) and the pending input line.
|
||||||
chat_messages: Vec<ChatEntry>,
|
chat_messages: Vec<ChatEntry>,
|
||||||
chat_input: String,
|
chat_input: String,
|
||||||
|
/// Last known window size, tracked so divider clamps stay valid on resize.
|
||||||
|
/// (The divider positions themselves are persisted in `config`.)
|
||||||
|
window_size: Size,
|
||||||
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
|
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
|
||||||
mic_level: f32,
|
mic_level: f32,
|
||||||
/// Whether the standalone (off-call) mic test stream is running.
|
/// Whether the standalone (off-call) mic test stream is running.
|
||||||
@@ -163,7 +206,12 @@ impl Default for AppState {
|
|||||||
let controller = Arc::new(CoreController::new(ui_tx));
|
let controller = Arc::new(CoreController::new(ui_tx));
|
||||||
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
||||||
|
|
||||||
let config = AppConfig::load();
|
let mut config = AppConfig::load();
|
||||||
|
// A persisted divider size from a differently-sized window could be out of
|
||||||
|
// range for the default window — clamp it before first render.
|
||||||
|
config.participants_width =
|
||||||
|
clamp_participants_width(config.participants_width, 900.0);
|
||||||
|
config.chat_height = clamp_chat_height(config.chat_height, 760.0);
|
||||||
notify::set_enabled(config.notifications_enabled);
|
notify::set_enabled(config.notifications_enabled);
|
||||||
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
|
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
|
||||||
let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume));
|
let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume));
|
||||||
@@ -203,6 +251,7 @@ impl Default for AppState {
|
|||||||
recording_started: None,
|
recording_started: None,
|
||||||
chat_messages: Vec::new(),
|
chat_messages: Vec::new(),
|
||||||
chat_input: String::new(),
|
chat_input: String::new(),
|
||||||
|
window_size: Size::new(900.0, 760.0),
|
||||||
mic_level: 0.0,
|
mic_level: 0.0,
|
||||||
mic_test_active: false,
|
mic_test_active: false,
|
||||||
connecting: HashSet::new(),
|
connecting: HashSet::new(),
|
||||||
@@ -510,6 +559,26 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
AppMessage::ChatInputChanged(val) => {
|
AppMessage::ChatInputChanged(val) => {
|
||||||
state.chat_input = val;
|
state.chat_input = val;
|
||||||
}
|
}
|
||||||
|
AppMessage::DividerDragged(kind, delta) => {
|
||||||
|
// Apply live; the final position is persisted on drag release (the
|
||||||
|
// divider publishes PersistConfig then) to avoid per-pixel disk writes.
|
||||||
|
match kind {
|
||||||
|
DividerKind::Panels => {
|
||||||
|
state.config.participants_width = clamp_participants_width(
|
||||||
|
state.config.participants_width + delta,
|
||||||
|
state.window_size.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DividerKind::Chat => {
|
||||||
|
// Dragging the divider down (positive delta) gives the main row
|
||||||
|
// more room and shrinks the dock below it, so subtract.
|
||||||
|
state.config.chat_height = clamp_chat_height(
|
||||||
|
state.config.chat_height - delta,
|
||||||
|
state.window_size.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
AppMessage::ChatSubmit => {
|
AppMessage::ChatSubmit => {
|
||||||
let text = state.chat_input.trim().to_string();
|
let text = state.chat_input.trim().to_string();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
@@ -548,6 +617,16 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
let _ = state.controller.send(CoreCommand::SetPttActive(false));
|
let _ = state.controller.send(CoreCommand::SetPttActive(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppMessage::EventOccurred(Event::Window(iced::window::Event::Resized(size))) => {
|
||||||
|
state.window_size = size;
|
||||||
|
// Keep divider positions valid for the new window dimensions. (Saved
|
||||||
|
// with the next drag-release or other config write; not worth a disk
|
||||||
|
// write on every resize tick.)
|
||||||
|
state.config.participants_width =
|
||||||
|
clamp_participants_width(state.config.participants_width, size.width);
|
||||||
|
state.config.chat_height =
|
||||||
|
clamp_chat_height(state.config.chat_height, size.height);
|
||||||
|
}
|
||||||
AppMessage::EventOccurred(_) => {}
|
AppMessage::EventOccurred(_) => {}
|
||||||
AppMessage::NavigateToSettings => {
|
AppMessage::NavigateToSettings => {
|
||||||
state.current_screen = Screen::Settings;
|
state.current_screen = Screen::Settings;
|
||||||
@@ -1133,7 +1212,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
)
|
)
|
||||||
.style(c_style(color_mantle, Color::TRANSPARENT, 0.0))
|
.style(c_style(color_mantle, Color::TRANSPARENT, 0.0))
|
||||||
.padding(15)
|
.padding(15)
|
||||||
.width(iced::Length::FillPortion(2))
|
.width(iced::Length::Fixed(state.config.participants_width))
|
||||||
.height(iced::Length::Fill);
|
.height(iced::Length::Fill);
|
||||||
|
|
||||||
// Control Panel Column
|
// Control Panel Column
|
||||||
@@ -1222,11 +1301,21 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
)
|
)
|
||||||
.style(c_style(color_mantle, Color::TRANSPARENT, 0.0))
|
.style(c_style(color_mantle, Color::TRANSPARENT, 0.0))
|
||||||
.padding(15)
|
.padding(15)
|
||||||
.width(iced::Length::FillPortion(1))
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fill);
|
.height(iced::Length::Fill);
|
||||||
|
|
||||||
let main_layout = row![peers_panel, control_panel]
|
// Draggable vertical divider between Participants and Controls.
|
||||||
.spacing(15)
|
let panels_divider = Canvas::new(Divider {
|
||||||
|
kind: DividerKind::Panels,
|
||||||
|
vertical: true,
|
||||||
|
line: color_surface,
|
||||||
|
grip: color_lavender,
|
||||||
|
})
|
||||||
|
.width(iced::Length::Fixed(DIVIDER_THICKNESS))
|
||||||
|
.height(iced::Length::Fill);
|
||||||
|
|
||||||
|
let main_layout = row![peers_panel, panels_divider, control_panel]
|
||||||
|
.spacing(0)
|
||||||
.height(iced::Length::Fill);
|
.height(iced::Length::Fill);
|
||||||
|
|
||||||
// --- CHAT DOCK (full-width strip along the bottom) ---
|
// --- CHAT DOCK (full-width strip along the bottom) ---
|
||||||
@@ -1279,7 +1368,17 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.style(c_style(color_mantle, color_surface, 8.0))
|
.style(c_style(color_mantle, color_surface, 8.0))
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fixed(180.0));
|
.height(iced::Length::Fixed(state.config.chat_height));
|
||||||
|
|
||||||
|
// Draggable horizontal divider between the main row and the Chat dock.
|
||||||
|
let chat_divider = Canvas::new(Divider {
|
||||||
|
kind: DividerKind::Chat,
|
||||||
|
vertical: false,
|
||||||
|
line: color_surface,
|
||||||
|
grip: color_lavender,
|
||||||
|
})
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fixed(DIVIDER_THICKNESS));
|
||||||
|
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
@@ -1287,7 +1386,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
header_container,
|
header_container,
|
||||||
vertical_space(12.0),
|
vertical_space(12.0),
|
||||||
main_layout,
|
main_layout,
|
||||||
vertical_space(12.0),
|
chat_divider,
|
||||||
chat_dock
|
chat_dock
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -1428,6 +1527,132 @@ impl Program<AppMessage> for GateMeter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A draggable splitter between two panels. Reports drag motion along its axis as
|
||||||
|
/// `AppMessage::DividerDragged(kind, delta_px)`; the parent applies + clamps it.
|
||||||
|
/// `vertical` = a vertical bar dragged horizontally (resizes width); otherwise a
|
||||||
|
/// horizontal bar dragged vertically (resizes height). Modeled on [`GateMeter`]'s
|
||||||
|
/// drag handling: the drag is tracked off the global cursor so it survives the
|
||||||
|
/// pointer leaving the thin divider strip.
|
||||||
|
struct Divider {
|
||||||
|
kind: DividerKind,
|
||||||
|
/// True = vertical bar (horizontal drag); false = horizontal bar (vertical drag).
|
||||||
|
vertical: bool,
|
||||||
|
/// Centre line colour.
|
||||||
|
line: Color,
|
||||||
|
/// Grip-dot colour.
|
||||||
|
grip: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drag state: the last cursor coordinate along the drag axis while dragging.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DividerState {
|
||||||
|
last: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Program<AppMessage> for Divider {
|
||||||
|
type State = DividerState;
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
state: &mut Self::State,
|
||||||
|
event: &Event,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> Option<Action<AppMessage>> {
|
||||||
|
// Cursor coordinate along the drag axis (x for a vertical bar, else y).
|
||||||
|
let axis = |p: Point| if self.vertical { p.x } else { p.y };
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
|
if cursor.is_over(bounds)
|
||||||
|
&& let Some(p) = cursor.position()
|
||||||
|
{
|
||||||
|
// Record the absolute start coordinate; subsequent moves yield
|
||||||
|
// deltas. Capture with a zero-delta no-op.
|
||||||
|
state.last = Some(axis(p));
|
||||||
|
return Some(
|
||||||
|
Action::publish(AppMessage::DividerDragged(self.kind, 0.0)).and_capture(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Track globally so the drag continues past the thin strip's bounds.
|
||||||
|
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.last.is_some() => {
|
||||||
|
if let Some(p) = cursor.position() {
|
||||||
|
let cur = axis(p);
|
||||||
|
let last = state.last.unwrap();
|
||||||
|
state.last = Some(cur);
|
||||||
|
return Some(
|
||||||
|
Action::publish(AppMessage::DividerDragged(self.kind, cur - last))
|
||||||
|
.and_capture(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||||
|
if state.last.is_some() =>
|
||||||
|
{
|
||||||
|
state.last = None;
|
||||||
|
// Persist the final position once, on release (not per pixel).
|
||||||
|
return Some(Action::publish(AppMessage::PersistConfig).and_capture());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &Self::State,
|
||||||
|
renderer: &Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
bounds: Rectangle,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> Vec<Geometry> {
|
||||||
|
let mut frame = Frame::new(renderer, bounds.size());
|
||||||
|
let w = bounds.width;
|
||||||
|
let h = bounds.height;
|
||||||
|
if self.vertical {
|
||||||
|
let x = w / 2.0;
|
||||||
|
frame.fill(&Path::rectangle(Point::new(x - 1.0, 0.0), Size::new(2.0, h)), self.line);
|
||||||
|
let cy = h / 2.0;
|
||||||
|
for i in -1..=1 {
|
||||||
|
let dy = cy + i as f32 * 6.0;
|
||||||
|
frame.fill(
|
||||||
|
&Path::rectangle(Point::new(x - 1.5, dy - 1.5), Size::new(3.0, 3.0)),
|
||||||
|
self.grip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let y = h / 2.0;
|
||||||
|
frame.fill(&Path::rectangle(Point::new(0.0, y - 1.0), Size::new(w, 2.0)), self.line);
|
||||||
|
let cx = w / 2.0;
|
||||||
|
for i in -1..=1 {
|
||||||
|
let dx = cx + i as f32 * 6.0;
|
||||||
|
frame.fill(
|
||||||
|
&Path::rectangle(Point::new(dx - 1.5, y - 1.5), Size::new(3.0, 3.0)),
|
||||||
|
self.grip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec![frame.into_geometry()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
state: &Self::State,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
if state.last.is_some() || cursor.is_over(bounds) {
|
||||||
|
if self.vertical {
|
||||||
|
mouse::Interaction::ResizingHorizontally
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::ResizingVertically
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mouse::Interaction::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{format_duration, reconnect_attempt_chime, reconnected_chime, GateMeter, METER_MAX};
|
use super::{format_duration, reconnect_attempt_chime, reconnected_chime, GateMeter, METER_MAX};
|
||||||
@@ -1445,6 +1670,37 @@ mod tests {
|
|||||||
assert_eq!(format_duration(3661), "1:01:01");
|
assert_eq!(format_duration(3661), "1:01:01");
|
||||||
assert_eq!(format_duration(3725), "1:02:05");
|
assert_eq!(format_duration(3725), "1:02:05");
|
||||||
}
|
}
|
||||||
|
use super::{clamp_chat_height, clamp_participants_width, CHAT_MIN_H, PARTICIPANTS_MIN_W};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn participants_width_clamps_to_min_and_leaves_room_for_controls() {
|
||||||
|
let window_w = 900.0;
|
||||||
|
// Mid-range value passes through unchanged.
|
||||||
|
assert_eq!(clamp_participants_width(500.0, window_w), 500.0);
|
||||||
|
// Below the minimum snaps up to it.
|
||||||
|
assert_eq!(clamp_participants_width(50.0, window_w), PARTICIPANTS_MIN_W);
|
||||||
|
// Too wide leaves at least CONTROLS_MIN_W (220) for the controls panel.
|
||||||
|
assert_eq!(clamp_participants_width(window_w, window_w), window_w - 220.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chat_height_clamps_to_min_and_leaves_room_above() {
|
||||||
|
let window_h = 760.0;
|
||||||
|
assert_eq!(clamp_chat_height(200.0, window_h), 200.0);
|
||||||
|
assert_eq!(clamp_chat_height(10.0, window_h), CHAT_MIN_H);
|
||||||
|
// Too tall leaves at least ABOVE_CHAT_MIN_H (300) above the dock.
|
||||||
|
assert_eq!(clamp_chat_height(window_h, window_h), window_h - 300.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn divider_clamps_are_finite_on_a_tiny_window() {
|
||||||
|
// A window smaller than the reserves must not produce NaN/inverted ranges.
|
||||||
|
let pw = clamp_participants_width(300.0, 100.0);
|
||||||
|
assert!(pw.is_finite() && pw >= PARTICIPANTS_MIN_W);
|
||||||
|
let ch = clamp_chat_height(300.0, 100.0);
|
||||||
|
assert!(ch.is_finite() && ch >= CHAT_MIN_H);
|
||||||
|
}
|
||||||
|
|
||||||
use crate::notify::Sound;
|
use crate::notify::Sound;
|
||||||
use iroh::EndpointId;
|
use iroh::EndpointId;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ fn default_volume() -> f32 {
|
|||||||
1.0
|
1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_participants_width() -> f32 {
|
||||||
|
540.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_chat_height() -> f32 {
|
||||||
|
180.0
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub input_device: String,
|
pub input_device: String,
|
||||||
@@ -63,6 +71,12 @@ pub struct AppConfig {
|
|||||||
pub echo_cancellation_enabled: bool,
|
pub echo_cancellation_enabled: bool,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub notifications_enabled: bool,
|
pub notifications_enabled: bool,
|
||||||
|
/// Persisted room-screen divider positions (px): Participants panel width and
|
||||||
|
/// Chat dock height. Re-clamped to the window on load and on resize.
|
||||||
|
#[serde(default = "default_participants_width")]
|
||||||
|
pub participants_width: f32,
|
||||||
|
#[serde(default = "default_chat_height")]
|
||||||
|
pub chat_height: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub custom_sound_self_join: Option<String>,
|
pub custom_sound_self_join: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -92,6 +106,8 @@ impl Default for AppConfig {
|
|||||||
network_mode: NetworkMode::default(),
|
network_mode: NetworkMode::default(),
|
||||||
echo_cancellation_enabled: false,
|
echo_cancellation_enabled: false,
|
||||||
notifications_enabled: true,
|
notifications_enabled: true,
|
||||||
|
participants_width: default_participants_width(),
|
||||||
|
chat_height: default_chat_height(),
|
||||||
custom_sound_self_join: None,
|
custom_sound_self_join: None,
|
||||||
custom_sound_peer_join: None,
|
custom_sound_peer_join: None,
|
||||||
custom_sound_peer_leave: None,
|
custom_sound_peer_leave: None,
|
||||||
@@ -157,6 +173,9 @@ mod tests {
|
|||||||
// Configs predating the volume sliders must load at unity gain.
|
// Configs predating the volume sliders must load at unity gain.
|
||||||
assert_eq!(deserialized.input_volume, 1.0);
|
assert_eq!(deserialized.input_volume, 1.0);
|
||||||
assert_eq!(deserialized.output_volume, 1.0);
|
assert_eq!(deserialized.output_volume, 1.0);
|
||||||
|
// Configs predating the draggable dividers must load the default sizes.
|
||||||
|
assert_eq!(deserialized.participants_width, 540.0);
|
||||||
|
assert_eq!(deserialized.chat_height, 180.0);
|
||||||
assert!(deserialized.custom_sound_self_join.is_none());
|
assert!(deserialized.custom_sound_self_join.is_none());
|
||||||
assert!(deserialized.custom_sound_peer_join.is_none());
|
assert!(deserialized.custom_sound_peer_join.is_none());
|
||||||
assert!(deserialized.custom_sound_peer_leave.is_none());
|
assert!(deserialized.custom_sound_peer_leave.is_none());
|
||||||
|
|||||||
Reference in New Issue
Block a user