custome input
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -1171,6 +1171,7 @@ dependencies = [
|
||||
"flume 0.11.1",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"input",
|
||||
"itertools 0.13.0",
|
||||
"linkify",
|
||||
"log",
|
||||
@@ -3675,6 +3676,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
|
||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
state = { path = "../state" }
|
||||
ui = { path = "../ui" }
|
||||
input = { path = "../input" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
person = { path = "../person" }
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui::{
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||
deferred, div, img, list, px, red, relative, svg, white,
|
||||
};
|
||||
use input::{Input, InputState, InputStateEvent, input};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
@@ -24,7 +25,6 @@ use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
@@ -116,15 +116,17 @@ impl ChatPanel {
|
||||
|
||||
// Define input state
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(format!("Message {}", name))
|
||||
.auto_grow(1, 20)
|
||||
.prevent_new_line_on_enter()
|
||||
.clean_on_escape()
|
||||
let mut this = InputState::new_multiline(cx);
|
||||
this.set_placeholder(format!("Message {}", name), cx);
|
||||
this
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// Define subscriptions
|
||||
@@ -133,7 +135,7 @@ impl ChatPanel {
|
||||
subscriptions.push(
|
||||
// Subscribe the chat input event
|
||||
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);
|
||||
};
|
||||
}),
|
||||
@@ -145,7 +147,7 @@ impl ChatPanel {
|
||||
&subject_input,
|
||||
window,
|
||||
move |this, _input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
if let InputStateEvent::Enter = event {
|
||||
this.change_subject(window, cx);
|
||||
};
|
||||
},
|
||||
@@ -294,7 +296,7 @@ impl ChatPanel {
|
||||
/// Get user input content and merged all attachments if available
|
||||
fn get_input_value(&self, cx: &Context<Self>) -> String {
|
||||
// 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
|
||||
let attachments = self.attachments.read(cx);
|
||||
@@ -317,7 +319,7 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
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
|
||||
.update(cx, |this, cx| {
|
||||
@@ -414,9 +416,9 @@ impl ChatPanel {
|
||||
/// Clear the input field, attachments, and replies
|
||||
///
|
||||
/// 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| {
|
||||
this.set_value("", window, cx);
|
||||
this.set_content("", cx);
|
||||
});
|
||||
self.attachments.update(cx, |this, cx| {
|
||||
this.clear();
|
||||
@@ -1497,12 +1499,7 @@ impl Render for ChatPanel {
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
TextInput::new(&self.subject_input)
|
||||
.text_sm()
|
||||
.small()
|
||||
.bordered(false),
|
||||
)
|
||||
.child(input(&self.subject_input, cx).text_sm())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.icon(IconName::CheckCircle)
|
||||
@@ -1553,12 +1550,7 @@ impl Render for ChatPanel {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(&self.input)
|
||||
.appearance(false)
|
||||
.text_sm()
|
||||
.flex_1(),
|
||||
)
|
||||
.child(input(&self.input, cx).text_sm().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.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