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
This commit is contained in:
parent
138fc9c7b5
commit
21f0e1c78a
3 changed files with 138 additions and 6 deletions
|
|
@ -300,6 +300,11 @@ impl TextInput {
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unselect(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.selected_range = self.cursor_offset()..self.cursor_offset();
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
fn offset_from_utf16(&self, offset: usize) -> usize {
|
fn offset_from_utf16(&self, offset: usize) -> usize {
|
||||||
let mut utf8_offset = 0;
|
let mut utf8_offset = 0;
|
||||||
let mut utf16_count = 0;
|
let mut utf16_count = 0;
|
||||||
|
|
@ -365,10 +370,80 @@ impl TextInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
|
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.unselect(cx);
|
||||||
self.blink_cursor.update(cx, |blink_cursor, cx| {
|
self.blink_cursor.update(cx, |blink_cursor, cx| {
|
||||||
blink_cursor.pause(cx);
|
blink_cursor.pause(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_mouse_left_down(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
text_hitbox: Hitbox,
|
||||||
|
cx: &mut ViewContext<TextInput>,
|
||||||
|
) {
|
||||||
|
// 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<Self>,
|
||||||
|
) {
|
||||||
|
// 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<Pixels>, 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 {
|
impl ViewInputHandler for TextInput {
|
||||||
|
|
@ -475,10 +550,47 @@ struct TextElement {
|
||||||
input: View<TextInput>,
|
input: View<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
struct PrepaintState {
|
||||||
line: Option<ShapedLine>,
|
line: Option<ShapedLine>,
|
||||||
cursor: Option<PaintQuad>,
|
cursor: Option<PaintQuad>,
|
||||||
selection: Option<PaintQuad>,
|
selection: Option<PaintQuad>,
|
||||||
|
hitbox: Hitbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for TextElement {
|
impl IntoElement for TextElement {
|
||||||
|
|
@ -491,7 +603,6 @@ impl IntoElement for TextElement {
|
||||||
|
|
||||||
impl Element for TextElement {
|
impl Element for TextElement {
|
||||||
type RequestLayoutState = ();
|
type RequestLayoutState = ();
|
||||||
|
|
||||||
type PrepaintState = PrepaintState;
|
type PrepaintState = PrepaintState;
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
|
@ -606,10 +717,14 @@ impl Element for TextElement {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let hitbox = cx.insert_hitbox(bounds, false);
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
line: Some(line),
|
line: Some(line),
|
||||||
cursor,
|
cursor,
|
||||||
selection,
|
selection,
|
||||||
|
hitbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -642,6 +757,7 @@ impl Element for TextElement {
|
||||||
self.input.update(cx, |input, _cx| {
|
self.input.update(cx, |input, _cx| {
|
||||||
input.last_layout = Some(line);
|
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))
|
.when_some(self.prefix.clone(), |this, prefix| this.child(prefix))
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(div().flex_grow().overflow_x_hidden().child(TextElement {
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_grow()
|
||||||
|
.overflow_x_hidden()
|
||||||
|
.cursor_text()
|
||||||
|
.child(TextElement {
|
||||||
input: cx.view().clone(),
|
input: cx.view().clone(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.when_some(self.suffix.clone(), |this, suffix| this.child(suffix))
|
.when_some(self.suffix.clone(), |this, suffix| this.child(suffix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
pub trait ActiveTheme {
|
||||||
fn theme(&self) -> &Theme;
|
fn theme(&self) -> &Theme;
|
||||||
|
|
@ -264,6 +266,7 @@ pub struct Theme {
|
||||||
pub title_bar_background: Hsla,
|
pub title_bar_background: Hsla,
|
||||||
/// Basic font size
|
/// Basic font size
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
|
pub font_family: SharedString,
|
||||||
pub background: Hsla,
|
pub background: Hsla,
|
||||||
pub foreground: Hsla,
|
pub foreground: Hsla,
|
||||||
pub card: Hsla,
|
pub card: Hsla,
|
||||||
|
|
@ -319,6 +322,13 @@ impl From<Colors> for Theme {
|
||||||
mode: ThemeMode::Dark,
|
mode: ThemeMode::Dark,
|
||||||
transparent: Hsla::transparent_black(),
|
transparent: Hsla::transparent_black(),
|
||||||
font_size: 14.0,
|
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,
|
radius: 4.0,
|
||||||
title_bar_background: colors.title_bar_background,
|
title_bar_background: colors.title_bar_background,
|
||||||
background: colors.background,
|
background: colors.background,
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ impl Render for Workspace {
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
// .font(cx.theme.font_size)
|
.font_family(cx.theme().font_family.clone())
|
||||||
.gap_0()
|
.gap_0()
|
||||||
.justify_start()
|
.justify_start()
|
||||||
.items_start()
|
.items_start()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue