diff --git a/src/app/mod.rs b/src/app/mod.rs index db19f3d..f79500f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,7 +11,7 @@ use crate::config::{AppConfig, NetworkMode, RecordingMode, RoomLayout}; use crate::hotkeys::{format_binding, HotkeyAction, HotkeyContext, KeyBinding}; use crate::presence::PresenceMode; use crate::theme::{AppTheme, Palette}; -use crate::widget::context_input::context_input; +use crate::widget::context_input::{context_input, locked_value}; use iced::widget::{ container, column, row, text, button, scrollable, slider, checkbox, pick_list, @@ -313,6 +313,8 @@ pub enum AppMessage { CopyToClipboard, /// Copy an arbitrary string to the clipboard (e.g. the full node ID). CopyText(String), + /// No-op message for controlled read-only selectable fields. + Noop, TogglePtt(bool), StartHotkeyCapture(HotkeyAction), ClearHotkey(HotkeyAction), @@ -1522,6 +1524,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { AppMessage::CopyText(s) => { return iced::clipboard::write(s); } + AppMessage::Noop => {} AppMessage::TogglePtt(enabled) => { state.ptt_enabled = enabled; let _ = state.controller.send(CoreCommand::SetPttMode(enabled)); @@ -3537,11 +3540,15 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { }) .into() }; - // The ID line shows a short form (iced text isn't selectable) plus a Copy - // button that puts the FULL node id on the clipboard, so it's shareable. + // The ID line exposes the full value in a locked selectable field while + // keeping the one-click Copy button for fast whole-ID copy. let id_row: Element = match state.self_node_id.clone() { Some(full) => row![ - text(format!("ID: {id_display}")).size(13).color(color_text), + text("ID:").size(13).color(color_text), + locked_value(&full, AppMessage::Noop) + .width(iced::Length::Fixed(260.0)) + .size(13) + .padding(4), button( row![ icon(IconKind::Copy, 13.0, color_text), @@ -3550,7 +3557,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { .spacing(5) .align_y(iced::alignment::Vertical::Center) ) - .on_press(AppMessage::CopyText(full)) + .on_press(AppMessage::CopyText(full.clone())) .style(b_style(color_surface, color_blue, color_text, 6.0)) .padding(6), ] @@ -4103,6 +4110,15 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { text(format!("My ID: {}", short_id(&state.self_id))) .size(14) .color(color_subtext), + row![ + text("Ticket:").size(12).color(color_subtext), + locked_value(&state.ticket, AppMessage::Noop) + .width(iced::Length::Fixed(260.0)) + .size(12) + .padding(4), + ] + .spacing(6) + .align_y(iced::alignment::Vertical::Center), button( row![ icon(IconKind::Copy, 14.0, color_text), diff --git a/src/widget/context_input.rs b/src/widget/context_input.rs index d184a4c..5e124b8 100644 --- a/src/widget/context_input.rs +++ b/src/widget/context_input.rs @@ -105,6 +105,20 @@ where ContextInput::new(placeholder, value) } +pub fn locked_value<'a, Message, Theme, Renderer>( + value: &str, + noop: Message, +) -> ContextInput<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Theme: text_input::Catalog + 'a, + Renderer: text::Renderer, +{ + ContextInput::new("", value) + .on_input(move |_| noop.clone()) + .locked(true) +} + pub struct ContextInput< 'a, Message, @@ -117,6 +131,7 @@ pub struct ContextInput< input: text_input::TextInput<'a, Message, Theme, Renderer>, value: String, is_secure: bool, + locked: bool, on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, style: Option>, @@ -134,6 +149,7 @@ where input: text_input::TextInput::new(placeholder, value), value: value.to_owned(), is_secure: false, + locked: false, on_input: None, on_paste: None, style: None, @@ -151,6 +167,11 @@ where self } + pub fn locked(mut self, yes: bool) -> Self { + self.locked = yes; + self + } + pub fn on_input( mut self, on_input: impl Fn(String) -> Message + 'a, @@ -448,6 +469,7 @@ where input_state, value: &self.value, is_secure: self.is_secure, + locked: self.locked, on_input: self.on_input.clone(), on_paste: self.on_paste.clone(), style: self.style.clone(), @@ -479,6 +501,7 @@ where input_state: &'a mut text_input::State, value: &'a str, is_secure: bool, + locked: bool, on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, style: Option>, @@ -686,13 +709,13 @@ where let has_selection = menu.selection.0 != menu.selection.1; let has_value = !text_input::Value::new(self.value).is_empty(); - match action { - MenuAction::Cut | MenuAction::Copy => { - has_selection && !self.is_secure - } - MenuAction::Paste => true, - MenuAction::SelectAll => has_value, - } + menu_action_enabled( + action, + has_selection, + has_value, + self.is_secure, + self.locked, + ) } fn hit_action( @@ -811,6 +834,21 @@ fn disabled_color(color: Color) -> Color { } } +fn menu_action_enabled( + action: MenuAction, + has_selection: bool, + has_value: bool, + is_secure: bool, + locked: bool, +) -> bool { + match action { + MenuAction::Cut => has_selection && !is_secure && !locked, + MenuAction::Copy => has_selection && !is_secure, + MenuAction::Paste => !locked, + MenuAction::SelectAll => has_value, + } +} + #[cfg(test)] mod tests { use super::*; @@ -891,4 +929,15 @@ mod tests { } ); } + + #[test] + fn locked_menu_allows_copy_and_select_all_only() { + assert!(!menu_action_enabled(MenuAction::Cut, true, true, false, true)); + assert!(menu_action_enabled(MenuAction::Copy, true, true, false, true)); + assert!(!menu_action_enabled(MenuAction::Paste, true, true, false, true)); + assert!(menu_action_enabled(MenuAction::SelectAll, true, true, false, true)); + + assert!(!menu_action_enabled(MenuAction::Copy, false, true, false, true)); + assert!(!menu_action_enabled(MenuAction::SelectAll, false, false, false, true)); + } }