W21 Phase 1: locked selectable display fields for node ID + ticket

Add a read-only-but-selectable "locked" mode to the A9 ContextInput so
share-critical values (full node ID, full room ticket) can be drag-selected
and copied with the mouse/keyboard, in addition to the existing one-click
Copy buttons (which are kept).

- context_input.rs: add `locked` flag + builder + `locked_value(value, noop)`
  constructor. A controlled text_input with a no-op on_input stays focusable
  and selection-capable while never mutating (iced treats on_input==None as
  Disabled, verified against iced_widget-0.14.2 source).
- Extract overlay gating into a pure `menu_action_enabled` seam: when locked,
  Cut/Paste are disabled, Copy is enabled with a (non-secure) selection, and
  Select All is enabled when there's a value. +1 unit test.
- app/mod.rs: add AppMessage::Noop; render the full node ID and full ticket in
  width-capped locked fields beside their existing Copy buttons.

Phase 2 (cross-message selectable chat transcript) intentionally deferred:
it requires a transcript-level custom widget that owns selection/layout/hit-
testing while preserving A13 links and attachment rows — out of scope for a
bounded edit. Design path recorded in the Codex task report.

Tests-green only (460 lib, clippy clean, release build green); wants a quick
field check of mouse drag-select + right-click Copy + Ctrl+A/C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 15:53:33 -04:00
co-authored by Claude Opus 4.8
parent a4bb6ce0be
commit a6d9a8cbd4
2 changed files with 77 additions and 12 deletions
+21 -5
View File
@@ -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> {
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<AppMessage> = 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),