aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/overlay_input.rs
diff options
context:
space:
mode:
authorMark Jaquith <mark@jaquith.me>2026-09-14 15:47:53 -0400
committerGitHub <noreply@github.com>2026-09-14 21:47:53 +0200
commitb0f21ed2cb85613050b794bf6c78a7c367a94e0d (patch)
tree25c14e09338f5300bedbc8131aa23fab3d37add9 /src/client/shell/overlay_input.rs
parentd8b36691d10c6055293ad45324b22af8b8f570e5 (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.rs175
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;
- }
}
}