feat: Add PTT, Volume Sliders, and Device Node Selection

This commit is contained in:
2026-05-27 15:23:07 -04:00
parent ea5ffb861b
commit 5ff0201d94
5 changed files with 150 additions and 20 deletions
+99 -6
View File
@@ -2,10 +2,10 @@ use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
use crate::network::PeerState;
use iced::widget::{
container, column, row, text, button, text_input, scrollable, Column,
container, column, row, text, button, text_input, scrollable, slider, checkbox, Column, Space,
};
use iced::{
Color, Background, Border, Element, Subscription, Task, Theme,
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
};
use iroh::EndpointId;
use std::collections::HashMap;
@@ -25,6 +25,12 @@ pub enum AppMessage {
ToggleDeafenPressed,
UiEventReceived(UiEvent),
CopyToClipboard,
TogglePtt(bool),
StartSettingHotkey,
PeerVolumeChanged(EndpointId, f32),
InputDeviceChanged(String),
OutputDeviceChanged(String),
EventOccurred(Event),
}
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
@@ -49,7 +55,14 @@ pub struct AppState {
ticket: String,
is_muted: bool,
is_deafened: bool,
ptt_enabled: bool,
ptt_active: bool,
ptt_hotkey: keyboard::Key,
is_setting_hotkey: bool,
input_device: String,
output_device: String,
peers: HashMap<EndpointId, PeerState>,
peer_volumes: HashMap<EndpointId, f32>,
audio_levels: HashMap<EndpointId, f32>,
controller: Arc<CoreController>,
}
@@ -68,7 +81,14 @@ impl Default for AppState {
ticket: "".to_string(),
is_muted: false,
is_deafened: false,
ptt_enabled: false,
ptt_active: false,
ptt_hotkey: keyboard::Key::Named(keyboard::key::Named::Space),
is_setting_hotkey: false,
input_device: "".to_string(),
output_device: "".to_string(),
peers: HashMap::new(),
peer_volumes: HashMap::new(),
audio_levels: HashMap::new(),
controller,
}
@@ -93,8 +113,10 @@ pub fn run_gui() -> iced::Result {
.run()
}
fn subscription(_state: &AppState) -> Subscription<AppMessage> {
Subscription::run(core_subscription).map(AppMessage::UiEventReceived)
fn subscription(state: &AppState) -> Subscription<AppMessage> {
let core_sub = Subscription::run(core_subscription).map(AppMessage::UiEventReceived);
let event_sub = iced::event::listen().map(AppMessage::EventOccurred);
Subscription::batch(vec![core_sub, event_sub])
}
fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
@@ -110,6 +132,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
let _ = state.controller.send(CoreCommand::Join {
name: state.name.clone(),
ticket: state.ticket_input.clone(),
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
});
}
AppMessage::CreatePressed => {
@@ -117,6 +141,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
let _ = state.controller.send(CoreCommand::Join {
name: state.name.clone(),
ticket: "".to_string(),
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
});
}
AppMessage::LeavePressed => {
@@ -170,6 +196,39 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
return iced::clipboard::write(state.ticket.clone());
}
}
AppMessage::TogglePtt(enabled) => {
state.ptt_enabled = enabled;
let _ = state.controller.send(CoreCommand::SetPttMode(enabled));
}
AppMessage::StartSettingHotkey => {
state.is_setting_hotkey = true;
}
AppMessage::PeerVolumeChanged(id, vol) => {
state.peer_volumes.insert(id, vol);
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
}
AppMessage::InputDeviceChanged(val) => {
state.input_device = val;
}
AppMessage::OutputDeviceChanged(val) => {
state.output_device = val;
}
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
if state.is_setting_hotkey {
state.ptt_hotkey = key.clone();
state.is_setting_hotkey = false;
} else if state.ptt_enabled && key == state.ptt_hotkey && !state.ptt_active {
state.ptt_active = true;
let _ = state.controller.send(CoreCommand::SetPttActive(true));
}
}
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyReleased { key, .. })) => {
if state.ptt_enabled && key == state.ptt_hotkey && state.ptt_active {
state.ptt_active = false;
let _ = state.controller.send(CoreCommand::SetPttActive(false));
}
}
AppMessage::EventOccurred(_) => {}
}
Task::none()
}
@@ -308,6 +367,13 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center),
vertical_space(16.0),
join_group,
vertical_space(16.0),
text("Device Settings (Optional Node Target IDs)").size(14).color(color_subtext),
row![
text_input("Input Target", &state.input_device).on_input(AppMessage::InputDeviceChanged).style(t_style.clone()).padding(8),
horizontal_space(),
text_input("Output Target", &state.output_device).on_input(AppMessage::OutputDeviceChanged).style(t_style.clone()).padding(8),
].spacing(10),
vertical_space(20.0),
status
]
@@ -380,7 +446,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
text("[Idle]").size(14).color(color_subtext)
};
let card = container(
let mut card_content = column![
row![
column![
text(&peer.name).size(16).color(color_text),
@@ -390,7 +456,19 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
indicator
]
.align_y(iced::alignment::Vertical::Center)
)
].spacing(8);
// Peer volume slider
let current_vol = state.peer_volumes.get(peer_id).copied().unwrap_or(1.0);
let peer_id_clone = *peer_id;
card_content = card_content.push(
row![
text("Vol:").size(12).color(color_subtext),
slider(0.0..=2.0, current_vol, move |v| AppMessage::PeerVolumeChanged(peer_id_clone, v))
].spacing(8).align_y(iced::alignment::Vertical::Center)
);
let card = container(card_content)
.style(c_style(
if is_speaking { color_base } else { color_mantle },
if is_speaking { color_green } else { color_surface },
@@ -446,6 +524,21 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.style(b_style(deafen_bg, deafen_hover, deafen_fg, 8.0))
.padding(14)
.width(iced::Length::Fill),
vertical_space(20.0),
checkbox(state.ptt_enabled).label("Push-to-Talk").on_toggle(AppMessage::TogglePtt),
vertical_space(10.0),
if state.ptt_enabled {
column![
text(format!("Hotkey: {}", if state.is_setting_hotkey { "Press any key...".to_string() } else { format!("{:?}", state.ptt_hotkey) })).size(14).color(color_subtext),
button(text("Set Hotkey").size(12).align_x(iced::alignment::Horizontal::Center))
.on_press(AppMessage::StartSettingHotkey)
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding(8)
.width(iced::Length::Fill)
].spacing(8)
} else {
column![]
},
vertical_space(30.0),
button(
text("Leave Room")