Add audio controls and focused hotkeys
This commit is contained in:
+404
-25
@@ -1,8 +1,10 @@
|
||||
use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
|
||||
use crate::network::PeerState;
|
||||
use crate::notify::{self, Sound};
|
||||
use crate::audio::eq::{EqSettings, EQ_GAIN_DB_MAX, EQ_GAIN_DB_MIN};
|
||||
use crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};
|
||||
use crate::config::{AppConfig, NetworkMode, RecordingMode, RoomLayout};
|
||||
use crate::hotkeys::{format_binding, HotkeyAction, HotkeyContext, KeyBinding};
|
||||
use crate::presence::PresenceMode;
|
||||
use crate::theme::{AppTheme, Palette};
|
||||
|
||||
@@ -62,6 +64,13 @@ pub enum DividerKind {
|
||||
ChatDrawer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EqBand {
|
||||
Low,
|
||||
Mid,
|
||||
High,
|
||||
}
|
||||
|
||||
/// 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).
|
||||
@@ -121,8 +130,11 @@ pub enum AppMessage {
|
||||
/// Copy an arbitrary string to the clipboard (e.g. the full node ID).
|
||||
CopyText(String),
|
||||
TogglePtt(bool),
|
||||
StartSettingHotkey,
|
||||
StartHotkeyCapture(HotkeyAction),
|
||||
ClearHotkey(HotkeyAction),
|
||||
PeerVolumeChanged(EndpointId, f32),
|
||||
PeerPanChanged(EndpointId, f32),
|
||||
PeerEqChanged(EndpointId, EqBand, f32),
|
||||
/// Toggle local mute of a peer (silence them just for us).
|
||||
TogglePeerMute(EndpointId),
|
||||
InputDeviceSelected(AudioDevice),
|
||||
@@ -186,6 +198,9 @@ pub enum AppMessage {
|
||||
/// Open / close the room-layout picker popup.
|
||||
OpenLayoutPicker,
|
||||
CloseLayoutPicker,
|
||||
/// Open / close the live hotkey reference popup.
|
||||
OpenHotkeyInfo,
|
||||
CloseHotkeyInfo,
|
||||
/// Open / close the "screen sharing needs pixelpass" explainer popup (A11).
|
||||
OpenPixelpassHelp,
|
||||
ClosePixelpassHelp,
|
||||
@@ -235,8 +250,7 @@ pub struct AppState {
|
||||
is_deafened: bool,
|
||||
ptt_enabled: bool,
|
||||
ptt_active: bool,
|
||||
ptt_hotkey: keyboard::Key,
|
||||
is_setting_hotkey: bool,
|
||||
hotkey_capture: Option<HotkeyAction>,
|
||||
input_devices: Vec<AudioDevice>,
|
||||
output_devices: Vec<AudioDevice>,
|
||||
selected_input: Option<AudioDevice>,
|
||||
@@ -261,6 +275,8 @@ pub struct AppState {
|
||||
window_size: Size,
|
||||
/// Whether the room-layout picker popup is open (launch + in-call screens).
|
||||
layout_picker_open: bool,
|
||||
/// Whether the hotkey reference popup is open.
|
||||
hotkey_info_open: bool,
|
||||
/// Whether the pixelpass screen-share explainer popup is open (A11).
|
||||
pixelpass_help_open: bool,
|
||||
/// Whether the Chat drawer is open (drawer layout only).
|
||||
@@ -353,6 +369,16 @@ impl Default for AppState {
|
||||
let _ = controller.send(CoreCommand::SetRecordingMode(config.recording_mode));
|
||||
let _ = controller.send(CoreCommand::SetPixelpassPath(config.pixelpass_path.clone()));
|
||||
let _ = controller.send(CoreCommand::SetPresenceMode(config.presence_mode));
|
||||
for (peer, settings) in &config.peer_eq {
|
||||
if let Ok(id) = peer.parse::<EndpointId>() {
|
||||
let _ = controller.send(CoreCommand::SetPeerEq(id, *settings));
|
||||
}
|
||||
}
|
||||
for (peer, pan) in &config.peer_pan {
|
||||
if let Ok(id) = peer.parse::<EndpointId>() {
|
||||
let _ = controller.send(CoreCommand::SetPeerPan(id, *pan));
|
||||
}
|
||||
}
|
||||
let pixelpass_available =
|
||||
crate::screenshare::is_available(config.pixelpass_path.as_deref());
|
||||
let all_devices = enumerate_audio_devices();
|
||||
@@ -375,8 +401,7 @@ impl Default for AppState {
|
||||
is_deafened: false,
|
||||
ptt_enabled: false,
|
||||
ptt_active: false,
|
||||
ptt_hotkey: keyboard::Key::Named(keyboard::key::Named::Space),
|
||||
is_setting_hotkey: false,
|
||||
hotkey_capture: None,
|
||||
input_devices,
|
||||
output_devices,
|
||||
selected_input,
|
||||
@@ -393,6 +418,7 @@ impl Default for AppState {
|
||||
chat_input: String::new(),
|
||||
window_size: Size::new(ww, wh),
|
||||
layout_picker_open: false,
|
||||
hotkey_info_open: false,
|
||||
pixelpass_help_open: false,
|
||||
drawer_chat_open: false,
|
||||
mic_level: 0.0,
|
||||
@@ -526,6 +552,105 @@ fn reconnected_chime(
|
||||
was_reconnect.then_some(Sound::Reconnected)
|
||||
}
|
||||
|
||||
fn in_call(state: &AppState) -> bool {
|
||||
!state.ticket.is_empty()
|
||||
}
|
||||
|
||||
fn toggle_mute(state: &mut AppState) {
|
||||
if !in_call(state) {
|
||||
return;
|
||||
}
|
||||
let _ = state.controller.send(CoreCommand::ToggleMute);
|
||||
state.is_muted = !state.is_muted;
|
||||
notify::play(Sound::MicToggle, state.config.custom_sound_mic_toggle.as_deref());
|
||||
}
|
||||
|
||||
fn toggle_deafen(state: &mut AppState) {
|
||||
if !in_call(state) {
|
||||
return;
|
||||
}
|
||||
let _ = state.controller.send(CoreCommand::ToggleDeafen);
|
||||
state.is_deafened = !state.is_deafened;
|
||||
notify::play(Sound::MicToggle, state.config.custom_sound_mic_toggle.as_deref());
|
||||
}
|
||||
|
||||
fn handle_hotkey_pressed(state: &mut AppState, action: HotkeyAction) {
|
||||
match action {
|
||||
HotkeyAction::ToggleMute => toggle_mute(state),
|
||||
HotkeyAction::ToggleDeafen => toggle_deafen(state),
|
||||
HotkeyAction::OpenSettings => {
|
||||
state.current_screen = Screen::Settings;
|
||||
state.hotkey_info_open = false;
|
||||
state.layout_picker_open = false;
|
||||
}
|
||||
HotkeyAction::PushToTalk => {
|
||||
if in_call(state) && state.ptt_enabled && !state.ptt_active {
|
||||
state.ptt_active = true;
|
||||
let _ = state.controller.send(CoreCommand::SetPttActive(true));
|
||||
}
|
||||
}
|
||||
HotkeyAction::LeaveRoom => {
|
||||
if in_call(state) {
|
||||
let _ = state.controller.send(CoreCommand::Leave);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_peer_pan_config(config: &mut AppConfig, id: EndpointId, pan: f32) -> f32 {
|
||||
let pan = pan.clamp(-1.0, 1.0);
|
||||
let key = id.to_string();
|
||||
if pan.abs() <= 0.001 {
|
||||
config.peer_pan.remove(&key);
|
||||
} else {
|
||||
config.peer_pan.insert(key, pan);
|
||||
}
|
||||
pan
|
||||
}
|
||||
|
||||
fn peer_eq_settings(config: &AppConfig, id: &EndpointId) -> EqSettings {
|
||||
config
|
||||
.peer_eq
|
||||
.get(&id.to_string())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.clamped()
|
||||
}
|
||||
|
||||
fn set_peer_eq_config(
|
||||
config: &mut AppConfig,
|
||||
id: EndpointId,
|
||||
band: EqBand,
|
||||
gain_db: f32,
|
||||
) -> EqSettings {
|
||||
let key = id.to_string();
|
||||
let mut settings = config.peer_eq.get(&key).copied().unwrap_or_default();
|
||||
let gain_db = gain_db.clamp(EQ_GAIN_DB_MIN, EQ_GAIN_DB_MAX);
|
||||
match band {
|
||||
EqBand::Low => settings.low_gain_db = gain_db,
|
||||
EqBand::Mid => settings.mid_gain_db = gain_db,
|
||||
EqBand::High => settings.high_gain_db = gain_db,
|
||||
}
|
||||
settings = settings.clamped();
|
||||
if settings.is_flat() {
|
||||
config.peer_eq.remove(&key);
|
||||
} else {
|
||||
config.peer_eq.insert(key, settings);
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
fn pan_label(pan: f32) -> String {
|
||||
let pan = pan.clamp(-1.0, 1.0);
|
||||
if pan.abs() <= 0.01 {
|
||||
"Center".to_string()
|
||||
} else if pan < 0.0 {
|
||||
format!("L {:.0}%", pan.abs() * 100.0)
|
||||
} else {
|
||||
format!("R {:.0}%", pan * 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
match message {
|
||||
AppMessage::NicknameChanged(val) => {
|
||||
@@ -593,14 +718,10 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.status_message = "Opening screen share…".to_string();
|
||||
}
|
||||
AppMessage::ToggleMutePressed => {
|
||||
let _ = state.controller.send(CoreCommand::ToggleMute);
|
||||
state.is_muted = !state.is_muted;
|
||||
notify::play(Sound::MicToggle, state.config.custom_sound_mic_toggle.as_deref());
|
||||
toggle_mute(state);
|
||||
}
|
||||
AppMessage::ToggleDeafenPressed => {
|
||||
let _ = state.controller.send(CoreCommand::ToggleDeafen);
|
||||
state.is_deafened = !state.is_deafened;
|
||||
notify::play(Sound::MicToggle, state.config.custom_sound_mic_toggle.as_deref());
|
||||
toggle_deafen(state);
|
||||
}
|
||||
AppMessage::UiEventReceived(event) => {
|
||||
match event {
|
||||
@@ -761,13 +882,26 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.ptt_enabled = enabled;
|
||||
let _ = state.controller.send(CoreCommand::SetPttMode(enabled));
|
||||
}
|
||||
AppMessage::StartSettingHotkey => {
|
||||
state.is_setting_hotkey = true;
|
||||
AppMessage::StartHotkeyCapture(action) => {
|
||||
state.hotkey_capture = Some(action);
|
||||
state.hotkey_info_open = false;
|
||||
}
|
||||
AppMessage::ClearHotkey(action) => {
|
||||
state.config.hotkeys.set_binding(action, None);
|
||||
state.config.save();
|
||||
}
|
||||
AppMessage::PeerVolumeChanged(id, vol) => {
|
||||
state.peer_volumes.insert(id, vol);
|
||||
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
||||
}
|
||||
AppMessage::PeerPanChanged(id, pan) => {
|
||||
let pan = set_peer_pan_config(&mut state.config, id, pan);
|
||||
let _ = state.controller.send(CoreCommand::SetPeerPan(id, pan));
|
||||
}
|
||||
AppMessage::PeerEqChanged(id, band, gain_db) => {
|
||||
let settings = set_peer_eq_config(&mut state.config, id, band, gain_db);
|
||||
let _ = state.controller.send(CoreCommand::SetPeerEq(id, settings));
|
||||
}
|
||||
AppMessage::TogglePeerMute(id) => {
|
||||
let now_muted = if state.locally_muted.contains(&id) {
|
||||
state.locally_muted.remove(&id);
|
||||
@@ -1021,10 +1155,18 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
}
|
||||
AppMessage::OpenLayoutPicker => {
|
||||
state.layout_picker_open = true;
|
||||
state.hotkey_info_open = false;
|
||||
}
|
||||
AppMessage::CloseLayoutPicker => {
|
||||
state.layout_picker_open = false;
|
||||
}
|
||||
AppMessage::OpenHotkeyInfo => {
|
||||
state.hotkey_info_open = true;
|
||||
state.layout_picker_open = false;
|
||||
}
|
||||
AppMessage::CloseHotkeyInfo => {
|
||||
state.hotkey_info_open = false;
|
||||
}
|
||||
AppMessage::OpenPixelpassHelp => {
|
||||
state.pixelpass_help_open = true;
|
||||
}
|
||||
@@ -1119,16 +1261,30 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
.send(CoreCommand::SetMicMonitor { enabled, input_device });
|
||||
}
|
||||
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));
|
||||
if let Some(action) = state.hotkey_capture.take() {
|
||||
if let Some(binding) = KeyBinding::from_key(&key) {
|
||||
state.config.hotkeys.set_binding(action, Some(binding));
|
||||
state.config.save();
|
||||
} else {
|
||||
state.hotkey_capture = Some(action);
|
||||
}
|
||||
} else if let Some(action) = state
|
||||
.config
|
||||
.hotkeys
|
||||
.lookup_key(&key, HotkeyContext { in_call: in_call(state) })
|
||||
{
|
||||
handle_hotkey_pressed(state, action);
|
||||
}
|
||||
}
|
||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyReleased { key, .. })) => {
|
||||
if state.ptt_enabled && key == state.ptt_hotkey && state.ptt_active {
|
||||
if state
|
||||
.config
|
||||
.hotkeys
|
||||
.lookup_key(&key, HotkeyContext { in_call: in_call(state) })
|
||||
== Some(HotkeyAction::PushToTalk)
|
||||
&& state.ptt_enabled
|
||||
&& state.ptt_active
|
||||
{
|
||||
state.ptt_active = false;
|
||||
let _ = state.controller.send(CoreCommand::SetPttActive(false));
|
||||
}
|
||||
@@ -1171,9 +1327,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
AppMessage::EventOccurred(_) => {}
|
||||
AppMessage::NavigateToSettings => {
|
||||
state.current_screen = Screen::Settings;
|
||||
state.layout_picker_open = false;
|
||||
state.hotkey_info_open = false;
|
||||
}
|
||||
AppMessage::NavigateBack => {
|
||||
state.config.save();
|
||||
state.hotkey_capture = None;
|
||||
// Release the mic when leaving Settings if the test was running.
|
||||
if state.mic_test_active {
|
||||
state.mic_test_active = false;
|
||||
@@ -1752,6 +1911,17 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
|
||||
let top_bar = row![
|
||||
horizontal_space(),
|
||||
tooltip(
|
||||
button(icon(IconKind::Info, 18.0, color_text))
|
||||
.on_press(AppMessage::OpenHotkeyInfo)
|
||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||
.padding(8),
|
||||
container(text("Hotkeys").size(11).color(color_text))
|
||||
.padding(8)
|
||||
.style(c_style(color_crust, color_surface, 6.0)),
|
||||
iced::widget::tooltip::Position::Bottom,
|
||||
)
|
||||
.gap(8),
|
||||
tooltip(
|
||||
button(
|
||||
Canvas::new(LayoutIcon { fg: color_text })
|
||||
@@ -2039,6 +2209,72 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut hotkey_rows = Column::new().spacing(8).width(iced::Length::Fill);
|
||||
for action in HotkeyAction::ALL {
|
||||
let capturing = state.hotkey_capture == Some(action);
|
||||
let binding = if capturing {
|
||||
"Press a key...".to_string()
|
||||
} else {
|
||||
format_binding(state.config.hotkeys.binding(action))
|
||||
};
|
||||
hotkey_rows = hotkey_rows.push(
|
||||
row![
|
||||
column![
|
||||
text(action.label()).size(13).color(color_text),
|
||||
text(match action.tier() {
|
||||
crate::hotkeys::HotkeyTier::AppWide => "App-wide",
|
||||
crate::hotkeys::HotkeyTier::RoomOnly => "Room-only",
|
||||
})
|
||||
.size(10)
|
||||
.color(color_subtext),
|
||||
]
|
||||
.spacing(2)
|
||||
.width(iced::Length::Fill),
|
||||
container(text(binding).size(12).color(if capturing { color_yellow } else { color_subtext }))
|
||||
.width(iced::Length::Fixed(110.0))
|
||||
.align_x(iced::alignment::Horizontal::Right),
|
||||
button(text("Set").size(12))
|
||||
.on_press(AppMessage::StartHotkeyCapture(action))
|
||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||
.padding(6),
|
||||
button(text("Clear").size(12))
|
||||
.on_press(AppMessage::ClearHotkey(action))
|
||||
.style(b_style(color_surface, color_red, color_text, 6.0))
|
||||
.padding(6),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
);
|
||||
}
|
||||
let hotkey_conflicts = state.config.hotkeys.conflicts();
|
||||
let conflict_block: Element<'_, AppMessage> = if hotkey_conflicts.is_empty() {
|
||||
vertical_space(0.0).into()
|
||||
} else {
|
||||
let mut lines = Column::new().spacing(4);
|
||||
for conflict in hotkey_conflicts {
|
||||
lines = lines.push(
|
||||
text(format!(
|
||||
"Conflict: {} is assigned to {} and {}.",
|
||||
conflict.binding.label(),
|
||||
conflict.first.label(),
|
||||
conflict.second.label()
|
||||
))
|
||||
.size(11)
|
||||
.color(color_red),
|
||||
);
|
||||
}
|
||||
lines.into()
|
||||
};
|
||||
let hotkey_section = column![
|
||||
hotkey_rows,
|
||||
conflict_block,
|
||||
text("Shortcuts work only while the PeerSpeak window has focus. Unset actions are ignored.")
|
||||
.size(11)
|
||||
.color(color_subtext),
|
||||
]
|
||||
.spacing(8)
|
||||
.width(iced::Length::Fill);
|
||||
|
||||
// --- Identity (W7) ---
|
||||
// Your persistent node id + a Regenerate control. When the key isn't
|
||||
// persisted (disk/permission failure → ephemeral fallback) we show a
|
||||
@@ -2163,6 +2399,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
].spacing(8).width(iced::Length::Fill),
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Hotkeys ---
|
||||
section_header("Hotkeys"),
|
||||
hotkey_section,
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Recording ---
|
||||
section_header("Recording"),
|
||||
column![
|
||||
@@ -2345,7 +2586,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.height(iced::Length::Fill)
|
||||
.style(c_style(color_crust, Color::TRANSPARENT, 0.0));
|
||||
|
||||
with_layout_picker(home.into(), state)
|
||||
with_hotkey_info(with_layout_picker(home.into(), state), state)
|
||||
} else {
|
||||
// --- ROOM SCREEN ---
|
||||
let participant_count = state.peers.len() + 1; // peers + you
|
||||
@@ -2643,6 +2884,46 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
].spacing(8).align_y(iced::alignment::Vertical::Center)
|
||||
);
|
||||
|
||||
let peer_key = peer_id.to_string();
|
||||
let current_pan = state.config.peer_pan.get(&peer_key).copied().unwrap_or(0.0);
|
||||
card_content = card_content.push(
|
||||
row![
|
||||
text("Pan:").size(12).color(color_subtext),
|
||||
container(text(pan_label(current_pan)).size(11).color(color_subtext))
|
||||
.width(iced::Length::Fixed(58.0)),
|
||||
slider(-1.0..=1.0, current_pan, move |v| AppMessage::PeerPanChanged(peer_id_clone, v))
|
||||
.step(0.05)
|
||||
.on_release(AppMessage::PersistConfig),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
);
|
||||
|
||||
let eq = peer_eq_settings(&state.config, peer_id);
|
||||
let eq_row = |label: &'static str, band: EqBand, value: f32| -> Element<'_, AppMessage> {
|
||||
row![
|
||||
container(text(format!("{label} {value:+.1} dB")).size(11).color(color_subtext))
|
||||
.width(iced::Length::Fixed(86.0)),
|
||||
slider(EQ_GAIN_DB_MIN..=EQ_GAIN_DB_MAX, value, move |v| {
|
||||
AppMessage::PeerEqChanged(peer_id_clone, band, v)
|
||||
})
|
||||
.step(0.5)
|
||||
.on_release(AppMessage::PersistConfig),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
card_content = card_content.push(
|
||||
column![
|
||||
text("EQ").size(11).color(color_subtext),
|
||||
eq_row("Low", EqBand::Low, eq.low_gain_db),
|
||||
eq_row("Mid", EqBand::Mid, eq.mid_gain_db),
|
||||
eq_row("High", EqBand::High, eq.high_gain_db),
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
|
||||
let card = container(card_content)
|
||||
.style(c_style(
|
||||
if is_speaking { color_base } else { color_mantle },
|
||||
@@ -2696,10 +2977,15 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
checkbox(state.ptt_enabled).label("Push-to-Talk").on_toggle(AppMessage::TogglePtt),
|
||||
vertical_space(10.0),
|
||||
if state.ptt_enabled {
|
||||
let ptt_binding = if state.hotkey_capture == Some(HotkeyAction::PushToTalk) {
|
||||
"Press a key...".to_string()
|
||||
} else {
|
||||
format_binding(state.config.hotkeys.binding(HotkeyAction::PushToTalk))
|
||||
};
|
||||
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)
|
||||
text(format!("PTT key: {ptt_binding}")).size(14).color(color_subtext),
|
||||
button(text("Set PTT Key").size(12).align_x(iced::alignment::Horizontal::Center))
|
||||
.on_press(AppMessage::StartHotkeyCapture(HotkeyAction::PushToTalk))
|
||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||
.padding(8)
|
||||
.width(iced::Length::Fill)
|
||||
@@ -2987,7 +3273,10 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.height(iced::Length::Fill)
|
||||
.style(c_style(color_crust, Color::TRANSPARENT, 0.0));
|
||||
|
||||
with_pixelpass_help(with_layout_picker(room.into(), state), state)
|
||||
with_hotkey_info(
|
||||
with_pixelpass_help(with_layout_picker(room.into(), state), state),
|
||||
state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3357,6 +3646,90 @@ fn with_layout_picker<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Overlay the live hotkey reference from the top-right info button. It reads
|
||||
/// directly from config, so Settings edits are reflected immediately.
|
||||
fn with_hotkey_info<'a>(
|
||||
base: Element<'a, AppMessage>,
|
||||
state: &'a AppState,
|
||||
) -> Element<'a, AppMessage> {
|
||||
if !state.hotkey_info_open {
|
||||
return base;
|
||||
}
|
||||
let pal = state.config.theme.palette();
|
||||
let crust = pal.crust;
|
||||
let mantle = pal.mantle;
|
||||
let surface = pal.surface;
|
||||
let text_c = pal.text;
|
||||
let subtext = pal.subtext;
|
||||
let blue = pal.blue;
|
||||
|
||||
let backdrop = mouse_area(
|
||||
container(horizontal_space())
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.style(move |_t: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color { a: 0.25, ..crust })),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.on_press(AppMessage::CloseHotkeyInfo);
|
||||
|
||||
let mut rows = Column::new().spacing(8).width(iced::Length::Fill);
|
||||
for action in HotkeyAction::ALL {
|
||||
rows = rows.push(
|
||||
row![
|
||||
text(action.label()).size(12).color(text_c),
|
||||
horizontal_space(),
|
||||
text(format_binding(state.config.hotkeys.binding(action)))
|
||||
.size(12)
|
||||
.color(subtext),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
);
|
||||
}
|
||||
|
||||
let dialog = container(
|
||||
column![
|
||||
row![
|
||||
text("Hotkeys").size(16).color(blue),
|
||||
horizontal_space(),
|
||||
button(text("✕").size(16).color(subtext))
|
||||
.on_press(AppMessage::CloseHotkeyInfo)
|
||||
.style(|_t: &Theme, _s: button::Status| button::Style {
|
||||
background: None,
|
||||
..Default::default()
|
||||
})
|
||||
.padding(2),
|
||||
]
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
rows,
|
||||
]
|
||||
.spacing(14),
|
||||
)
|
||||
.style(move |_t: &Theme| container::Style {
|
||||
text_color: Some(text_c),
|
||||
background: Some(Background::Color(mantle)),
|
||||
border: Border { color: surface, width: 1.0, radius: 8.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.padding(16)
|
||||
.width(iced::Length::Fixed(320.0));
|
||||
|
||||
stack![
|
||||
base,
|
||||
backdrop,
|
||||
container(column![
|
||||
vertical_space(48.0),
|
||||
row![horizontal_space(), dialog].width(iced::Length::Fill),
|
||||
])
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.padding(12),
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Overlays the "screen sharing needs pixelpass" explainer popup over `base`
|
||||
/// when open (A11). Triggered by the Share Screen / Watch controls when the
|
||||
/// optional `pixelpass` companion isn't installed, so those controls open a
|
||||
@@ -3725,6 +4098,7 @@ enum IconKind {
|
||||
Chat,
|
||||
People,
|
||||
Clock,
|
||||
Info,
|
||||
Settings,
|
||||
Copy,
|
||||
Leave,
|
||||
@@ -3994,6 +4368,11 @@ impl Program<AppMessage> for Icon {
|
||||
f.stroke(&poly(&[(12.0, 7.0), (12.0, 12.0)], false), stk());
|
||||
f.stroke(&poly(&[(12.0, 12.0), (15.5, 14.0)], false), stk());
|
||||
}
|
||||
IconKind::Info => {
|
||||
f.stroke(&Path::circle(p(12.0, 12.0), 8.5 * s), stk());
|
||||
f.stroke(&poly(&[(12.0, 10.5), (12.0, 17.0)], false), stk());
|
||||
f.fill(&Path::circle(p(12.0, 7.0), 1.1 * s), col);
|
||||
}
|
||||
IconKind::Settings => {
|
||||
f.stroke(&poly(&[(4.0, 7.0), (20.0, 7.0)], false), stk());
|
||||
f.stroke(&poly(&[(4.0, 12.0), (20.0, 12.0)], false), stk());
|
||||
|
||||
Reference in New Issue
Block a user