custome input
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -1171,6 +1171,7 @@ dependencies = [
|
|||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
|
"input",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"linkify",
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
@@ -3675,6 +3676,22 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "input"
|
||||||
|
version = "1.0.0-beta4"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"gpui",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"smol",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
state = { path = "../state" }
|
state = { path = "../state" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
|
input = { path = "../input" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use gpui::{
|
|||||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||||
deferred, div, img, list, px, red, relative, svg, white,
|
deferred, div, img, list, px, red, relative, svg, white,
|
||||||
};
|
};
|
||||||
|
use input::{Input, InputState, InputStateEvent, input};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
@@ -24,7 +25,6 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
@@ -116,15 +116,17 @@ impl ChatPanel {
|
|||||||
|
|
||||||
// Define input state
|
// Define input state
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
let mut this = InputState::new_multiline(cx);
|
||||||
.placeholder(format!("Message {}", name))
|
this.set_placeholder(format!("Message {}", name), cx);
|
||||||
.auto_grow(1, 20)
|
this
|
||||||
.prevent_new_line_on_enter()
|
|
||||||
.clean_on_escape()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define subject input state
|
// Define subject input state
|
||||||
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
let subject_input = cx.new(|cx| {
|
||||||
|
let mut this = InputState::new(cx);
|
||||||
|
this.set_placeholder("New subject...", cx);
|
||||||
|
this
|
||||||
|
});
|
||||||
let subject_bar = cx.new(|_cx| false);
|
let subject_bar = cx.new(|_cx| false);
|
||||||
|
|
||||||
// Define subscriptions
|
// Define subscriptions
|
||||||
@@ -133,7 +135,7 @@ impl ChatPanel {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe the chat input event
|
// Subscribe the chat input event
|
||||||
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
if let InputStateEvent::Enter = event {
|
||||||
this.send_text_message(window, cx);
|
this.send_text_message(window, cx);
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -145,7 +147,7 @@ impl ChatPanel {
|
|||||||
&subject_input,
|
&subject_input,
|
||||||
window,
|
window,
|
||||||
move |this, _input, event, window, cx| {
|
move |this, _input, event, window, cx| {
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
if let InputStateEvent::Enter = event {
|
||||||
this.change_subject(window, cx);
|
this.change_subject(window, cx);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -294,7 +296,7 @@ impl ChatPanel {
|
|||||||
/// Get user input content and merged all attachments if available
|
/// Get user input content and merged all attachments if available
|
||||||
fn get_input_value(&self, cx: &Context<Self>) -> String {
|
fn get_input_value(&self, cx: &Context<Self>) -> String {
|
||||||
// Get input's value
|
// Get input's value
|
||||||
let mut content = self.input.read(cx).value().trim().to_string();
|
let mut content = self.input.read(cx).content().trim().to_string();
|
||||||
|
|
||||||
// Get all attaches and merge its with message
|
// Get all attaches and merge its with message
|
||||||
let attachments = self.attachments.read(cx);
|
let attachments = self.attachments.read(cx);
|
||||||
@@ -317,7 +319,7 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn change_subject(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn change_subject(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let subject = self.subject_input.read(cx).value();
|
let subject = self.subject_input.read(cx).content().to_string();
|
||||||
|
|
||||||
self.room
|
self.room
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
@@ -414,9 +416,9 @@ impl ChatPanel {
|
|||||||
/// Clear the input field, attachments, and replies
|
/// Clear the input field, attachments, and replies
|
||||||
///
|
///
|
||||||
/// Only run after sending a message
|
/// Only run after sending a message
|
||||||
fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.input.update(cx, |this, cx| {
|
self.input.update(cx, |this, cx| {
|
||||||
this.set_value("", window, cx);
|
this.set_content("", cx);
|
||||||
});
|
});
|
||||||
self.attachments.update(cx, |this, cx| {
|
self.attachments.update(cx, |this, cx| {
|
||||||
this.clear();
|
this.clear();
|
||||||
@@ -1497,12 +1499,7 @@ impl Render for ChatPanel {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(
|
.child(input(&self.subject_input, cx).text_sm())
|
||||||
TextInput::new(&self.subject_input)
|
|
||||||
.text_sm()
|
|
||||||
.small()
|
|
||||||
.bordered(false),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("change")
|
Button::new("change")
|
||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CheckCircle)
|
||||||
@@ -1553,12 +1550,7 @@ impl Render for ChatPanel {
|
|||||||
this.upload(window, cx);
|
this.upload(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(input(&self.input, cx).text_sm().flex_1())
|
||||||
TextInput::new(&self.input)
|
|
||||||
.appearance(false)
|
|
||||||
.text_sm()
|
|
||||||
.flex_1(),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
|
|||||||
19
crates/input/Cargo.toml
Normal file
19
crates/input/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "input"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
|
unicode-bidi = "0.3"
|
||||||
166
crates/input/src/bidi.rs
Normal file
166
crates/input/src/bidi.rs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
use unicode_bidi::{BidiClass, bidi_class};
|
||||||
|
|
||||||
|
/// Text direction for bidirectional text support.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TextDirection {
|
||||||
|
/// Left-to-right text direction (default for Latin, Greek, Cyrillic, etc.)
|
||||||
|
#[default]
|
||||||
|
Ltr,
|
||||||
|
/// Right-to-left text direction (for Arabic, Hebrew, etc.)
|
||||||
|
Rtl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextDirection {
|
||||||
|
/// Returns true if this is left-to-right direction.
|
||||||
|
pub fn is_ltr(self) -> bool {
|
||||||
|
matches!(self, TextDirection::Ltr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this is right-to-left direction.
|
||||||
|
pub fn is_rtl(self) -> bool {
|
||||||
|
matches!(self, TextDirection::Rtl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects the base direction of text using the first strong directional character.
|
||||||
|
///
|
||||||
|
/// This follows the Unicode Bidirectional Algorithm (UBA) rule P2/P3:
|
||||||
|
/// - Find the first character with a strong directional type (L, R, or AL)
|
||||||
|
/// - If it's L, the paragraph direction is LTR
|
||||||
|
/// - If it's R or AL, the paragraph direction is RTL
|
||||||
|
/// - If no strong character is found, defaults to LTR
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use gpui::input::bidi::detect_base_direction;
|
||||||
|
///
|
||||||
|
/// // English text is LTR
|
||||||
|
/// assert!(detect_base_direction("Hello world").is_ltr());
|
||||||
|
///
|
||||||
|
/// // Arabic text is RTL
|
||||||
|
/// assert!(detect_base_direction("مرحبا").is_rtl());
|
||||||
|
///
|
||||||
|
/// // Hebrew text is RTL
|
||||||
|
/// assert!(detect_base_direction("שלום").is_rtl());
|
||||||
|
///
|
||||||
|
/// // Mixed text uses first strong character
|
||||||
|
/// assert!(detect_base_direction("Hello مرحبا").is_ltr());
|
||||||
|
/// assert!(detect_base_direction("مرحبا Hello").is_rtl());
|
||||||
|
///
|
||||||
|
/// // Empty or neutral-only text defaults to LTR
|
||||||
|
/// assert!(detect_base_direction("").is_ltr());
|
||||||
|
/// assert!(detect_base_direction("123").is_ltr());
|
||||||
|
/// ```
|
||||||
|
pub fn detect_base_direction(text: &str) -> TextDirection {
|
||||||
|
for c in text.chars() {
|
||||||
|
match bidi_class(c) {
|
||||||
|
BidiClass::L => return TextDirection::Ltr,
|
||||||
|
BidiClass::R | BidiClass::AL => return TextDirection::Rtl,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextDirection::Ltr
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_string() {
|
||||||
|
assert_eq!(detect_base_direction(""), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_whitespace_only() {
|
||||||
|
assert_eq!(detect_base_direction(" "), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_numbers_only() {
|
||||||
|
assert_eq!(detect_base_direction("12345"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_punctuation_only() {
|
||||||
|
assert_eq!(detect_base_direction("!@#$%"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_latin_text() {
|
||||||
|
assert_eq!(detect_base_direction("Hello world"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arabic_text() {
|
||||||
|
assert_eq!(detect_base_direction("مرحبا بالعالم"), TextDirection::Rtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hebrew_text() {
|
||||||
|
assert_eq!(detect_base_direction("שלום עולם"), TextDirection::Rtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_ltr_first() {
|
||||||
|
assert_eq!(detect_base_direction("Hello مرحبا"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_rtl_first() {
|
||||||
|
assert_eq!(detect_base_direction("مرحبا Hello"), TextDirection::Rtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_numbers_before_arabic() {
|
||||||
|
assert_eq!(detect_base_direction("123 مرحبا"), TextDirection::Rtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_numbers_before_latin() {
|
||||||
|
assert_eq!(detect_base_direction("123 Hello"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_punctuation_before_hebrew() {
|
||||||
|
assert_eq!(detect_base_direction("... שלום"), TextDirection::Rtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_greek_text() {
|
||||||
|
assert_eq!(detect_base_direction("Γειά σου κόσμε"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cyrillic_text() {
|
||||||
|
assert_eq!(detect_base_direction("Привет мир"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chinese_text() {
|
||||||
|
assert_eq!(detect_base_direction("你好世界"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_japanese_text() {
|
||||||
|
assert_eq!(detect_base_direction("こんにちは"), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_is_ltr() {
|
||||||
|
assert!(TextDirection::Ltr.is_ltr());
|
||||||
|
assert!(!TextDirection::Rtl.is_ltr());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_is_rtl() {
|
||||||
|
assert!(TextDirection::Rtl.is_rtl());
|
||||||
|
assert!(!TextDirection::Ltr.is_rtl());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_default() {
|
||||||
|
assert_eq!(TextDirection::default(), TextDirection::Ltr);
|
||||||
|
}
|
||||||
|
}
|
||||||
512
crates/input/src/bindings.rs
Normal file
512
crates/input/src/bindings.rs
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
use gpui::{App, KeyBinding, actions};
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
input,
|
||||||
|
[
|
||||||
|
/// Delete the character before the cursor.
|
||||||
|
Backspace,
|
||||||
|
/// Delete the character after the cursor.
|
||||||
|
Delete,
|
||||||
|
/// Blur focus from the input.
|
||||||
|
Escape,
|
||||||
|
/// Delete the word before the cursor.
|
||||||
|
DeleteWordLeft,
|
||||||
|
/// Delete the word after the cursor.
|
||||||
|
DeleteWordRight,
|
||||||
|
/// Delete from the cursor to the beginning of the line.
|
||||||
|
DeleteToBeginningOfLine,
|
||||||
|
/// Delete from the cursor to the end of the line.
|
||||||
|
DeleteToEndOfLine,
|
||||||
|
/// Insert a tab character at the cursor position.
|
||||||
|
Tab,
|
||||||
|
/// Move the cursor one character to the left.
|
||||||
|
Left,
|
||||||
|
/// Move the cursor one character to the right.
|
||||||
|
Right,
|
||||||
|
/// Move the cursor up one visual line.
|
||||||
|
Up,
|
||||||
|
/// Move the cursor down one visual line.
|
||||||
|
Down,
|
||||||
|
/// Extend selection one character to the left.
|
||||||
|
SelectLeft,
|
||||||
|
/// Extend selection one character to the right.
|
||||||
|
SelectRight,
|
||||||
|
/// Extend selection up one visual line.
|
||||||
|
SelectUp,
|
||||||
|
/// Extend selection down one visual line.
|
||||||
|
SelectDown,
|
||||||
|
/// Select all text content.
|
||||||
|
SelectAll,
|
||||||
|
/// Move cursor to the start of the current line.
|
||||||
|
Home,
|
||||||
|
/// Move cursor to the end of the current line.
|
||||||
|
End,
|
||||||
|
/// Extend selection to the beginning of the content.
|
||||||
|
SelectToBeginning,
|
||||||
|
/// Extend selection to the end of the content.
|
||||||
|
SelectToEnd,
|
||||||
|
/// Move cursor to the beginning of the content.
|
||||||
|
MoveToBeginning,
|
||||||
|
/// Move cursor to the end of the content.
|
||||||
|
MoveToEnd,
|
||||||
|
/// Paste from clipboard at the cursor position.
|
||||||
|
Paste,
|
||||||
|
/// Cut selected text to clipboard.
|
||||||
|
Cut,
|
||||||
|
/// Copy selected text to clipboard.
|
||||||
|
Copy,
|
||||||
|
/// Insert a newline at the cursor position.
|
||||||
|
Enter,
|
||||||
|
/// Move cursor one word to the left.
|
||||||
|
WordLeft,
|
||||||
|
/// Move cursor one word to the right.
|
||||||
|
WordRight,
|
||||||
|
/// Extend selection one word to the left.
|
||||||
|
SelectWordLeft,
|
||||||
|
/// Extend selection one word to the right.
|
||||||
|
SelectWordRight,
|
||||||
|
/// Undo the last edit.
|
||||||
|
Undo,
|
||||||
|
/// Redo the last undone edit.
|
||||||
|
Redo,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The key context used for input element keybindings.
|
||||||
|
pub const INPUT_CONTEXT: &str = "Input";
|
||||||
|
|
||||||
|
/// Keybindings configuration for input elements.
|
||||||
|
///
|
||||||
|
/// Each field is an `Option<KeyBinding>` to allow:
|
||||||
|
/// - Using defaults (via `Default::default()`)
|
||||||
|
/// - Overriding with custom bindings
|
||||||
|
/// - Unbinding keys by setting fields to `None`
|
||||||
|
///
|
||||||
|
/// The `Default` implementation returns platform-specific keybindings.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct InputBindings {
|
||||||
|
/// Binding for deleting the character before the cursor.
|
||||||
|
/// Default: `backspace`
|
||||||
|
pub backspace: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for deleting the character after the cursor.
|
||||||
|
/// Default: `delete`
|
||||||
|
pub delete: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for deleting the word before the cursor.
|
||||||
|
/// Default: `alt-backspace` (macOS) / `ctrl-backspace` (other platforms)
|
||||||
|
pub delete_word_left: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for deleting the word after the cursor.
|
||||||
|
/// Default: `alt-delete` (macOS) / `ctrl-delete` (other platforms)
|
||||||
|
pub delete_word_right: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for deleting from cursor to beginning of line.
|
||||||
|
/// Default: `cmd-backspace` (macOS) / `ctrl-shift-backspace` (other platforms)
|
||||||
|
pub delete_to_beginning_of_line: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for deleting from cursor to end of line.
|
||||||
|
/// Default: `ctrl-k` (macOS) / `ctrl-shift-delete` (other platforms)
|
||||||
|
pub delete_to_end_of_line: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for inserting a tab character.
|
||||||
|
/// Default: `tab`
|
||||||
|
pub tab: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for inserting a newline (multi-line) or confirming input (single-line).
|
||||||
|
/// Default: `enter`
|
||||||
|
pub enter: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving the cursor one character to the left.
|
||||||
|
/// Default: `left`
|
||||||
|
pub left: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving the cursor one character to the right.
|
||||||
|
/// Default: `right`
|
||||||
|
pub right: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving the cursor up one line (multi-line) or to the start of the line (single-line).
|
||||||
|
/// Default: `up`
|
||||||
|
pub up: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving the cursor down one line (multi-line) or to the end of the line (single-line).
|
||||||
|
/// Default: `down`
|
||||||
|
pub down: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection one character to the left.
|
||||||
|
/// Default: `shift-left`
|
||||||
|
pub select_left: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection one character to the right.
|
||||||
|
/// Default: `shift-right`
|
||||||
|
pub select_right: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection up one line (multi-line) or to the start (single-line).
|
||||||
|
/// Default: `shift-up`
|
||||||
|
pub select_up: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection down one line (multi-line) or to the end (single-line).
|
||||||
|
/// Default: `shift-down`
|
||||||
|
pub select_down: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for selecting all text content.
|
||||||
|
/// Default: `cmd-a` (macOS) / `ctrl-a` (other platforms)
|
||||||
|
pub select_all: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor to the start of the current line.
|
||||||
|
/// Default: `home`
|
||||||
|
pub home: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor to the end of the current line.
|
||||||
|
/// Default: `end`
|
||||||
|
pub end: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor to the beginning of all content.
|
||||||
|
/// Default: `cmd-up` (macOS) / `ctrl-home` (other platforms)
|
||||||
|
pub move_to_beginning: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor to the end of all content.
|
||||||
|
/// Default: `cmd-down` (macOS) / `ctrl-end` (other platforms)
|
||||||
|
pub move_to_end: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection to the beginning of all content.
|
||||||
|
/// Default: `cmd-shift-up` (macOS) / `ctrl-shift-home` (other platforms)
|
||||||
|
pub select_to_beginning: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection to the end of all content.
|
||||||
|
/// Default: `cmd-shift-down` (macOS) / `ctrl-shift-end` (other platforms)
|
||||||
|
pub select_to_end: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor one word to the left.
|
||||||
|
/// Default: `alt-left` (macOS) / `ctrl-left` (other platforms)
|
||||||
|
pub word_left: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for moving cursor one word to the right.
|
||||||
|
/// Default: `alt-right` (macOS) / `ctrl-right` (other platforms)
|
||||||
|
pub word_right: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection one word to the left.
|
||||||
|
/// Default: `alt-shift-left` (macOS) / `ctrl-shift-left` (other platforms)
|
||||||
|
pub select_word_left: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for extending selection one word to the right.
|
||||||
|
/// Default: `alt-shift-right` (macOS) / `ctrl-shift-right` (other platforms)
|
||||||
|
pub select_word_right: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for copying selected text to clipboard.
|
||||||
|
/// Default: `cmd-c` (macOS) / `ctrl-c` (other platforms)
|
||||||
|
pub copy: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for cutting selected text to clipboard.
|
||||||
|
/// Default: `cmd-x` (macOS) / `ctrl-x` (other platforms)
|
||||||
|
pub cut: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for pasting from clipboard.
|
||||||
|
/// Default: `cmd-v` (macOS) / `ctrl-v` (other platforms)
|
||||||
|
pub paste: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for undoing the last edit.
|
||||||
|
/// Default: `cmd-z` (macOS) / `ctrl-z` (other platforms)
|
||||||
|
pub undo: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for redoing the last undone edit.
|
||||||
|
/// Default: `cmd-shift-z` (macOS) / `ctrl-shift-z` (other platforms)
|
||||||
|
pub redo: Option<KeyBinding>,
|
||||||
|
|
||||||
|
/// Binding for blurring focus from the input.
|
||||||
|
/// Default: `escape`
|
||||||
|
pub escape: Option<KeyBinding>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InputBindings {
|
||||||
|
/// Returns platform-specific default keybindings for input elements.
|
||||||
|
fn default() -> Self {
|
||||||
|
let context = Some(INPUT_CONTEXT);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
|
||||||
|
delete: Some(KeyBinding::new("delete", Delete, context)),
|
||||||
|
delete_word_left: Some(KeyBinding::new("alt-backspace", DeleteWordLeft, context)),
|
||||||
|
delete_word_right: Some(KeyBinding::new("alt-delete", DeleteWordRight, context)),
|
||||||
|
delete_to_beginning_of_line: Some(KeyBinding::new(
|
||||||
|
"cmd-backspace",
|
||||||
|
DeleteToBeginningOfLine,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
delete_to_end_of_line: Some(KeyBinding::new("ctrl-k", DeleteToEndOfLine, context)),
|
||||||
|
tab: Some(KeyBinding::new("tab", Tab, context)),
|
||||||
|
enter: Some(KeyBinding::new("enter", Enter, context)),
|
||||||
|
left: Some(KeyBinding::new("left", Left, context)),
|
||||||
|
right: Some(KeyBinding::new("right", Right, context)),
|
||||||
|
up: Some(KeyBinding::new("up", Up, context)),
|
||||||
|
down: Some(KeyBinding::new("down", Down, context)),
|
||||||
|
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
|
||||||
|
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
|
||||||
|
select_up: Some(KeyBinding::new("shift-up", SelectUp, context)),
|
||||||
|
select_down: Some(KeyBinding::new("shift-down", SelectDown, context)),
|
||||||
|
select_all: Some(KeyBinding::new("cmd-a", SelectAll, context)),
|
||||||
|
home: Some(KeyBinding::new("home", Home, context)),
|
||||||
|
end: Some(KeyBinding::new("end", End, context)),
|
||||||
|
move_to_beginning: Some(KeyBinding::new("cmd-up", MoveToBeginning, context)),
|
||||||
|
move_to_end: Some(KeyBinding::new("cmd-down", MoveToEnd, context)),
|
||||||
|
select_to_beginning: Some(KeyBinding::new(
|
||||||
|
"cmd-shift-up",
|
||||||
|
SelectToBeginning,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
select_to_end: Some(KeyBinding::new("cmd-shift-down", SelectToEnd, context)),
|
||||||
|
word_left: Some(KeyBinding::new("alt-left", WordLeft, context)),
|
||||||
|
word_right: Some(KeyBinding::new("alt-right", WordRight, context)),
|
||||||
|
select_word_left: Some(KeyBinding::new("alt-shift-left", SelectWordLeft, context)),
|
||||||
|
select_word_right: Some(KeyBinding::new(
|
||||||
|
"alt-shift-right",
|
||||||
|
SelectWordRight,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
copy: Some(KeyBinding::new("cmd-c", Copy, context)),
|
||||||
|
cut: Some(KeyBinding::new("cmd-x", Cut, context)),
|
||||||
|
paste: Some(KeyBinding::new("cmd-v", Paste, context)),
|
||||||
|
undo: Some(KeyBinding::new("cmd-z", Undo, context)),
|
||||||
|
redo: Some(KeyBinding::new("cmd-shift-z", Redo, context)),
|
||||||
|
escape: Some(KeyBinding::new("escape", Escape, context)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
|
||||||
|
delete: Some(KeyBinding::new("delete", Delete, context)),
|
||||||
|
delete_word_left: Some(KeyBinding::new("ctrl-backspace", DeleteWordLeft, context)),
|
||||||
|
delete_word_right: Some(KeyBinding::new("ctrl-delete", DeleteWordRight, context)),
|
||||||
|
delete_to_beginning_of_line: Some(KeyBinding::new(
|
||||||
|
"ctrl-shift-backspace",
|
||||||
|
DeleteToBeginningOfLine,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
delete_to_end_of_line: Some(KeyBinding::new(
|
||||||
|
"ctrl-shift-delete",
|
||||||
|
DeleteToEndOfLine,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
tab: Some(KeyBinding::new("tab", Tab, context)),
|
||||||
|
enter: Some(KeyBinding::new("enter", Enter, context)),
|
||||||
|
left: Some(KeyBinding::new("left", Left, context)),
|
||||||
|
right: Some(KeyBinding::new("right", Right, context)),
|
||||||
|
up: Some(KeyBinding::new("up", Up, context)),
|
||||||
|
down: Some(KeyBinding::new("down", Down, context)),
|
||||||
|
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
|
||||||
|
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
|
||||||
|
select_up: Some(KeyBinding::new("shift-up", SelectUp, context)),
|
||||||
|
select_down: Some(KeyBinding::new("shift-down", SelectDown, context)),
|
||||||
|
select_all: Some(KeyBinding::new("ctrl-a", SelectAll, context)),
|
||||||
|
home: Some(KeyBinding::new("home", Home, context)),
|
||||||
|
end: Some(KeyBinding::new("end", End, context)),
|
||||||
|
move_to_beginning: Some(KeyBinding::new("ctrl-home", MoveToBeginning, context)),
|
||||||
|
move_to_end: Some(KeyBinding::new("ctrl-end", MoveToEnd, context)),
|
||||||
|
select_to_beginning: Some(KeyBinding::new(
|
||||||
|
"ctrl-shift-home",
|
||||||
|
SelectToBeginning,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
select_to_end: Some(KeyBinding::new("ctrl-shift-end", SelectToEnd, context)),
|
||||||
|
word_left: Some(KeyBinding::new("ctrl-left", WordLeft, context)),
|
||||||
|
word_right: Some(KeyBinding::new("ctrl-right", WordRight, context)),
|
||||||
|
select_word_left: Some(KeyBinding::new("ctrl-shift-left", SelectWordLeft, context)),
|
||||||
|
select_word_right: Some(KeyBinding::new(
|
||||||
|
"ctrl-shift-right",
|
||||||
|
SelectWordRight,
|
||||||
|
context,
|
||||||
|
)),
|
||||||
|
copy: Some(KeyBinding::new("ctrl-c", Copy, context)),
|
||||||
|
cut: Some(KeyBinding::new("ctrl-x", Cut, context)),
|
||||||
|
paste: Some(KeyBinding::new("ctrl-v", Paste, context)),
|
||||||
|
undo: Some(KeyBinding::new("ctrl-z", Undo, context)),
|
||||||
|
redo: Some(KeyBinding::new("ctrl-shift-z", Redo, context)),
|
||||||
|
escape: Some(KeyBinding::new("escape", Escape, context)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputBindings {
|
||||||
|
/// Creates an empty `InputBindings` with all fields set to `None`.
|
||||||
|
///
|
||||||
|
/// Use this as a starting point when you want to override only specific bindings:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let bindings = InputBindings::empty();
|
||||||
|
/// bindings.select_all = Some(KeyBinding::new("ctrl-shift-a", SelectAll, Some(INPUT_CONTEXT)));
|
||||||
|
/// ```
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
backspace: None,
|
||||||
|
delete: None,
|
||||||
|
delete_word_left: None,
|
||||||
|
delete_word_right: None,
|
||||||
|
delete_to_beginning_of_line: None,
|
||||||
|
delete_to_end_of_line: None,
|
||||||
|
tab: None,
|
||||||
|
enter: None,
|
||||||
|
left: None,
|
||||||
|
right: None,
|
||||||
|
up: None,
|
||||||
|
down: None,
|
||||||
|
select_left: None,
|
||||||
|
select_right: None,
|
||||||
|
select_up: None,
|
||||||
|
select_down: None,
|
||||||
|
select_all: None,
|
||||||
|
home: None,
|
||||||
|
end: None,
|
||||||
|
move_to_beginning: None,
|
||||||
|
move_to_end: None,
|
||||||
|
select_to_beginning: None,
|
||||||
|
select_to_end: None,
|
||||||
|
word_left: None,
|
||||||
|
word_right: None,
|
||||||
|
select_word_left: None,
|
||||||
|
select_word_right: None,
|
||||||
|
copy: None,
|
||||||
|
cut: None,
|
||||||
|
paste: None,
|
||||||
|
undo: None,
|
||||||
|
redo: None,
|
||||||
|
escape: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges these bindings with defaults, using `self` values where `Some`,
|
||||||
|
/// falling back to defaults for `None` values.
|
||||||
|
pub fn merged_with_defaults(self) -> Self {
|
||||||
|
let defaults = Self::default();
|
||||||
|
Self {
|
||||||
|
backspace: self.backspace.or(defaults.backspace),
|
||||||
|
delete: self.delete.or(defaults.delete),
|
||||||
|
delete_word_left: self.delete_word_left.or(defaults.delete_word_left),
|
||||||
|
delete_word_right: self.delete_word_right.or(defaults.delete_word_right),
|
||||||
|
delete_to_beginning_of_line: self
|
||||||
|
.delete_to_beginning_of_line
|
||||||
|
.or(defaults.delete_to_beginning_of_line),
|
||||||
|
delete_to_end_of_line: self
|
||||||
|
.delete_to_end_of_line
|
||||||
|
.or(defaults.delete_to_end_of_line),
|
||||||
|
tab: self.tab.or(defaults.tab),
|
||||||
|
enter: self.enter.or(defaults.enter),
|
||||||
|
left: self.left.or(defaults.left),
|
||||||
|
right: self.right.or(defaults.right),
|
||||||
|
up: self.up.or(defaults.up),
|
||||||
|
down: self.down.or(defaults.down),
|
||||||
|
select_left: self.select_left.or(defaults.select_left),
|
||||||
|
select_right: self.select_right.or(defaults.select_right),
|
||||||
|
select_up: self.select_up.or(defaults.select_up),
|
||||||
|
select_down: self.select_down.or(defaults.select_down),
|
||||||
|
select_all: self.select_all.or(defaults.select_all),
|
||||||
|
home: self.home.or(defaults.home),
|
||||||
|
end: self.end.or(defaults.end),
|
||||||
|
move_to_beginning: self.move_to_beginning.or(defaults.move_to_beginning),
|
||||||
|
move_to_end: self.move_to_end.or(defaults.move_to_end),
|
||||||
|
select_to_beginning: self.select_to_beginning.or(defaults.select_to_beginning),
|
||||||
|
select_to_end: self.select_to_end.or(defaults.select_to_end),
|
||||||
|
word_left: self.word_left.or(defaults.word_left),
|
||||||
|
word_right: self.word_right.or(defaults.word_right),
|
||||||
|
select_word_left: self.select_word_left.or(defaults.select_word_left),
|
||||||
|
select_word_right: self.select_word_right.or(defaults.select_word_right),
|
||||||
|
copy: self.copy.or(defaults.copy),
|
||||||
|
cut: self.cut.or(defaults.cut),
|
||||||
|
paste: self.paste.or(defaults.paste),
|
||||||
|
undo: self.undo.or(defaults.undo),
|
||||||
|
redo: self.redo.or(defaults.redo),
|
||||||
|
escape: self.escape.or(defaults.escape),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects all `Some` bindings into a `Vec<KeyBinding>`.
|
||||||
|
pub fn into_bindings(self) -> Vec<KeyBinding> {
|
||||||
|
[
|
||||||
|
self.backspace,
|
||||||
|
self.delete,
|
||||||
|
self.delete_word_left,
|
||||||
|
self.delete_word_right,
|
||||||
|
self.delete_to_beginning_of_line,
|
||||||
|
self.delete_to_end_of_line,
|
||||||
|
self.tab,
|
||||||
|
self.enter,
|
||||||
|
self.left,
|
||||||
|
self.right,
|
||||||
|
self.up,
|
||||||
|
self.down,
|
||||||
|
self.select_left,
|
||||||
|
self.select_right,
|
||||||
|
self.select_up,
|
||||||
|
self.select_down,
|
||||||
|
self.select_all,
|
||||||
|
self.home,
|
||||||
|
self.end,
|
||||||
|
self.move_to_beginning,
|
||||||
|
self.move_to_end,
|
||||||
|
self.select_to_beginning,
|
||||||
|
self.select_to_end,
|
||||||
|
self.word_left,
|
||||||
|
self.word_right,
|
||||||
|
self.select_word_left,
|
||||||
|
self.select_word_right,
|
||||||
|
self.copy,
|
||||||
|
self.cut,
|
||||||
|
self.paste,
|
||||||
|
self.undo,
|
||||||
|
self.redo,
|
||||||
|
self.escape,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binds input keybindings to the application.
|
||||||
|
///
|
||||||
|
/// If no bindings are provided, platform defaults are used. When bindings are
|
||||||
|
/// provided, they are used exactly as-is - fields set to `None` will not have
|
||||||
|
/// any keybinding registered for that action.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Use all platform defaults:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// bind_input_keys(cx, None);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Unbind a specific key while keeping other defaults:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// bind_input_keys(cx, InputBindings {
|
||||||
|
/// up: None, // Unbind up arrow
|
||||||
|
/// ..Default::default()
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Override a specific binding while keeping other defaults:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// bind_input_keys(cx, InputBindings {
|
||||||
|
/// select_all: Some(KeyBinding::new("ctrl-shift-a", SelectAll, Some(INPUT_CONTEXT))),
|
||||||
|
/// ..Default::default()
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use [`InputBindings::empty()`] with [`merged_with_defaults()`](InputBindings::merged_with_defaults)
|
||||||
|
/// if you only want to specify a few custom bindings and fill in the rest with defaults:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let mut bindings = InputBindings::empty();
|
||||||
|
/// bindings.select_all = Some(KeyBinding::new("ctrl-shift-a", SelectAll, Some(INPUT_CONTEXT)));
|
||||||
|
/// bind_input_keys(cx, bindings.merged_with_defaults());
|
||||||
|
/// ```
|
||||||
|
pub fn bind_input_keys(cx: &mut App, bindings: impl Into<Option<InputBindings>>) {
|
||||||
|
let bindings = bindings.into().unwrap_or_default();
|
||||||
|
cx.bind_keys(bindings.into_bindings());
|
||||||
|
}
|
||||||
121
crates/input/src/blink.rs
Normal file
121
crates/input/src/blink.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::Context;
|
||||||
|
use smol::Timer;
|
||||||
|
|
||||||
|
/// Manages the blinking state of a text cursor.
|
||||||
|
///
|
||||||
|
/// The cursor blinks at a configurable interval when enabled. Blinking can be
|
||||||
|
/// temporarily paused (e.g., during typing) to provide immediate visual feedback.
|
||||||
|
pub struct CursorBlink {
|
||||||
|
interval: Duration,
|
||||||
|
generation: usize,
|
||||||
|
visible: bool,
|
||||||
|
active: bool,
|
||||||
|
paused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorBlink {
|
||||||
|
/// Creates a new cursor blink manager with the given interval.
|
||||||
|
///
|
||||||
|
/// The cursor starts in a disabled state with visibility set to true.
|
||||||
|
pub fn new(interval: Duration, _cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
interval,
|
||||||
|
generation: 0,
|
||||||
|
visible: true,
|
||||||
|
active: false,
|
||||||
|
paused: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the cursor should currently be rendered.
|
||||||
|
pub fn visible(&self) -> bool {
|
||||||
|
self.visible
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether blinking is currently active.
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.active
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activates cursor blinking.
|
||||||
|
///
|
||||||
|
/// When activated, the cursor will alternate between visible and hidden
|
||||||
|
/// states at the configured interval. Has no effect if already active.
|
||||||
|
pub fn enable(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.active = true;
|
||||||
|
self.visible = false;
|
||||||
|
self.paused = false;
|
||||||
|
self.tick(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivates cursor blinking.
|
||||||
|
///
|
||||||
|
/// The cursor visibility is set to false when disabled. Call
|
||||||
|
/// `pause_blinking` instead if you want to temporarily stop blinking
|
||||||
|
/// while keeping the cursor visible.
|
||||||
|
pub fn disable(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.active = false;
|
||||||
|
self.visible = false;
|
||||||
|
self.paused = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily pauses blinking and shows the cursor.
|
||||||
|
///
|
||||||
|
/// This is useful during user input to provide immediate feedback.
|
||||||
|
/// Blinking resumes automatically after the blink interval elapses.
|
||||||
|
pub fn pause_blinking(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if !self.visible {
|
||||||
|
self.visible = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.paused = true;
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
|
||||||
|
let generation = self.generation;
|
||||||
|
let interval = self.interval;
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
Timer::after(interval).await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if this.generation == generation {
|
||||||
|
this.paused = false;
|
||||||
|
this.tick(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if !self.active || self.paused {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.visible = !self.visible;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
let generation = self.generation;
|
||||||
|
let interval = self.interval;
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
Timer::after(interval).await;
|
||||||
|
if let Some(this) = this.upgrade() {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if this.generation == generation {
|
||||||
|
this.tick(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
181
crates/input/src/handler.rs
Normal file
181
crates/input/src/handler.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, Bounds, Context, Entity, InputHandler, Pixels, Point, UTF16Selection, Window};
|
||||||
|
|
||||||
|
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||||
|
///
|
||||||
|
/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
|
||||||
|
/// This input handler can then be assigned during paint by calling [`Window::handle_input`].
|
||||||
|
///
|
||||||
|
/// See [`InputHandler`] for details on how to implement each method.
|
||||||
|
pub trait EntityInputHandler: 'static + Sized {
|
||||||
|
/// See [`InputHandler::text_for_range`] for details
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range: Range<usize>,
|
||||||
|
adjusted_range: &mut Option<Range<usize>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<String>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::selected_text_range`] for details
|
||||||
|
fn selected_text_range(
|
||||||
|
&mut self,
|
||||||
|
ignore_disabled_input: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<UTF16Selection>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::marked_text_range`] for details
|
||||||
|
fn marked_text_range(
|
||||||
|
&self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::unmark_text`] for details
|
||||||
|
fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>);
|
||||||
|
|
||||||
|
/// See [`InputHandler::replace_text_in_range`] for details
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range: Option<Range<usize>>,
|
||||||
|
text: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// See [`InputHandler::replace_and_mark_text_in_range`] for details
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// See [`InputHandler::bounds_for_range`] for details
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
element_bounds: Bounds<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<Bounds<Pixels>>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::character_index_for_point`] for details
|
||||||
|
fn character_index_for_point(
|
||||||
|
&mut self,
|
||||||
|
point: Point<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The canonical implementation of [`gpui::PlatformInputHandler`]. Call [`Window::handle_input`]
|
||||||
|
/// with an instance during your element's paint.
|
||||||
|
pub struct ElementInputHandler<V> {
|
||||||
|
view: Entity<V>,
|
||||||
|
element_bounds: Bounds<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> ElementInputHandler<V> {
|
||||||
|
/// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context.
|
||||||
|
///
|
||||||
|
/// [element_paint]: gpui::Element::paint
|
||||||
|
pub fn new(element_bounds: Bounds<Pixels>, view: Entity<V>) -> Self {
|
||||||
|
ElementInputHandler {
|
||||||
|
view,
|
||||||
|
element_bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
|
||||||
|
fn selected_text_range(
|
||||||
|
&mut self,
|
||||||
|
ignore_disabled_input: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<UTF16Selection> {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.selected_text_range(ignore_disabled_input, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>> {
|
||||||
|
self.view
|
||||||
|
.update(cx, |view, cx| view.marked_text_range(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
adjusted_range: &mut Option<Range<usize>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<String> {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.text_for_range(range_utf16, adjusted_range, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
replacement_range: Option<Range<usize>>,
|
||||||
|
text: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.replace_text_in_range(replacement_range, text, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.replace_and_mark_text_in_range(
|
||||||
|
range_utf16,
|
||||||
|
new_text,
|
||||||
|
new_selected_range,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmark_text(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
|
self.view
|
||||||
|
.update(cx, |view, cx| view.unmark_text(window, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Bounds<Pixels>> {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_index_for_point(
|
||||||
|
&mut self,
|
||||||
|
point: Point<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<usize> {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.character_index_for_point(point, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1301
crates/input/src/input.rs
Normal file
1301
crates/input/src/input.rs
Normal file
File diff suppressed because it is too large
Load Diff
9
crates/input/src/lib.rs
Normal file
9
crates/input/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod bidi;
|
||||||
|
mod bindings;
|
||||||
|
mod blink;
|
||||||
|
mod handler;
|
||||||
|
mod input;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
pub use input::*;
|
||||||
|
pub use state::*;
|
||||||
1731
crates/input/src/state.rs
Normal file
1731
crates/input/src/state.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user