diff options
| author | Mark Jaquith <mark@jaquith.me> | 2026-09-14 15:47:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-14 21:47:53 +0200 |
| commit | b0f21ed2cb85613050b794bf6c78a7c367a94e0d (patch) | |
| tree | 25c14e09338f5300bedbc8131aa23fab3d37add9 /src/client/shell/overlay_input.rs | |
| parent | d8b36691d10c6055293ad45324b22af8b8f570e5 (diff) | |
feat: add cursor-based editing to text inputs (#3698)
* feat: add cursor-based editing to text inputs
refs #1803
* fix: preserve dialog actions with associated text
refs #1803
* refactor: simplify text editor outcomes and tests
refs #1803
---------
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/client/shell/overlay_input.rs')
| -rw-r--r-- | src/client/shell/overlay_input.rs | 175 |
1 files changed, 59 insertions, 116 deletions
diff --git a/src/client/shell/overlay_input.rs b/src/client/shell/overlay_input.rs index 42e331af..7cf151cb 100644 --- a/src/client/shell/overlay_input.rs +++ b/src/client/shell/overlay_input.rs @@ -200,7 +200,7 @@ impl ClientShellState { }) .collect(); let mut navigator = ClientNavigatorOverlay { - query: String::new(), + query: TextEditor::default(), search_focused: false, selected: None, scroll: 0, @@ -344,8 +344,7 @@ impl ClientShellState { .unwrap_or_else(|| "workspace".to_owned()); self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "new workspace", - input: suggested_name.clone(), - replace_on_type: true, + input: TextEditor::new(&suggested_name, true), target: ClientRenameTarget::NewWorkspace { source_workspace_id, cwd, @@ -370,8 +369,7 @@ impl ClientShellState { }; self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename workspace", - input: workspace.label.clone(), - replace_on_type: false, + input: TextEditor::new(&workspace.label, false), target: ClientRenameTarget::Workspace { workspace_id }, })); } @@ -392,8 +390,7 @@ impl ClientShellState { .to_string(); self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "new tab", - input: default_name.clone(), - replace_on_type: true, + input: TextEditor::new(&default_name, true), target: ClientRenameTarget::NewTab { workspace_id, default_name, @@ -413,8 +410,7 @@ impl ClientShellState { }; self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename tab", - input: tab.label.clone(), - replace_on_type: false, + input: TextEditor::new(&tab.label, false), target: ClientRenameTarget::Tab { tab_id: tab.tab_id.clone(), auto_name: !tab.custom_label, @@ -435,8 +431,10 @@ impl ClientShellState { }; self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename pane", - input: pane.label.clone().unwrap_or_default(), - replace_on_type: pane.label.is_none(), + input: TextEditor::new( + pane.label.as_deref().unwrap_or_default(), + pane.label.is_none(), + ), target: ClientRenameTarget::Pane { pane_id: pane.pane_id.clone(), }, @@ -449,23 +447,20 @@ impl ClientShellState { } match self.overlay.as_mut() { Some(ClientShellOverlay::Rename(rename)) => { - if rename.replace_on_type { - rename.input.clear(); - rename.replace_on_type = false; - } - rename.input.push_str(text); + rename.input.insert(text); true } Some(ClientShellOverlay::Help(help)) if help.search_focused => { - help.query - .extend(text.chars().filter(|character| !character.is_control())); - help.scroll = 0; + if help.query.insert(text) { + help.scroll = 0; + } true } Some(ClientShellOverlay::Navigator(navigator)) if navigator.search_focused => { - navigator.query.push_str(text); - navigator.filter = None; - navigator.selected = None; + if navigator.query.insert(text) { + navigator.filter = None; + navigator.selected = None; + } true } _ => false, @@ -655,42 +650,30 @@ impl ClientShellState { return; } if search_focused { + if let Some(ClientShellOverlay::Navigator(navigator)) = self.overlay.as_mut() { + if let Some(content_changed) = navigator.query.handle_key(key) { + if content_changed { + navigator.filter = None; + navigator.selected = None; + } + outcome.repaint = true; + return; + } + } if code == KeyCode::Up - || code == KeyCode::Char('p') && modifiers.contains(KeyModifiers::CONTROL) + || code == KeyCode::Char('p') && modifiers == KeyModifiers::CONTROL { self.move_navigator_selection(-1); outcome.repaint = true; return; } if code == KeyCode::Down - || code == KeyCode::Char('n') && modifiers.contains(KeyModifiers::CONTROL) + || code == KeyCode::Char('n') && modifiers == KeyModifiers::CONTROL { self.move_navigator_selection(1); outcome.repaint = true; return; } - if let Some(ClientShellOverlay::Navigator(navigator)) = self.overlay.as_mut() { - if code == KeyCode::Char('u') && modifiers.contains(KeyModifiers::CONTROL) { - navigator.query.clear(); - navigator.filter = None; - navigator.selected = None; - } else if code == KeyCode::Backspace { - navigator.query.pop(); - navigator.filter = None; - navigator.selected = None; - } else if let KeyCode::Char(character) = code { - if modifiers.difference(KeyModifiers::SHIFT).is_empty() { - navigator.filter = None; - if let Some(text) = key.generated_text.as_deref() { - navigator.query.push_str(text); - } else { - navigator.query.push(character); - } - navigator.selected = None; - } - } - outcome.repaint = true; - } return; } if code == KeyCode::Backspace && modifiers.is_empty() { @@ -798,6 +781,15 @@ impl ClientShellState { })) ); if search_focused { + if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { + if let Some(content_changed) = help.query.handle_key(key) { + if content_changed { + help.scroll = 0; + } + outcome.repaint = true; + return; + } + } match code { KeyCode::Esc => { if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { @@ -807,20 +799,17 @@ impl ClientShellState { } } KeyCode::Enter => self.overlay = None, - KeyCode::Home => { - if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { - help.scroll = 0; - } - } - KeyCode::End => { - if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { - help.scroll = self.hits.help_max_scroll; - } - } - KeyCode::Up | KeyCode::Down | KeyCode::PageUp | KeyCode::PageDown => { + KeyCode::Up + | KeyCode::Down + | KeyCode::PageUp + | KeyCode::PageDown + | KeyCode::Char('n' | 'p') + if !matches!(code, KeyCode::Char(_)) + || modifiers == KeyModifiers::CONTROL => + { let delta = match code { - KeyCode::Up => -1, - KeyCode::Down => 1, + KeyCode::Up | KeyCode::Char('p') => -1, + KeyCode::Down | KeyCode::Char('n') => 1, KeyCode::PageUp => -8, KeyCode::PageDown => 8, _ => unreachable!(), @@ -832,26 +821,7 @@ impl ClientShellState { .min(self.hits.help_max_scroll); } } - KeyCode::Backspace => { - if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { - help.query.pop(); - help.scroll = 0; - } - } - KeyCode::Char('u') if modifiers == KeyModifiers::CONTROL => { - if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { - help.query.clear(); - help.scroll = 0; - } - } - _ => { - if let Some(character) = text_character { - if let Some(ClientShellOverlay::Help(help)) = self.overlay.as_mut() { - help.query.push(character); - help.scroll = 0; - } - } - } + _ => {} } outcome.repaint = true; return; @@ -939,53 +909,26 @@ impl ClientShellState { outcome.repaint = true; return; } - if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { - rename.input.clear(); - rename.replace_on_type = false; - outcome.repaint = true; + if key + .generated_text + .as_deref() + .is_some_and(|text| !text.is_empty()) + { + outcome.repaint |= rename.input.handle_key(key).is_some(); return; } - if (key.code == KeyCode::Char('u') && key.modifiers.contains(KeyModifiers::CONTROL)) - || (key.code == KeyCode::Backspace && key.modifiers.contains(KeyModifiers::SUPER)) - { + if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL { rename.input.clear(); - rename.replace_on_type = false; outcome.repaint = true; return; } - if key.code == KeyCode::Backspace - && (key.modifiers.contains(KeyModifiers::CONTROL) - || key.modifiers.contains(KeyModifiers::ALT)) - || matches!(key.code, KeyCode::Char('h' | 'w')) - && key.modifiers.contains(KeyModifiers::CONTROL) - { - delete_overlay_word(rename); + if key.code == KeyCode::Backspace && key.modifiers.contains(KeyModifiers::SUPER) { + rename.input.clear(); outcome.repaint = true; return; } - if key.code == KeyCode::Backspace { - if rename.replace_on_type { - rename.input.clear(); - rename.replace_on_type = false; - } else { - rename.input.pop(); - } + if rename.input.handle_key(key).is_some() { outcome.repaint = true; - return; - } - if let KeyCode::Char(character) = key.code { - if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() { - if rename.replace_on_type { - rename.input.clear(); - rename.replace_on_type = false; - } - if let Some(text) = key.generated_text.as_deref() { - rename.input.push_str(text); - } else { - rename.input.push(character); - } - outcome.repaint = true; - } } } |
