Compare commits

...

49 Commits

Author SHA1 Message Date
Mikayla Maki
bf8b7274ba Code from office hours 2025-12-09 10:06:18 -08:00
iamnbutler
8c2e1b48fb use absolute glyph positions (unbreak RTL) 2025-12-02 14:42:37 -05:00
iamnbutler
2e2477f29a tidy up noisy logs 2025-12-01 14:13:55 -05:00
iamnbutler
baf2889a48 Blur on esc 2025-12-01 14:12:27 -05:00
iamnbutler
3bb9cd666b Emit events from InputState 2025-12-01 14:10:54 -05:00
iamnbutler
e601ec2750 Support new actions
- `DeleteWordLeft` (`alt-backspace`) - `DeleteWordRight` (`alt-delete`)
- `DeleteToBeginningOfLine` (`cmd-backspace`) - `DeleteToEndOfLine`
(`ctrl-k`)
2025-12-01 09:46:02 -05:00
iamnbutler
b96c966113 Remove redundant text style refinement 2025-11-29 13:38:40 -05:00
iamnbutler
54b3f7fad5 Update state.rs 2025-11-29 13:28:31 -05:00
iamnbutler
1bbebcfd0b Add at_top/at_bottom 2025-11-28 23:03:08 -05:00
iamnbutler
bb1382c03a Allow setting text style 2025-11-28 23:02:54 -05:00
iamnbutler
24521bdcdf Pass entire style 2025-11-28 22:45:16 -05:00
iamnbutler
1929a805ac Allow externally refining the input text style 2025-11-28 22:35:05 -05:00
iamnbutler
45d9fcadbf More sandbox 2025-11-28 21:07:31 -05:00
iamnbutler
35198599ac blink cursor by default 2025-11-28 20:44:15 -05:00
iamnbutler
1568de039b More layout 2025-11-28 20:11:42 -05:00
iamnbutler
65ef4e8abc Update sandbox 2025-11-28 19:54:47 -05:00
iamnbutler
3507eefabb Start on cursor blink 2025-11-28 17:38:09 -05:00
iamnbutler
338c37796d move blink_manager to gpui 2025-11-28 17:21:00 -05:00
iamnbutler
801c6645ff use workspace 2025-11-28 16:58:21 -05:00
iamnbutler
7fde67ff48 approve input namespace 2025-11-28 16:47:52 -05:00
iamnbutler
e6ad27dec3 Merge remote-tracking branch 'upstream/main' into gpui/text-area 2025-11-28 16:20:01 -05:00
iamnbutler
081c2048aa Remove logs 2025-11-28 15:44:14 -05:00
iamnbutler
3d5d66e35a Reorganize 2025-11-28 15:28:43 -05:00
iamnbutler
26522ea60f Add better dx for binding keys to inputs 2025-11-28 15:18:14 -05:00
iamnbutler
b0a51ed68e add undo/redo 2025-11-28 14:36:51 -05:00
iamnbutler
7ce563a4e8 wip unified 2025-11-28 14:23:53 -05:00
iamnbutler
226ebd7579 add basic rtl support 2025-11-28 13:29:06 -05:00
iamnbutler
ace07d6afd wip: single line input 2025-11-28 10:34:28 -05:00
iamnbutler
3a25ef2682 Support singleline inputs 2025-11-27 18:19:05 -05:00
iamnbutler
96cbe7e6f0 Update input.rs 2025-11-26 19:14:41 -05:00
iamnbutler
eb0767146d input -> input state 2025-11-26 19:14:28 -05:00
iamnbutler
914e795d90 More cleanup 2025-11-26 19:09:07 -05:00
iamnbutler
cb748e2fb2 tidy up paint_cursor, is_cursor_in_line 2025-11-26 18:52:49 -05:00
iamnbutler
749adff2ef Reorganize input 2025-11-26 18:39:48 -05:00
iamnbutler
4b6e90e0d0 remove empty test sections 2025-11-26 13:44:12 -05:00
iamnbutler
c287fb8cc8 clippy 2025-11-26 13:44:12 -05:00
iamnbutler
ba8ed43004 Update text_area.rs 2025-11-26 13:44:12 -05:00
iamnbutler
3636fe5031 Update input.rs 2025-11-26 13:44:12 -05:00
iamnbutler
aebebe0996 wip 2025-11-26 13:44:12 -05:00
iamnbutler
2f27430348 Remove stickies demo from text-area branch 2025-11-26 13:44:12 -05:00
iamnbutler
de003caaa0 Fix cursor disappearing at end of line in TextArea
The cursor at text_range.end was not recognized as belonging to a line
because Rust ranges are exclusive (0..13 doesn't contain 13). This caused
the cursor to disappear when positioned at the end of any line.

Fixed is_cursor_in_line to treat cursor_offset == text_range.end as valid.
2025-11-26 13:44:12 -05:00
iamnbutler
cc03d1b419 work on debugging eol issue 2025-11-26 13:44:12 -05:00
iamnbutler
987e0edd2a Add more modifier variants for Enter and Backspace keybindings
Enter should insert newline regardless of modifier (shift, alt, ctrl).
Backspace should delete regardless of modifier.

The philosophy is: these are the default behaviors, and specific apps
can override if they want different behavior (e.g., ctrl-enter to submit).
2025-11-26 13:44:12 -05:00
iamnbutler
e58402de33 Fix cursor positioning at end of line before newline
Previously, the cursor would skip over newline characters when navigating
with arrow keys, making it impossible to position the cursor at the end of
a line (right before the newline). This was incorrect behavior.

Now:
- Right arrow from 'b' in 'ab\ncd' stops at position 2 (after 'b', before '\n')
- Left arrow from 'c' in 'ab\ncd' stops at position 2 (after 'b', before '\n')
- Another arrow press crosses the newline to continue navigation

This allows clicking at the end of a line to work correctly, and arrow
key navigation is now consistent with click behavior.
2025-11-26 13:44:12 -05:00
iamnbutler
4d23776315 Add missing keybindings in text_area example
Added keybindings for:
- shift-enter -> Enter (insert newline)
- tab -> Tab (insert tab character)
- shift-backspace -> Backspace (delete character, macOS convention)
2025-11-26 13:44:12 -05:00
iamnbutler
357805a819 Add Tab action to insert tab character in TextArea
The Tab key was not inserting any characters. Added:
- Tab action definition in input.rs
- Input::tab() method to insert '\t' character
- Registered Tab action in TextArea
2025-11-26 13:44:12 -05:00
iamnbutler
e5e646c24e Update input.rs 2025-11-26 13:44:12 -05:00
iamnbutler
17bc109746 support IME marked text 2025-11-26 13:44:12 -05:00
iamnbutler
dceb76e334 Start on TextArea 2025-11-26 13:44:12 -05:00
20 changed files with 6389 additions and 356 deletions

1
Cargo.lock generated
View File

@@ -7348,6 +7348,7 @@ dependencies = [
"sum_tree",
"taffy",
"thiserror 2.0.17",
"unicode-bidi",
"unicode-segmentation",
"usvg",
"util",

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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"

View 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"#;

View 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

View File

@@ -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),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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::*;

View File

@@ -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;

View File

@@ -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};

View 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);
}
}

View 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());
}

View 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
}
}

View 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))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 {

View File

@@ -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,

View File

@@ -4757,6 +4757,7 @@ mod tests {
"git_panel",
"go_to_line",
"icon_theme_selector",
"input",
"journal",
"keymap_editor",
"keystroke_input",