Compare commits
49 Commits
main
...
gpui-offic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8b7274ba | ||
|
|
8c2e1b48fb | ||
|
|
2e2477f29a | ||
|
|
baf2889a48 | ||
|
|
3bb9cd666b | ||
|
|
e601ec2750 | ||
|
|
b96c966113 | ||
|
|
54b3f7fad5 | ||
|
|
1bbebcfd0b | ||
|
|
bb1382c03a | ||
|
|
24521bdcdf | ||
|
|
1929a805ac | ||
|
|
45d9fcadbf | ||
|
|
35198599ac | ||
|
|
1568de039b | ||
|
|
65ef4e8abc | ||
|
|
3507eefabb | ||
|
|
338c37796d | ||
|
|
801c6645ff | ||
|
|
7fde67ff48 | ||
|
|
e6ad27dec3 | ||
|
|
081c2048aa | ||
|
|
3d5d66e35a | ||
|
|
26522ea60f | ||
|
|
b0a51ed68e | ||
|
|
7ce563a4e8 | ||
|
|
226ebd7579 | ||
|
|
ace07d6afd | ||
|
|
3a25ef2682 | ||
|
|
96cbe7e6f0 | ||
|
|
eb0767146d | ||
|
|
914e795d90 | ||
|
|
cb748e2fb2 | ||
|
|
749adff2ef | ||
|
|
4b6e90e0d0 | ||
|
|
c287fb8cc8 | ||
|
|
ba8ed43004 | ||
|
|
3636fe5031 | ||
|
|
aebebe0996 | ||
|
|
2f27430348 | ||
|
|
de003caaa0 | ||
|
|
cc03d1b419 | ||
|
|
987e0edd2a | ||
|
|
e58402de33 | ||
|
|
4d23776315 | ||
|
|
357805a819 | ||
|
|
e5e646c24e | ||
|
|
17bc109746 | ||
|
|
dceb76e334 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -7348,6 +7348,7 @@ dependencies = [
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-bidi",
|
||||
"unicode-segmentation",
|
||||
"usvg",
|
||||
"util",
|
||||
|
||||
@@ -694,6 +694,7 @@ tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unicode-bidi = "0.3"
|
||||
unicode-script = "0.5.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unindent = "0.2.0"
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
use gpui::Context;
|
||||
use settings::SettingsStore;
|
||||
use smol::Timer;
|
||||
use std::time::Duration;
|
||||
use ui::App;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
blink_epoch: usize,
|
||||
/// Whether the blinking is paused.
|
||||
blinking_paused: bool,
|
||||
/// Whether the cursor should be visibly rendered or not.
|
||||
visible: bool,
|
||||
/// Whether the blinking currently enabled.
|
||||
enabled: bool,
|
||||
/// Whether the blinking is enabled in the settings.
|
||||
blink_enabled_in_settings: fn(&App) -> bool,
|
||||
}
|
||||
|
||||
impl BlinkManager {
|
||||
pub fn new(
|
||||
blink_interval: Duration,
|
||||
blink_enabled_in_settings: fn(&App) -> bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Make sure we blink the cursors if the setting is re-enabled
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.blink_cursors(this.blink_epoch, cx)
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
blink_interval,
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
enabled: false,
|
||||
blink_enabled_in_settings,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
}
|
||||
|
||||
pub fn pause_blinking(&mut self, cx: &mut Context<Self>) {
|
||||
self.show_cursor(cx);
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(async move |this, cx| {
|
||||
Timer::after(interval).await;
|
||||
this.update(cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
||||
if epoch == self.blink_epoch {
|
||||
self.blinking_paused = false;
|
||||
self.blink_cursors(epoch, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
||||
if (self.blink_enabled_in_settings)(cx) {
|
||||
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(async move |this, cx| {
|
||||
Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| this.blink_cursors(epoch, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
} else {
|
||||
self.show_cursor(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self, cx: &mut Context<BlinkManager>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable the blinking of the cursor.
|
||||
pub fn enable(&mut self, cx: &mut Context<Self>) {
|
||||
if self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.enabled = true;
|
||||
// Set cursors as invisible and start blinking: this causes cursors
|
||||
// to be visible during the next render.
|
||||
self.visible = false;
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
}
|
||||
|
||||
/// Disable the blinking of the cursor.
|
||||
pub fn disable(&mut self, _cx: &mut Context<Self>) {
|
||||
self.visible = false;
|
||||
self.enabled = false;
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
//!
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
|
||||
pub mod actions;
|
||||
pub mod blink_manager;
|
||||
mod bracket_colorization;
|
||||
mod clangd_ext;
|
||||
pub mod code_context_menus;
|
||||
@@ -78,7 +77,6 @@ use ::git::{
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use client::{Collaborator, ParticipantIndex, parse_zed_link};
|
||||
use clock::ReplicaId;
|
||||
@@ -100,6 +98,7 @@ use futures::{
|
||||
};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use git::blame::{GitBlame, GlobalBlameRenderer};
|
||||
use gpui::input::BlinkManager;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
|
||||
AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
|
||||
@@ -1887,11 +1886,7 @@ impl Editor {
|
||||
let selections = SelectionsCollection::new();
|
||||
|
||||
let blink_manager = cx.new(|cx| {
|
||||
let mut blink_manager = BlinkManager::new(
|
||||
CURSOR_BLINK_INTERVAL,
|
||||
|cx| EditorSettings::get_global(cx).cursor_blink,
|
||||
cx,
|
||||
);
|
||||
let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx);
|
||||
if is_minimap {
|
||||
blink_manager.disable(cx);
|
||||
}
|
||||
@@ -2293,8 +2288,9 @@ impl Editor {
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
let cursor_blink = EditorSettings::get_global(cx).cursor_blink;
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
if active {
|
||||
if active && cursor_blink {
|
||||
blink_manager.enable(cx);
|
||||
} else {
|
||||
blink_manager.disable(cx);
|
||||
@@ -21614,6 +21610,17 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let cursor_blink = EditorSettings::get_global(cx).cursor_blink;
|
||||
let is_focused = self.focus_handle.is_focused(window);
|
||||
self.blink_manager.update(cx, |blink_manager, cx| {
|
||||
if cursor_blink && is_focused {
|
||||
blink_manager.enable(cx);
|
||||
} else if !cursor_blink {
|
||||
blink_manager.disable(cx);
|
||||
blink_manager.show_cursor(cx);
|
||||
}
|
||||
});
|
||||
|
||||
let new_language_settings = self.fetch_applicable_language_settings(cx);
|
||||
let language_settings_changed = new_language_settings != self.applicable_language_settings;
|
||||
self.applicable_language_settings = new_language_settings;
|
||||
@@ -22177,7 +22184,9 @@ impl Editor {
|
||||
blame.update(cx, GitBlame::focus)
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::enable);
|
||||
if EditorSettings::get_global(cx).cursor_blink {
|
||||
self.blink_manager.update(cx, BlinkManager::enable);
|
||||
}
|
||||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
@@ -132,6 +132,8 @@ strum.workspace = true
|
||||
sum_tree.workspace = true
|
||||
taffy = "=0.9.0"
|
||||
thiserror.workspace = true
|
||||
unicode-bidi.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
waker-fn = "1.2.0"
|
||||
@@ -278,6 +280,10 @@ path = "examples/image/image.rs"
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input_sandbox"
|
||||
path = "examples/input_sandbox.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/on_window_close_quit.rs"
|
||||
@@ -329,3 +335,7 @@ path = "examples/window_shadow.rs"
|
||||
[[example]]
|
||||
name = "grid_layout"
|
||||
path = "examples/grid_layout.rs"
|
||||
|
||||
[[example]]
|
||||
name = "todo_mvc"
|
||||
path = "examples/todo_mvc.rs"
|
||||
|
||||
541
crates/gpui/examples/input_sandbox.rs
Normal file
541
crates/gpui/examples/input_sandbox.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
//! Input Sandbox - A simple example for testing single-line and multi-line inputs.
|
||||
//!
|
||||
//! Run with: `cargo run -p gpui --example input_sandbox`
|
||||
|
||||
use gpui::input::bind_input_keys;
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Div, Entity, FocusHandle, Focusable, InputState,
|
||||
InputStateEvent, KeyBinding, Stateful, Subscription, Window, WindowBounds, WindowOptions, div,
|
||||
input, prelude::*, px, rgb, size, text_area,
|
||||
};
|
||||
|
||||
struct InputSandbox {
|
||||
multiline_input: Entity<InputState>,
|
||||
singleline_input: Entity<InputState>,
|
||||
use_multiline: bool,
|
||||
current_sample: SampleText,
|
||||
last_event: Option<String>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl InputSandbox {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
let initial_sample = SampleText::Typography;
|
||||
|
||||
let multiline_input = cx.new(|cx| {
|
||||
let mut input = InputState::new_multiline(cx);
|
||||
input.set_content(initial_sample.content(), cx);
|
||||
input
|
||||
});
|
||||
|
||||
let singleline_input = cx.new(|cx| {
|
||||
let mut input = InputState::new_singleline(cx);
|
||||
input.set_content("Single-line text input example", cx);
|
||||
input
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe(&multiline_input, |this, _, event, cx| {
|
||||
this.handle_input_event("multiline", event, cx);
|
||||
}),
|
||||
cx.subscribe(&singleline_input, |this, _, event, cx| {
|
||||
this.handle_input_event("singleline", event, cx);
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
multiline_input,
|
||||
singleline_input,
|
||||
use_multiline: true,
|
||||
current_sample: initial_sample,
|
||||
last_event: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input_event(
|
||||
&mut self,
|
||||
source: &str,
|
||||
event: &InputStateEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let event_name = match event {
|
||||
InputStateEvent::Focus => "Focus",
|
||||
InputStateEvent::Blur => "Blur",
|
||||
InputStateEvent::TextChanged => "TextChanged",
|
||||
InputStateEvent::Undo => "Undo",
|
||||
InputStateEvent::Redo => "Redo",
|
||||
};
|
||||
self.last_event = Some(format!("{}: {}", source, event_name));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.use_multiline = !self.use_multiline;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_sample(&mut self, sample: SampleText, cx: &mut Context<Self>) {
|
||||
self.current_sample = sample;
|
||||
self.multiline_input.update(cx, |input, cx| {
|
||||
input.set_content(sample.content(), cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn active_input(&self) -> &Entity<InputState> {
|
||||
if self.use_multiline {
|
||||
&self.multiline_input
|
||||
} else {
|
||||
&self.singleline_input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for InputSandbox {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.active_input().focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputSandbox {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_input = self.active_input().clone();
|
||||
let input_state = active_input.read(cx);
|
||||
let content = input_state.content().to_string();
|
||||
let selected_range = input_state.selected_range().clone();
|
||||
let cursor_offset = input_state.cursor_offset();
|
||||
let char_count = content.chars().count();
|
||||
let line_count = content.lines().count().max(1);
|
||||
|
||||
let focus_handle = active_input.focus_handle(cx);
|
||||
|
||||
let multiline_focus = self.multiline_input.focus_handle(cx);
|
||||
let singleline_focus = self.singleline_input.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.id("input-sandbox")
|
||||
.key_context("InputSandbox")
|
||||
.track_focus(&focus_handle)
|
||||
.on_action(cx.listener(Self::toggle_mode))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.bg(rgb(0x1e1e1e))
|
||||
.text_color(rgb(0xcccccc))
|
||||
.size_full()
|
||||
// Left panel - Content area
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p_4()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.when(self.use_multiline, |this| {
|
||||
this.child(
|
||||
text_area(&self.multiline_input)
|
||||
.size_full()
|
||||
.bg(rgb(0x1e1e1e))
|
||||
.text_color(rgb(0xd4d4d4))
|
||||
.text_base()
|
||||
.selection_color(gpui::rgba(0x3388ff44))
|
||||
.cursor_color(rgb(0xffffff)),
|
||||
)
|
||||
})
|
||||
.when(!self.use_multiline, |this| {
|
||||
this.child(
|
||||
div().flex().items_center().h(px(40.)).child(
|
||||
input(&self.singleline_input)
|
||||
.size_full()
|
||||
.bg(rgb(0x1e1e1e))
|
||||
.text_color(rgb(0xd4d4d4))
|
||||
.text_base()
|
||||
.selection_color(gpui::rgba(0x3388ff44))
|
||||
.cursor_color(rgb(0xffffff)),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
// Right panel - Sidebar
|
||||
.child(
|
||||
div()
|
||||
.id("sidebar")
|
||||
.w(px(240.))
|
||||
.flex_shrink_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.bg(rgb(0x252526))
|
||||
.border_l_1()
|
||||
.border_color(rgb(0x3c3c3c))
|
||||
.overflow_y_scroll()
|
||||
// Mode toggle section
|
||||
.child(
|
||||
sidebar_section("Mode").child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
toggle_button("multi-btn", "Multi-line", self.use_multiline)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if !this.use_multiline {
|
||||
this.toggle_mode(&ToggleMode, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
toggle_button("single-btn", "Single-line", !self.use_multiline)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if this.use_multiline {
|
||||
this.toggle_mode(&ToggleMode, window, cx);
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Sample text selector (only in multiline mode)
|
||||
.when(self.use_multiline, |this| {
|
||||
let current_sample = self.current_sample;
|
||||
this.child(
|
||||
sidebar_section("Sample Text").child(
|
||||
div().flex().flex_col().gap_1().children(
|
||||
SampleText::ALL.iter().map(|sample| {
|
||||
let sample = *sample;
|
||||
let is_active = current_sample == sample;
|
||||
sample_button(sample, is_active).on_click(cx.listener(
|
||||
move |this, _, _window, cx| {
|
||||
this.set_sample(sample, cx);
|
||||
},
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
// Stats section
|
||||
.child(
|
||||
sidebar_section("Statistics")
|
||||
.child(stat_row("Cursor", format!("{}", cursor_offset)))
|
||||
.child(stat_row(
|
||||
"Selection",
|
||||
format!("{}..{}", selected_range.start, selected_range.end),
|
||||
))
|
||||
.child(stat_row("Characters", format!("{}", char_count)))
|
||||
.child(stat_row("Lines", format!("{}", line_count)))
|
||||
.child(stat_row("Bytes", format!("{}", content.len()))),
|
||||
)
|
||||
// Focus state section
|
||||
.child(
|
||||
sidebar_section("Focus State")
|
||||
.child(stat_row(
|
||||
"Multi-line",
|
||||
if multiline_focus.is_focused(window) {
|
||||
"focused"
|
||||
} else {
|
||||
"—"
|
||||
},
|
||||
))
|
||||
.child(stat_row(
|
||||
"Single-line",
|
||||
if singleline_focus.is_focused(window) {
|
||||
"focused"
|
||||
} else {
|
||||
"—"
|
||||
},
|
||||
)),
|
||||
)
|
||||
// Events section
|
||||
.child(sidebar_section("Last Event").child(stat_row(
|
||||
"Event",
|
||||
self.last_event.clone().unwrap_or_else(|| "—".to_string()),
|
||||
)))
|
||||
// Keybindings section
|
||||
.child(
|
||||
sidebar_section("Keybindings")
|
||||
.child(key_row("Ctrl+T", "Toggle mode"))
|
||||
.child(key_row("Cmd+Z", "Undo"))
|
||||
.child(key_row("Cmd+Shift+Z", "Redo"))
|
||||
.child(key_row("Cmd+A", "Select all"))
|
||||
.child(key_row("Cmd+C", "Copy"))
|
||||
.child(key_row("Cmd+X", "Cut"))
|
||||
.child(key_row("Cmd+V", "Paste"))
|
||||
.child(key_row("Alt+←/→", "Word nav"))
|
||||
.child(key_row("Cmd+←/→", "Line start/end"))
|
||||
.child(key_row("Cmd+↑/↓", "Doc start/end")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn sidebar_section(title: &str) -> gpui::Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_3()
|
||||
.border_b_1()
|
||||
.border_color(rgb(0x3c3c3c))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(rgb(0x888888))
|
||||
.child(title.to_uppercase()),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_button(id: &'static str, label: &str, active: bool) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.when(active, |this| {
|
||||
this.bg(rgb(0x0e639c)).text_color(rgb(0xffffff))
|
||||
})
|
||||
.when(!active, |this| {
|
||||
this.bg(rgb(0x3c3c3c))
|
||||
.text_color(rgb(0x888888))
|
||||
.hover(|s| s.bg(rgb(0x4c4c4c)))
|
||||
})
|
||||
.child(label.to_string())
|
||||
}
|
||||
|
||||
fn sample_button(sample: SampleText, active: bool) -> Stateful<Div> {
|
||||
div()
|
||||
.id(sample.label())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.w_full()
|
||||
.when(active, |this| {
|
||||
this.bg(rgb(0x0e639c)).text_color(rgb(0xffffff))
|
||||
})
|
||||
.when(!active, |this| {
|
||||
this.bg(rgb(0x3c3c3c))
|
||||
.text_color(rgb(0x888888))
|
||||
.hover(|s| s.bg(rgb(0x4c4c4c)))
|
||||
})
|
||||
.child(sample.label().to_string())
|
||||
}
|
||||
|
||||
fn stat_row(label: &str, value: impl Into<gpui::SharedString>) -> gpui::Div {
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.text_xs()
|
||||
.child(div().text_color(rgb(0x888888)).child(label.to_string()))
|
||||
.child(div().text_color(rgb(0xcccccc)).child(value.into()))
|
||||
}
|
||||
|
||||
fn key_row(key: &str, desc: &str) -> gpui::Div {
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.bg(rgb(0x3c3c3c))
|
||||
.rounded_sm()
|
||||
.text_color(rgb(0xaaaaaa))
|
||||
.flex_shrink_0()
|
||||
.child(key.to_string()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(0x888888))
|
||||
.overflow_hidden()
|
||||
.child(desc.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
gpui::actions!(input_sandbox, [ToggleMode]);
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
bind_input_keys(cx, None);
|
||||
|
||||
cx.bind_keys([KeyBinding::new("ctrl-t", ToggleMode, None)]);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(900.), px(700.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
let view = cx.new(InputSandbox::new);
|
||||
let focus_handle = view.read(cx).active_input().focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
view
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum SampleText {
|
||||
Typography,
|
||||
RtlMixed,
|
||||
TrickyText,
|
||||
}
|
||||
|
||||
impl SampleText {
|
||||
const ALL: &[SampleText] = &[
|
||||
SampleText::Typography,
|
||||
SampleText::RtlMixed,
|
||||
SampleText::TrickyText,
|
||||
];
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SampleText::Typography => "Typography",
|
||||
SampleText::RtlMixed => "RTL/Bidi",
|
||||
SampleText::TrickyText => "Tricky Text",
|
||||
}
|
||||
}
|
||||
|
||||
fn content(&self) -> &'static str {
|
||||
match self {
|
||||
SampleText::Typography => TYPOGRAPHY_TEXT,
|
||||
SampleText::RtlMixed => RTL_MIXED_TEXT,
|
||||
SampleText::TrickyText => TRICKY_TEXT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TYPOGRAPHY_TEXT: &str = r#"ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
0123456789!?.
|
||||
|
||||
Pixel preview Resize to fit zenith zone
|
||||
Frame Group Feedback Reset
|
||||
Day day Month month Year year
|
||||
Hour hour Minute minute Second second
|
||||
|
||||
The quick brown fox jumps over the lazy dog
|
||||
Pack my box with five dozen liquor jugs
|
||||
Sphinx of black quartz, judge my vow
|
||||
|
||||
jumping far—but not really—over the bar
|
||||
We found a fix to the ffi problem
|
||||
Irrational fi ffi fl ffl
|
||||
|
||||
12.4 pt 64% 90px 45 kg 12 o'clock
|
||||
$64 $7 €64 €64 £7 £7
|
||||
3° °C °F
|
||||
|
||||
#80A6F3 #FFFFFF #000000
|
||||
in Drafts • 3 hours ago
|
||||
|
||||
• Buy milk? cc cd ce cq co
|
||||
• ec ed ee eq eo oc od oe oq oo"#;
|
||||
|
||||
const RTL_MIXED_TEXT: &str = r#"Hebrew:
|
||||
שלום עולם
|
||||
מה שלומך היום?
|
||||
|
||||
Arabic:
|
||||
مرحبا بالعالم
|
||||
كيف حالك اليوم؟
|
||||
|
||||
Mixed LTR and RTL:
|
||||
Hello שלום World עולם
|
||||
The word مرحبا means hello
|
||||
|
||||
Numbers in RTL context:
|
||||
בשנת 2024 היו 365 ימים
|
||||
في عام 2024 كان هناك 365 يومًا
|
||||
|
||||
Bidirectional with punctuation:
|
||||
(שלום) "עולם" [מה]!
|
||||
«مرحبا» "العالم" (كيف)؟
|
||||
|
||||
Mixed script sentence:
|
||||
I learned שלום in Hebrew class
|
||||
تعلمت "hello" في صف الإنجليزية
|
||||
|
||||
Nested direction changes:
|
||||
Start שלום hello עולם end
|
||||
Begin مرحبا world العالم finish"#;
|
||||
|
||||
const TRICKY_TEXT: &str = r#"═══ EMOJI ═══
|
||||
|
||||
Simple: 😀 😎 🎉 ❤️ 🔥 ✨
|
||||
Skin tones: 👋 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿
|
||||
ZWJ sequences: 👨👩👧👦 👩💻 👨🍳 🧑🚀
|
||||
Flags: 🇺🇸 🇬🇧 🇯🇵 🇩🇪 🇫🇷 🏳️🌈
|
||||
Keycaps: 1️⃣ 2️⃣ 3️⃣ #️⃣ *️⃣
|
||||
Presentation: ☺︎ vs ☺️ ▶︎ vs ▶️
|
||||
Mixed: Hello 👋 World 🌍! I ❤️ Rust 🦀
|
||||
Cursor test: →😀← →👨👩👧👦← →🇺🇸← →1️⃣←
|
||||
|
||||
═══ MULTIBYTE ═══
|
||||
|
||||
Chinese: 你好世界 中文测试
|
||||
Japanese: こんにちは世界 カタカナ
|
||||
Korean: 안녕하세요 한국어
|
||||
Thai: สวัสดีครับ
|
||||
Hindi: नमस्ते दुनिया
|
||||
Greek: Γεια σου κόσμε
|
||||
Russian: Привет мир
|
||||
Mixed: Hello 你好 こんにちは 안녕 Привет
|
||||
|
||||
═══ COMBINING CHARACTERS ═══
|
||||
|
||||
Precomposed vs decomposed:
|
||||
é (precomposed) vs é (e + ́)
|
||||
ñ vs ñ • ü vs ü
|
||||
|
||||
Multiple combiners:
|
||||
ẗ̈ (t + two diacritics)
|
||||
q̃̃ (q + two tildes)
|
||||
|
||||
═══ ZERO-WIDTH & INVISIBLE ═══
|
||||
|
||||
WordBreak (ZWJ)
|
||||
WordBreak (ZWNJ)
|
||||
WordBreak (word joiner)
|
||||
LeftRight (LRM)
|
||||
RightLeft (RLM)
|
||||
|
||||
Spaces: [ ] (regular) [ ] (NBSP) [] (zero-width)
|
||||
|
||||
═══ HOMOGLYPHS ═══
|
||||
|
||||
ABCabc (Latin)
|
||||
АВСавс (Cyrillic - different!)
|
||||
ΑΒΓαβγ (Greek - different!)
|
||||
|
||||
═══ ASTRAL PLANE ═══
|
||||
|
||||
𝕳𝖊𝖑𝖑𝖔 (math fraktur)
|
||||
𝒜𝒷𝒸 (math script)
|
||||
🜀🜁🜂🜃 (alchemical)
|
||||
|
||||
═══ WHITESPACE ═══
|
||||
|
||||
Tab: Column1 Column2
|
||||
Trailing spaces
|
||||
Leading spaces
|
||||
|
||||
═══ STRESS TEST ═══
|
||||
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
|
||||
a
|
||||
b
|
||||
c"#;
|
||||
121
crates/gpui/examples/todo_mvc.rs
Normal file
121
crates/gpui/examples/todo_mvc.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Entity, InputBindings, InputState, InputStateEvent, Rgba,
|
||||
SharedString, Window, WindowBounds, WindowOptions, bind_input_keys, div, input, prelude::*, px,
|
||||
rgb, size,
|
||||
};
|
||||
|
||||
struct TodoMvc {
|
||||
todo_items: Vec<TodoItem>,
|
||||
}
|
||||
|
||||
struct TodoItem {
|
||||
text: SharedString,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
impl Render for TodoMvc {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let this = cx.weak_entity();
|
||||
|
||||
let input_state = window.use_state(cx, move |_window, cx| {
|
||||
let mut state = InputState::new(cx);
|
||||
state.set_placeholder("What needs to be done?", cx);
|
||||
cx.subscribe_self(move |input_state, e: &InputStateEvent, cx| match e {
|
||||
InputStateEvent::Enter => {
|
||||
this.update(cx, |todo_mvc, cx| {
|
||||
let todo_text = input_state.content();
|
||||
todo_mvc.todo_items.push(TodoItem {
|
||||
text: todo_text.to_string().into(),
|
||||
checked: false,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
state
|
||||
});
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(rgb(0xffb3b3))
|
||||
.child(
|
||||
div().flex().items_center().h(px(40.)).child(
|
||||
input(&input_state)
|
||||
.size_full()
|
||||
.bg(rgb(0xbf00ff))
|
||||
.text_color(rgb(0xd4d4d4))
|
||||
.text_base()
|
||||
.selection_color(gpui::rgba(0x3388ff44))
|
||||
.cursor_color(rgb(0xffffff)),
|
||||
),
|
||||
)
|
||||
.children(self.todo_items.iter().enumerate().map(|(ix, todo_item)| {
|
||||
div()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.w_5()
|
||||
.h_5()
|
||||
.id(("todo-item-checkbox", ix))
|
||||
.on_click(cx.listener(move |todo_mvc, _, _, cx| {
|
||||
let todo_item = &mut todo_mvc.todo_items[ix];
|
||||
todo_item.checked = !todo_item.checked;
|
||||
cx.notify();
|
||||
}))
|
||||
.bg(if todo_item.checked {
|
||||
gpui::green()
|
||||
} else {
|
||||
gpui::red()
|
||||
}),
|
||||
)
|
||||
.child(todo_item.text.clone())
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(format!(
|
||||
"Checked items: {}",
|
||||
self.todo_items
|
||||
.iter()
|
||||
.filter(|todo_item| todo_item.checked)
|
||||
.count()
|
||||
))
|
||||
.child(format!(
|
||||
"Unchecked items: {}",
|
||||
self.todo_items
|
||||
.iter()
|
||||
.filter(|todo_item| !todo_item.checked)
|
||||
.count()
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
bind_input_keys(cx, Some(InputBindings::default()));
|
||||
let bounds = Bounds::centered(None, size(px(700.0), px(700.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| TodoMvc { todo_items: vec![] }),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Text box at the top
|
||||
// Type in it, hit enter, adds a new todo item
|
||||
// TODO items can be marked as complete
|
||||
// Metadata: # of items left, filtering item type
|
||||
// ALSO: everything is live updating
|
||||
@@ -23,6 +23,8 @@ pub struct Colors {
|
||||
pub separator: Rgba,
|
||||
/// Container color
|
||||
pub container: Rgba,
|
||||
/// Cursor color
|
||||
pub cursor: Rgba,
|
||||
}
|
||||
|
||||
impl Default for Colors {
|
||||
@@ -51,6 +53,7 @@ impl Colors {
|
||||
border: rgb(0x000000),
|
||||
separator: rgb(0xd9d9d9),
|
||||
container: rgb(0x262626),
|
||||
cursor: rgb(0x3378F6),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +68,7 @@ impl Colors {
|
||||
border: rgb(0xd9d9d9),
|
||||
separator: rgb(0xe6e6e6),
|
||||
container: rgb(0xf4f5f5),
|
||||
cursor: rgb(0x3378F6),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1301
crates/gpui/src/elements/input.rs
Normal file
1301
crates/gpui/src/elements/input.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ mod deferred;
|
||||
mod div;
|
||||
mod image_cache;
|
||||
mod img;
|
||||
mod input;
|
||||
mod list;
|
||||
mod surface;
|
||||
mod svg;
|
||||
@@ -18,6 +19,7 @@ pub use deferred::*;
|
||||
pub use div::*;
|
||||
pub use image_cache::*;
|
||||
pub use img::*;
|
||||
pub use input::*;
|
||||
pub use list::*;
|
||||
pub use surface::*;
|
||||
pub use svg::*;
|
||||
|
||||
@@ -22,7 +22,8 @@ mod elements;
|
||||
mod executor;
|
||||
mod geometry;
|
||||
mod global;
|
||||
mod input;
|
||||
/// Input
|
||||
pub mod input;
|
||||
mod inspector;
|
||||
mod interactive;
|
||||
mod key_dispatch;
|
||||
|
||||
@@ -1,190 +1,14 @@
|
||||
use crate::{App, Bounds, Context, Entity, InputHandler, Pixels, UTF16Selection, Window};
|
||||
use std::ops::Range;
|
||||
|
||||
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||
mod bidi;
|
||||
/// Input keybinding configuration & actions that can be bound (`Backspace`, `Copy`, 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>;
|
||||
/// Explicitly not exported using `pub use bindings::*` to avoid namespace pollution.
|
||||
pub mod bindings;
|
||||
mod blink_manager;
|
||||
mod handler;
|
||||
mod state;
|
||||
|
||||
/// 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: crate::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize>;
|
||||
|
||||
/// See [`InputHandler::accepts_text_input`] for details
|
||||
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical implementation of [`crate::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]: crate::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: crate::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<usize> {
|
||||
self.view.update(cx, |view, cx| {
|
||||
view.character_index_for_point(point, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.accepts_text_input(window, cx))
|
||||
}
|
||||
}
|
||||
pub use bidi::{TextDirection, detect_base_direction};
|
||||
pub use bindings::{INPUT_CONTEXT, InputBindings, bind_input_keys};
|
||||
pub use blink_manager::BlinkManager;
|
||||
pub use handler::*;
|
||||
pub use state::{InputLineLayout, InputState, InputStateEvent};
|
||||
|
||||
166
crates/gpui/src/input/bidi.rs
Normal file
166
crates/gpui/src/input/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/gpui/src/input/bindings.rs
Normal file
512
crates/gpui/src/input/bindings.rs
Normal file
@@ -0,0 +1,512 @@
|
||||
use crate::{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());
|
||||
}
|
||||
126
crates/gpui/src/input/blink_manager.rs
Normal file
126
crates/gpui/src/input/blink_manager.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use crate::Context;
|
||||
use smol::Timer;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Manages cursor blinking state for text input components.
|
||||
///
|
||||
/// `BlinkManager` handles the timing and visibility state for cursor blinking.
|
||||
/// It can be enabled/disabled and paused (e.g., during typing), and tracks
|
||||
/// whether the cursor should be visible at any given moment.
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
visible: bool,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl BlinkManager {
|
||||
/// Creates a new `BlinkManager` with the specified blink interval.
|
||||
///
|
||||
/// The blink manager starts in a disabled state with the cursor visible.
|
||||
pub fn new(blink_interval: Duration, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
blink_interval,
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
}
|
||||
|
||||
/// Pauses cursor blinking temporarily, showing the cursor immediately.
|
||||
///
|
||||
/// This is typically called when the user performs an action like typing,
|
||||
/// to ensure the cursor remains visible during interaction. Blinking will
|
||||
/// automatically resume after the blink interval has elapsed.
|
||||
pub fn pause_blinking(&mut self, cx: &mut Context<Self>) {
|
||||
self.show_cursor(cx);
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(async move |this, cx| {
|
||||
Timer::after(interval).await;
|
||||
this.update(cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
||||
if epoch == self.blink_epoch {
|
||||
self.blinking_paused = false;
|
||||
self.blink_cursors(epoch, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
||||
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
cx.spawn(async move |this, cx| {
|
||||
Timer::after(interval).await;
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| this.blink_cursors(epoch, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the cursor visible immediately.
|
||||
///
|
||||
/// If the cursor is already visible, this is a no-op. Otherwise, it sets
|
||||
/// the cursor to visible and notifies observers.
|
||||
pub fn show_cursor(&mut self, cx: &mut Context<BlinkManager>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables cursor blinking.
|
||||
///
|
||||
/// When enabled, the cursor will alternate between visible and invisible
|
||||
/// states at the configured blink interval. If already enabled, this is a no-op.
|
||||
pub fn enable(&mut self, cx: &mut Context<Self>) {
|
||||
if self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.enabled = true;
|
||||
// Set cursor as invisible and start blinking: this causes cursor
|
||||
// to be visible during the next render.
|
||||
self.visible = false;
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
}
|
||||
|
||||
/// Disables cursor blinking.
|
||||
///
|
||||
/// When disabled, the cursor visibility is set to false and blinking stops.
|
||||
/// Call `show_cursor` after this if you want the cursor to remain visible
|
||||
/// while blinking is disabled.
|
||||
pub fn disable(&mut self, cx: &mut Context<Self>) {
|
||||
self.visible = false;
|
||||
self.enabled = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Returns whether the cursor should currently be rendered as visible.
|
||||
pub fn visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
/// Returns whether cursor blinking is currently enabled.
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
190
crates/gpui/src/input/handler.rs
Normal file
190
crates/gpui/src/input/handler.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use crate::{App, Bounds, Context, Entity, InputHandler, Pixels, UTF16Selection, Window};
|
||||
use std::ops::Range;
|
||||
|
||||
/// 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: crate::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize>;
|
||||
|
||||
/// See [`InputHandler::accepts_text_input`] for details
|
||||
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical implementation of [`crate::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]: crate::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: crate::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<usize> {
|
||||
self.view.update(cx, |view, cx| {
|
||||
view.character_index_for_point(point, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.accepts_text_input(window, cx))
|
||||
}
|
||||
}
|
||||
3350
crates/gpui/src/input/state.rs
Normal file
3350
crates/gpui/src/input/state.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -213,25 +213,24 @@ fn paint_line(
|
||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||
let text_system = cx.text_system().clone();
|
||||
let mut glyph_origin = point(
|
||||
aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
px(0.0),
|
||||
&align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
),
|
||||
origin.y,
|
||||
let line_start_x = aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
px(0.0),
|
||||
&align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
);
|
||||
let mut prev_glyph_position = Point::default();
|
||||
let mut glyph_origin = point(line_start_x, origin.y);
|
||||
let mut current_line_start_x = line_start_x;
|
||||
let mut current_wrap_offset = px(0.0);
|
||||
let mut max_glyph_size = size(px(0.), px(0.));
|
||||
let mut first_glyph_x = origin.x;
|
||||
for (run_ix, run) in layout.runs.iter().enumerate() {
|
||||
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
|
||||
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
glyph_origin.x = current_line_start_x + glyph.position.x - current_wrap_offset;
|
||||
if glyph_ix == 0 && run_ix == 0 {
|
||||
first_glyph_x = glyph_origin.x;
|
||||
}
|
||||
@@ -265,7 +264,7 @@ fn paint_line(
|
||||
strikethrough_origin.y += line_height;
|
||||
}
|
||||
|
||||
glyph_origin.x = aligned_origin_x(
|
||||
current_line_start_x = aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
glyph.position.x,
|
||||
@@ -273,9 +272,10 @@ fn paint_line(
|
||||
layout,
|
||||
wraps.peek(),
|
||||
);
|
||||
current_wrap_offset = glyph.position.x;
|
||||
glyph_origin.x = current_line_start_x;
|
||||
glyph_origin.y += line_height;
|
||||
}
|
||||
prev_glyph_position = glyph.position;
|
||||
|
||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||
@@ -446,24 +446,23 @@ fn paint_line_background(
|
||||
let mut run_end = 0;
|
||||
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||
let text_system = cx.text_system().clone();
|
||||
let mut glyph_origin = point(
|
||||
aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
px(0.0),
|
||||
&align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
),
|
||||
origin.y,
|
||||
let line_start_x = aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
px(0.0),
|
||||
&align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
);
|
||||
let mut prev_glyph_position = Point::default();
|
||||
let mut glyph_origin = point(line_start_x, origin.y);
|
||||
let mut current_line_start_x = line_start_x;
|
||||
let mut current_wrap_offset = px(0.0);
|
||||
let mut max_glyph_size = size(px(0.), px(0.));
|
||||
for (run_ix, run) in layout.runs.iter().enumerate() {
|
||||
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
|
||||
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
glyph_origin.x = current_line_start_x + glyph.position.x - current_wrap_offset;
|
||||
|
||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
wraps.next();
|
||||
@@ -483,7 +482,7 @@ fn paint_line_background(
|
||||
background_origin.y += line_height;
|
||||
}
|
||||
|
||||
glyph_origin.x = aligned_origin_x(
|
||||
current_line_start_x = aligned_origin_x(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
glyph.position.x,
|
||||
@@ -491,9 +490,10 @@ fn paint_line_background(
|
||||
layout,
|
||||
wraps.peek(),
|
||||
);
|
||||
current_wrap_offset = glyph.position.x;
|
||||
glyph_origin.x = current_line_start_x;
|
||||
glyph_origin.y += line_height;
|
||||
}
|
||||
prev_glyph_position = glyph.position;
|
||||
|
||||
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||
if glyph.index >= run_end {
|
||||
|
||||
@@ -7,7 +7,8 @@ mod terminal_slash_command;
|
||||
pub mod terminal_tab_tooltip;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
|
||||
use editor::{EditorSettings, actions::SelectAll};
|
||||
use gpui::input::BlinkManager;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
@@ -234,18 +235,7 @@ impl TerminalView {
|
||||
|
||||
let scroll_handle = TerminalScrollHandle::new(terminal.read(cx));
|
||||
|
||||
let blink_manager = cx.new(|cx| {
|
||||
BlinkManager::new(
|
||||
CURSOR_BLINK_INTERVAL,
|
||||
|cx| {
|
||||
!matches!(
|
||||
TerminalSettings::get_global(cx).blinking,
|
||||
TerminalBlink::Off
|
||||
)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
|
||||
|
||||
let _subscriptions = vec![
|
||||
focus_in,
|
||||
|
||||
@@ -4757,6 +4757,7 @@ mod tests {
|
||||
"git_panel",
|
||||
"go_to_line",
|
||||
"icon_theme_selector",
|
||||
"input",
|
||||
"journal",
|
||||
"keymap_editor",
|
||||
"keystroke_input",
|
||||
|
||||
Reference in New Issue
Block a user