From 21f0e1c78a11d6c097c8833f991552e3b0f285b0 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 12 Jul 2024 22:41:18 +0800 Subject: [PATCH] Add mouse selection support to Input (#26) Done reset mouse support of https://github.com/zed-industries/zed/pull/13534 https://github.com/user-attachments/assets/71b1d4e4-eef7-4746-8400-b2e3812e5ad4 --- crates/ui/src/input.rs | 130 +++++++++++++++++++++++++++++- crates/ui/src/theme.rs | 12 ++- crates/workspace/src/workspace.rs | 2 +- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index f74a0798..377168fa 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -300,6 +300,11 @@ impl TextInput { cx.notify() } + fn unselect(&mut self, cx: &mut ViewContext) { + self.selected_range = self.cursor_offset()..self.cursor_offset(); + cx.notify() + } + fn offset_from_utf16(&self, offset: usize) -> usize { let mut utf8_offset = 0; let mut utf16_count = 0; @@ -365,10 +370,80 @@ impl TextInput { } fn on_blur(&mut self, cx: &mut ViewContext) { + self.unselect(cx); self.blink_cursor.update(cx, |blink_cursor, cx| { blink_cursor.pause(cx); }); } + + fn on_mouse_left_down( + &mut self, + event: &MouseDownEvent, + text_hitbox: Hitbox, + cx: &mut ViewContext, + ) { + // Ignore if text is empty + if self.text.is_empty() { + return; + } + + if !text_hitbox.contains(&event.position) { + return; + } + + let offset = self.offset_of_position(event.position, &text_hitbox); + if event.modifiers.shift { + self.select_to(offset, cx); + } else { + self.move_to(offset, cx); + } + } + + fn on_drag_move( + &mut self, + event: &MouseMoveEvent, + text_hitbox: Hitbox, + cx: &mut ViewContext, + ) { + // Ignore if text is empty + if self.text.is_empty() { + return; + } + + if self.last_layout.is_none() { + return; + } + + if !text_hitbox.contains(&event.position) { + return; + } + + let offset = self.offset_of_position(event.position, &text_hitbox); + if offset == self.cursor_offset() { + return; + } + self.select_to(offset, cx); + } + + fn offset_of_position(&self, position: Point, hitbox: &Hitbox) -> usize { + let position = position - hitbox.origin; + self.last_layout + .as_ref() + .map(|line| match line.index_for_x(position.x) { + Some(ix) => ix, + None => { + let last_index = line.len(); + // If the mouse is on the right side of the last character, move to the end + // Otherwise, move to the start of the line + if position.x > line.x_for_index(last_index) { + last_index + } else { + 0 + } + } + }) + .unwrap_or(0) + } } impl ViewInputHandler for TextInput { @@ -475,10 +550,47 @@ struct TextElement { input: View, } +impl TextElement { + fn paint_mouse_listeners(&mut self, hitbox: &Hitbox, cx: &mut WindowContext) { + cx.on_mouse_event({ + let input = self.input.clone(); + let hitbox = hitbox.clone(); + + move |event: &MouseDownEvent, phase, cx| { + let hitbox = hitbox.clone(); + if phase == DispatchPhase::Bubble { + match event.button { + MouseButton::Left => { + input.update(cx, |input, cx| { + input.on_mouse_left_down(event, hitbox, cx); + }); + } + _ => {} + } + } + } + }); + + cx.on_mouse_event({ + let input = self.input.clone(); + let hitbox = hitbox.clone(); + + move |event: &MouseMoveEvent, _, cx| { + if event.pressed_button == Some(MouseButton::Left) { + input.update(cx, |input, cx| { + input.on_drag_move(event, hitbox.clone(), cx); + }); + } + } + }); + } +} + struct PrepaintState { line: Option, cursor: Option, selection: Option, + hitbox: Hitbox, } impl IntoElement for TextElement { @@ -491,7 +603,6 @@ impl IntoElement for TextElement { impl Element for TextElement { type RequestLayoutState = (); - type PrepaintState = PrepaintState; fn id(&self) -> Option { @@ -606,10 +717,14 @@ impl Element for TextElement { None, ) }; + + let hitbox = cx.insert_hitbox(bounds, false); + PrepaintState { line: Some(line), cursor, selection, + hitbox, } } @@ -642,6 +757,7 @@ impl Element for TextElement { self.input.update(cx, |input, _cx| { input.last_layout = Some(line); }); + self.paint_mouse_listeners(&prepaint.hitbox, cx); } } @@ -695,9 +811,15 @@ impl Render for TextInput { .when_some(self.prefix.clone(), |this, prefix| this.child(prefix)) .gap_1() .items_center() - .child(div().flex_grow().overflow_x_hidden().child(TextElement { - input: cx.view().clone(), - })) + .child( + div() + .flex_grow() + .overflow_x_hidden() + .cursor_text() + .child(TextElement { + input: cx.view().clone(), + }), + ) .when_some(self.suffix.clone(), |this, suffix| this.child(suffix)) } } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index a9442922..dac9e76f 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -1,4 +1,6 @@ -use gpui::{hsla, point, AppContext, BoxShadow, Global, Hsla, Pixels, WindowAppearance}; +use gpui::{ + hsla, point, AppContext, BoxShadow, Global, Hsla, Pixels, SharedString, WindowAppearance, +}; pub trait ActiveTheme { fn theme(&self) -> &Theme; @@ -264,6 +266,7 @@ pub struct Theme { pub title_bar_background: Hsla, /// Basic font size pub font_size: f32, + pub font_family: SharedString, pub background: Hsla, pub foreground: Hsla, pub card: Hsla, @@ -319,6 +322,13 @@ impl From for Theme { mode: ThemeMode::Dark, transparent: Hsla::transparent_black(), font_size: 14.0, + font_family: if cfg!(target_os = "macos") { + ".SystemUIFont".into() + } else if cfg!(target_os = "windows") { + "Segoe UI".into() + } else { + "FreeMono".into() + }, radius: 4.0, title_bar_background: colors.title_bar_background, background: colors.background, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cb897281..312b04a4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -132,7 +132,7 @@ impl Render for Workspace { .size_full() .flex() .flex_col() - // .font(cx.theme.font_size) + .font_family(cx.theme().font_family.clone()) .gap_0() .justify_start() .items_start()