mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-11 05:00:16 +00:00
bc0359a474
Release Notes: - N/A
574 lines
18 KiB
Rust
574 lines
18 KiB
Rust
use std::ops::Range;
|
|
|
|
use gpui::*;
|
|
use unicode_segmentation::*;
|
|
|
|
actions!(
|
|
text_input,
|
|
[
|
|
Backspace,
|
|
Delete,
|
|
Left,
|
|
Right,
|
|
SelectLeft,
|
|
SelectRight,
|
|
SelectAll,
|
|
Home,
|
|
End,
|
|
ShowCharacterPalette
|
|
]
|
|
);
|
|
|
|
struct TextInput {
|
|
focus_handle: FocusHandle,
|
|
content: SharedString,
|
|
selected_range: Range<usize>,
|
|
selection_reversed: bool,
|
|
marked_range: Option<Range<usize>>,
|
|
last_layout: Option<ShapedLine>,
|
|
last_bounds: Option<Bounds<Pixels>>,
|
|
is_selecting: bool,
|
|
}
|
|
|
|
impl TextInput {
|
|
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
|
} else {
|
|
self.move_to(self.selected_range.start, cx)
|
|
}
|
|
}
|
|
|
|
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
|
} else {
|
|
self.move_to(self.selected_range.end, cx)
|
|
}
|
|
}
|
|
|
|
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
|
}
|
|
|
|
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
|
}
|
|
|
|
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
|
self.move_to(0, cx);
|
|
self.select_to(self.content.len(), cx)
|
|
}
|
|
|
|
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
|
|
self.move_to(0, cx);
|
|
}
|
|
|
|
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
|
|
self.move_to(self.content.len(), cx);
|
|
}
|
|
|
|
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
|
}
|
|
self.replace_text_in_range(None, "", cx)
|
|
}
|
|
|
|
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
|
}
|
|
self.replace_text_in_range(None, "", cx)
|
|
}
|
|
|
|
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
|
self.is_selecting = true;
|
|
self.move_to(self.index_for_mouse_position(event.position), cx)
|
|
}
|
|
|
|
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext<Self>) {
|
|
self.is_selecting = false;
|
|
}
|
|
|
|
fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut ViewContext<Self>) {
|
|
if self.is_selecting {
|
|
self.select_to(self.index_for_mouse_position(event.position), cx);
|
|
}
|
|
}
|
|
|
|
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
|
cx.show_character_palette();
|
|
}
|
|
|
|
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
|
self.selected_range = offset..offset;
|
|
cx.notify()
|
|
}
|
|
|
|
fn cursor_offset(&self) -> usize {
|
|
if self.selection_reversed {
|
|
self.selected_range.start
|
|
} else {
|
|
self.selected_range.end
|
|
}
|
|
}
|
|
|
|
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
|
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
|
else {
|
|
return 0;
|
|
};
|
|
if position.y < bounds.top() {
|
|
return 0;
|
|
}
|
|
if position.y > bounds.bottom() {
|
|
return self.content.len();
|
|
}
|
|
line.closest_index_for_x(position.x - bounds.left())
|
|
}
|
|
|
|
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
|
if self.selection_reversed {
|
|
self.selected_range.start = offset
|
|
} else {
|
|
self.selected_range.end = offset
|
|
};
|
|
if self.selected_range.end < self.selected_range.start {
|
|
self.selection_reversed = !self.selection_reversed;
|
|
self.selected_range = self.selected_range.end..self.selected_range.start;
|
|
}
|
|
cx.notify()
|
|
}
|
|
|
|
fn offset_from_utf16(&self, offset: usize) -> usize {
|
|
let mut utf8_offset = 0;
|
|
let mut utf16_count = 0;
|
|
|
|
for ch in self.content.chars() {
|
|
if utf16_count >= offset {
|
|
break;
|
|
}
|
|
utf16_count += ch.len_utf16();
|
|
utf8_offset += ch.len_utf8();
|
|
}
|
|
|
|
utf8_offset
|
|
}
|
|
|
|
fn offset_to_utf16(&self, offset: usize) -> usize {
|
|
let mut utf16_offset = 0;
|
|
let mut utf8_count = 0;
|
|
|
|
for ch in self.content.chars() {
|
|
if utf8_count >= offset {
|
|
break;
|
|
}
|
|
utf8_count += ch.len_utf8();
|
|
utf16_offset += ch.len_utf16();
|
|
}
|
|
|
|
utf16_offset
|
|
}
|
|
|
|
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
|
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
|
}
|
|
|
|
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
|
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
|
}
|
|
|
|
fn previous_boundary(&self, offset: usize) -> usize {
|
|
self.content
|
|
.grapheme_indices(true)
|
|
.rev()
|
|
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn next_boundary(&self, offset: usize) -> usize {
|
|
self.content
|
|
.grapheme_indices(true)
|
|
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
|
.unwrap_or(self.content.len())
|
|
}
|
|
}
|
|
|
|
impl ViewInputHandler for TextInput {
|
|
fn text_for_range(
|
|
&mut self,
|
|
range_utf16: Range<usize>,
|
|
_cx: &mut ViewContext<Self>,
|
|
) -> Option<String> {
|
|
let range = self.range_from_utf16(&range_utf16);
|
|
Some(self.content[range].to_string())
|
|
}
|
|
|
|
fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
|
Some(self.range_to_utf16(&self.selected_range))
|
|
}
|
|
|
|
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
|
self.marked_range
|
|
.as_ref()
|
|
.map(|range| self.range_to_utf16(range))
|
|
}
|
|
|
|
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
|
|
self.marked_range = None;
|
|
}
|
|
|
|
fn replace_text_in_range(
|
|
&mut self,
|
|
range_utf16: Option<Range<usize>>,
|
|
new_text: &str,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let range = range_utf16
|
|
.as_ref()
|
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
|
.or(self.marked_range.clone())
|
|
.unwrap_or(self.selected_range.clone());
|
|
|
|
self.content =
|
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
|
.into();
|
|
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
|
self.marked_range.take();
|
|
cx.notify();
|
|
}
|
|
|
|
fn replace_and_mark_text_in_range(
|
|
&mut self,
|
|
range_utf16: Option<Range<usize>>,
|
|
new_text: &str,
|
|
new_selected_range_utf16: Option<Range<usize>>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let range = range_utf16
|
|
.as_ref()
|
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
|
.or(self.marked_range.clone())
|
|
.unwrap_or(self.selected_range.clone());
|
|
|
|
self.content =
|
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
|
.into();
|
|
self.marked_range = Some(range.start..range.start + new_text.len());
|
|
self.selected_range = new_selected_range_utf16
|
|
.as_ref()
|
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
|
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
|
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn bounds_for_range(
|
|
&mut self,
|
|
range_utf16: Range<usize>,
|
|
bounds: Bounds<Pixels>,
|
|
_cx: &mut ViewContext<Self>,
|
|
) -> Option<Bounds<Pixels>> {
|
|
let Some(last_layout) = self.last_layout.as_ref() else {
|
|
return None;
|
|
};
|
|
let range = self.range_from_utf16(&range_utf16);
|
|
Some(Bounds::from_corners(
|
|
point(
|
|
bounds.left() + last_layout.x_for_index(range.start),
|
|
bounds.top(),
|
|
),
|
|
point(
|
|
bounds.left() + last_layout.x_for_index(range.end),
|
|
bounds.bottom(),
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct TextElement {
|
|
input: View<TextInput>,
|
|
}
|
|
|
|
struct PrepaintState {
|
|
line: Option<ShapedLine>,
|
|
cursor: Option<PaintQuad>,
|
|
selection: Option<PaintQuad>,
|
|
}
|
|
|
|
impl IntoElement for TextElement {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for TextElement {
|
|
type RequestLayoutState = ();
|
|
|
|
type PrepaintState = PrepaintState;
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
cx: &mut WindowContext,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
let mut style = Style::default();
|
|
style.size.width = relative(1.).into();
|
|
style.size.height = cx.line_height().into();
|
|
(cx.request_layout(style, []), ())
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_request_layout: &mut Self::RequestLayoutState,
|
|
cx: &mut WindowContext,
|
|
) -> Self::PrepaintState {
|
|
let input = self.input.read(cx);
|
|
let content = input.content.clone();
|
|
let selected_range = input.selected_range.clone();
|
|
let cursor = input.cursor_offset();
|
|
let style = cx.text_style();
|
|
let run = TextRun {
|
|
len: input.content.len(),
|
|
font: style.font(),
|
|
color: style.color,
|
|
background_color: None,
|
|
underline: None,
|
|
strikethrough: None,
|
|
};
|
|
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
|
vec![
|
|
TextRun {
|
|
len: marked_range.start,
|
|
..run.clone()
|
|
},
|
|
TextRun {
|
|
len: marked_range.end - marked_range.start,
|
|
underline: Some(UnderlineStyle {
|
|
color: Some(run.color),
|
|
thickness: px(1.0),
|
|
wavy: false,
|
|
}),
|
|
..run.clone()
|
|
},
|
|
TextRun {
|
|
len: input.content.len() - marked_range.end,
|
|
..run.clone()
|
|
},
|
|
]
|
|
.into_iter()
|
|
.filter(|run| run.len > 0)
|
|
.collect()
|
|
} else {
|
|
vec![run]
|
|
};
|
|
|
|
let font_size = style.font_size.to_pixels(cx.rem_size());
|
|
let line = cx
|
|
.text_system()
|
|
.shape_line(content, font_size, &runs)
|
|
.unwrap();
|
|
|
|
let cursor_pos = line.x_for_index(cursor);
|
|
let (selection, cursor) = if selected_range.is_empty() {
|
|
(
|
|
None,
|
|
Some(fill(
|
|
Bounds::new(
|
|
point(bounds.left() + cursor_pos, bounds.top()),
|
|
size(px(2.), bounds.bottom() - bounds.top()),
|
|
),
|
|
gpui::blue(),
|
|
)),
|
|
)
|
|
} else {
|
|
(
|
|
Some(fill(
|
|
Bounds::from_corners(
|
|
point(
|
|
bounds.left() + line.x_for_index(selected_range.start),
|
|
bounds.top(),
|
|
),
|
|
point(
|
|
bounds.left() + line.x_for_index(selected_range.end),
|
|
bounds.bottom(),
|
|
),
|
|
),
|
|
rgba(0x3311FF30),
|
|
)),
|
|
None,
|
|
)
|
|
};
|
|
PrepaintState {
|
|
line: Some(line),
|
|
cursor,
|
|
selection,
|
|
}
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_request_layout: &mut Self::RequestLayoutState,
|
|
prepaint: &mut Self::PrepaintState,
|
|
cx: &mut WindowContext,
|
|
) {
|
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
|
cx.handle_input(
|
|
&focus_handle,
|
|
ElementInputHandler::new(bounds, self.input.clone()),
|
|
);
|
|
if let Some(selection) = prepaint.selection.take() {
|
|
cx.paint_quad(selection)
|
|
}
|
|
let line = prepaint.line.take().unwrap();
|
|
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
|
|
|
|
if let Some(cursor) = prepaint.cursor.take() {
|
|
cx.paint_quad(cursor);
|
|
}
|
|
self.input.update(cx, |input, _cx| {
|
|
input.last_layout = Some(line);
|
|
input.last_bounds = Some(bounds);
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Render for TextInput {
|
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
div()
|
|
.flex()
|
|
.key_context("TextInput")
|
|
.track_focus(&self.focus_handle)
|
|
.cursor(CursorStyle::IBeam)
|
|
.on_action(cx.listener(Self::backspace))
|
|
.on_action(cx.listener(Self::delete))
|
|
.on_action(cx.listener(Self::left))
|
|
.on_action(cx.listener(Self::right))
|
|
.on_action(cx.listener(Self::select_left))
|
|
.on_action(cx.listener(Self::select_right))
|
|
.on_action(cx.listener(Self::select_all))
|
|
.on_action(cx.listener(Self::home))
|
|
.on_action(cx.listener(Self::end))
|
|
.on_action(cx.listener(Self::show_character_palette))
|
|
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
|
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
|
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
|
.on_mouse_move(cx.listener(Self::on_mouse_move))
|
|
.bg(rgb(0xeeeeee))
|
|
.size_full()
|
|
.line_height(px(30.))
|
|
.text_size(px(24.))
|
|
.child(
|
|
div()
|
|
.h(px(30. + 4. * 2.))
|
|
.w_full()
|
|
.p(px(4.))
|
|
.bg(white())
|
|
.child(TextElement {
|
|
input: cx.view().clone(),
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl FocusableView for TextInput {
|
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
struct InputExample {
|
|
text_input: View<TextInput>,
|
|
recent_keystrokes: Vec<Keystroke>,
|
|
}
|
|
|
|
impl Render for InputExample {
|
|
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
|
div()
|
|
.bg(rgb(0xaaaaaa))
|
|
.flex()
|
|
.flex_col()
|
|
.size_full()
|
|
.child(self.text_input.clone())
|
|
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
|
format!(
|
|
"{:} {}",
|
|
ks,
|
|
if let Some(ime_key) = ks.ime_key.as_ref() {
|
|
format!("-> {}", ime_key)
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
)
|
|
}))
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
App::new().run(|cx: &mut AppContext| {
|
|
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
|
cx.bind_keys([
|
|
KeyBinding::new("backspace", Backspace, None),
|
|
KeyBinding::new("delete", Delete, None),
|
|
KeyBinding::new("left", Left, None),
|
|
KeyBinding::new("right", Right, None),
|
|
KeyBinding::new("shift-left", SelectLeft, None),
|
|
KeyBinding::new("shift-right", SelectRight, None),
|
|
KeyBinding::new("cmd-a", SelectAll, None),
|
|
KeyBinding::new("home", Home, None),
|
|
KeyBinding::new("end", End, None),
|
|
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
|
]);
|
|
let window = cx
|
|
.open_window(
|
|
WindowOptions {
|
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
|
..Default::default()
|
|
},
|
|
|cx| {
|
|
let text_input = cx.new_view(|cx| TextInput {
|
|
focus_handle: cx.focus_handle(),
|
|
content: "".into(),
|
|
selected_range: 0..0,
|
|
selection_reversed: false,
|
|
marked_range: None,
|
|
last_layout: None,
|
|
last_bounds: None,
|
|
is_selecting: false,
|
|
});
|
|
cx.new_view(|_| InputExample {
|
|
text_input,
|
|
recent_keystrokes: vec![],
|
|
})
|
|
},
|
|
)
|
|
.unwrap();
|
|
cx.observe_keystrokes(move |ev, cx| {
|
|
window
|
|
.update(cx, |view, cx| {
|
|
view.recent_keystrokes.push(ev.keystroke.clone());
|
|
cx.notify();
|
|
})
|
|
.unwrap();
|
|
})
|
|
.detach();
|
|
window
|
|
.update(cx, |view, cx| {
|
|
cx.focus_view(&view.text_input);
|
|
cx.activate(true);
|
|
})
|
|
.unwrap();
|
|
});
|
|
}
|