feat(ui): selectable room layouts with a thumbnail picker

Add three in-call room layouts — 3-Column (Participants | Chat | Controls),
Bottom Dock (Participants+Controls over a full-width Chat strip), and Drawer
(Participants | Controls with a collapsible Chat panel) — chosen via one
persisted RoomLayout config setting and applied live.

Picker UX: a square layout button (drawn LayoutIcon glyph) in the top bar of the
launch and in-call screens opens a popup gallery (dimmed click-to-dismiss
backdrop + centered panel) of clickable schematic thumbnails; the Settings screen
shows the same thumbnails inline (no button). Thumbnails are drawn with the
canvas widget (new LayoutThumb program — colored panel boxes, blue border on the
selected one), so no image-decoding dependency is added.

Each layout's panel boundaries are draggable (DividerKind gains Controls +
ChatDrawer for the 3-column right divider and the drawer's left edge; new
clamp_controls_width / clamp_chat_drawer_width, persisted + re-clamped on resize).
Participants width is shared across layouts but capped per layout at render time
so a fixed panel can't starve the Fill panel (e.g. a wide Participants width set
in the dock layout won't collapse Chat in 3-column or Controls in the drawer).
The Drawer layout adds a header chat-toggle. +1 clamp test (now 127 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 00:23:27 -04:00
co-authored by Claude Opus 4.8
parent b436b57f13
commit 2f12d54a80
2 changed files with 541 additions and 57 deletions
+52
View File
@@ -25,6 +25,34 @@ impl NetworkMode {
[NetworkMode::RelayNoDiscovery, NetworkMode::N0Full, NetworkMode::DirectOnly];
}
/// Arrangement of the in-call room screen, chosen via the layout picker.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum RoomLayout {
/// Participants | Chat | Controls, all columns visible at once.
ThreeColumn,
/// Participants + Controls on top, full-width Chat docked along the bottom.
#[default]
BottomDock,
/// Participants | Controls, with a collapsible Chat drawer on the right edge.
Drawer,
}
impl RoomLayout {
/// All variants, in picker display order.
pub const ALL: [RoomLayout; 3] =
[RoomLayout::ThreeColumn, RoomLayout::BottomDock, RoomLayout::Drawer];
}
impl std::fmt::Display for RoomLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
RoomLayout::ThreeColumn => "3-Column",
RoomLayout::BottomDock => "Bottom Dock",
RoomLayout::Drawer => "Drawer",
})
}
}
impl std::fmt::Display for NetworkMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
@@ -56,6 +84,14 @@ fn default_chat_height() -> f32 {
180.0
}
fn default_controls_width() -> f32 {
280.0
}
fn default_chat_drawer_width() -> f32 {
320.0
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AppConfig {
/// Last nickname used to join/create a room; pre-filled on the launch screen.
@@ -84,6 +120,15 @@ pub struct AppConfig {
pub participants_width: f32,
#[serde(default = "default_chat_height")]
pub chat_height: f32,
/// Controls panel width for the 3-column layout (px).
#[serde(default = "default_controls_width")]
pub controls_width: f32,
/// Chat drawer width for the drawer layout (px).
#[serde(default = "default_chat_drawer_width")]
pub chat_drawer_width: f32,
/// Chosen arrangement of the in-call room screen.
#[serde(default)]
pub room_layout: RoomLayout,
#[serde(default)]
pub custom_sound_self_join: Option<String>,
#[serde(default)]
@@ -116,6 +161,9 @@ impl Default for AppConfig {
notifications_enabled: true,
participants_width: default_participants_width(),
chat_height: default_chat_height(),
controls_width: default_controls_width(),
chat_drawer_width: default_chat_drawer_width(),
room_layout: RoomLayout::default(),
custom_sound_self_join: None,
custom_sound_peer_join: None,
custom_sound_peer_leave: None,
@@ -186,6 +234,10 @@ mod tests {
assert_eq!(deserialized.chat_height, 180.0);
// Configs predating the remembered username load the default nickname.
assert_eq!(deserialized.username, "Peer");
// Configs predating the layout picker load the default layout + sizes.
assert_eq!(deserialized.room_layout, RoomLayout::BottomDock);
assert_eq!(deserialized.controls_width, 280.0);
assert_eq!(deserialized.chat_drawer_width, 320.0);
assert!(deserialized.custom_sound_self_join.is_none());
assert!(deserialized.custom_sound_peer_join.is_none());
assert!(deserialized.custom_sound_peer_leave.is_none());