A9: right-click context menu (Cut/Copy/Paste/Select All) for all text fields
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled

iced 0.14 ships no native right-click menu on text_input. Add a custom
ContextInput widget (src/widget/context_input.rs) that wraps text_input,
intercepts right-click to read the inner text_input::State selection, and
renders a themed 4-action overlay menu operating on that selection.

- Pure, grapheme-indexed edit seam (copy/cut/paste/select_all over
  iced text_input::Value), unit-tested for ASCII and multi-byte/emoji.
- iced::advanced Widget + overlay::Overlay; clipboard via &mut dyn
  Clipboard, edits published through the existing on_input/on_paste.
- Cut/Copy disabled on empty selection (and on secure fields), Select
  All disabled on empty field, Paste always enabled; dismiss on
  click-out / Esc / item-click.
- Route all 10 text_input call sites in app/mod.rs through context_input.
- Cargo.toml: enable iced "advanced" feature (same crate, no new dep).

459 lib tests (+5), clippy --all-targets clean, release green.

Implemented by Codex (gpt-5.5), senior-audited against the 5-point brief
and re-verified (tests/clippy/release) here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Codex (gpt-5.5) <noreply@openai.com>
This commit is contained in:
2026-06-27 05:00:47 -04:00
co-authored by Claude Opus 4.8 Codex
parent ebfc39de46
commit a4bb6ce0be
5 changed files with 910 additions and 12 deletions
+13 -11
View File
@@ -11,12 +11,14 @@ 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 iced::widget::{
container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list,
container, column, row, text, button, scrollable, slider, checkbox, pick_list,
radio, tooltip, progress_bar, canvas, Canvas, Column, stack, mouse_area,
rich_text, span, responsive,
};
use iced::widget::text_input;
use iced::widget::canvas::{Action, Frame, Geometry, Path, Program};
use iced::{
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard, mouse,
@@ -2624,7 +2626,7 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
let nickname_input = column![
text("Nickname").size(14).color(color_subtext),
vertical_space(4.0),
text_input("Enter nickname...", &state.name)
context_input("Enter nickname...", &state.name)
.on_input(AppMessage::NicknameChanged)
.style(t_style)
.padding(10)
@@ -2633,7 +2635,7 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
// Optional cosmetic room label (W7) above the Create button: it rides in the
// minted ticket so everyone who joins inherits "in <name>". Enter also creates.
let create_group = column![
text_input("Room name (optional)", &state.room_name_input)
context_input("Room name (optional)", &state.room_name_input)
.on_input(AppMessage::RoomNameChanged)
.on_submit(AppMessage::CreatePressed)
.style(t_style)
@@ -2649,7 +2651,7 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
let join_group = column![
text("Join Existing Room").size(14).color(color_subtext),
vertical_space(4.0),
text_input("Paste room ticket here...", &state.ticket_input)
context_input("Paste room ticket here...", &state.ticket_input)
.on_input(AppMessage::TicketInputChanged)
.style(t_style)
.padding(10),
@@ -2875,7 +2877,7 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
};
friend_rows = friend_rows.push(
row![
text_input("name", &f.name)
context_input("name", &f.name)
.on_input(move |v| AppMessage::RenameFriend(fid, v))
.style(t_style)
.padding(6)
@@ -2909,13 +2911,13 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
};
let add_form = column![
text_input("Friend's node ID", &state.friend_add_id)
context_input("Friend's node ID", &state.friend_add_id)
.on_input(AppMessage::FriendAddIdChanged)
.style(t_style)
.padding(6),
vertical_space(6.0),
row![
text_input("Name (optional)", &state.friend_add_name)
context_input("Name (optional)", &state.friend_add_name)
.on_input(AppMessage::FriendAddNameChanged)
.style(t_style)
.padding(6)
@@ -3164,7 +3166,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
horizontal_space(),
validation_widget,
].spacing(6).align_y(iced::alignment::Vertical::Center),
text_input("Default (embedded)...", path)
context_input("Default (embedded)...", path)
.on_input(move |val| AppMessage::CustomSoundPathChanged(sound, val))
.style(t_style)
.padding(8)
@@ -3834,10 +3836,10 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
text("Steam games are detected automatically. For other launchers, map an executable name to a display name.")
.size(11).color(color_subtext),
row![
text_input("executable (e.g. hl2_linux)", &state.game_map_exe_input)
context_input("executable (e.g. hl2_linux)", &state.game_map_exe_input)
.on_input(AppMessage::GameMapExeChanged)
.width(iced::Length::Fill),
text_input("shown name (e.g. Half-Life 2)", &state.game_map_name_input)
context_input("shown name (e.g. Half-Life 2)", &state.game_map_name_input)
.on_input(AppMessage::GameMapNameChanged)
.width(iced::Length::Fill),
button(text("Add").size(13)).on_press(AppMessage::AddGameMapping),
@@ -4836,7 +4838,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.on_press(AppMessage::PickAttachmentFile)
.style(b_style(color_surface, color_overlay, color_text, 6.0))
.padding(8),
text_input("Message the room…", &state.chat_input)
context_input("Message the room…", &state.chat_input)
.on_input(AppMessage::ChatInputChanged)
.on_submit(AppMessage::ChatSubmit)
.style(t_style)