Add cursor_layout

This commit is contained in:
Jason Lee 2024-06-24 11:21:30 +08:00
parent ae5e4f6149
commit b70f5d6dc7
9 changed files with 339 additions and 239 deletions

View file

@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, AbsoluteLength, DefiniteLength, Div, Half, Hsla, IntoElement, div, prelude::FluentBuilder as _, AbsoluteLength, DefiniteLength, Div, Hsla, IntoElement,
ParentElement, RenderOnce, SharedString, Styled, WindowContext, ParentElement, RenderOnce, SharedString, Styled, WindowContext,
}; };

View file

@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, ClickEvent, IntoElement, ParentElement as _, Render, RenderOnce, Styled as _, ViewContext, div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext,
WindowContext, WindowContext,
}; };

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext, div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext,
WindowContext, VisualContext as _, WindowContext,
}; };
use crate::text_field::TextField; use crate::text_field::TextField;
@ -24,8 +24,8 @@ impl Render for InputStory {
.flex_col() .flex_col()
.justify_start() .justify_start()
.gap_3() .gap_3()
.child(TextField::new(cx, "Enter text here...", false)) .child(cx.new_view(|cx| TextField::new("Enter text here...", false, cx)))
.child(TextField::new(cx, "Enter text here...", false)), .child(cx.new_view(|cx| TextField::new("Enter text here...", false, cx))),
) )
} }
} }

View file

@ -1,5 +0,0 @@
mod blink_manager;
mod cursor_layout;
mod text_field;
pub use text_field::*;

View file

@ -11,7 +11,7 @@ pub struct BlinkManager {
} }
impl BlinkManager { impl BlinkManager {
pub fn new(blink_interval: Duration) -> Self { pub fn new(blink_interval: Duration, _cx: &mut ModelContext<Self>) -> Self {
Self { Self {
blink_interval, blink_interval,
blink_epoch: 0, blink_epoch: 0,
@ -36,4 +36,12 @@ impl BlinkManager {
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) { pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = false; self.enabled = false;
} }
pub fn enable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = true;
}
pub fn visible(&self) -> bool {
self.visible
}
} }

View file

@ -1,5 +1,6 @@
use gpui::{outline, px, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext}; use gpui::{outline, px, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext, WindowContext};
#[derive(Debug, Clone)]
pub struct CursorLayout { pub struct CursorLayout {
origin: gpui::Point<Pixels>, origin: gpui::Point<Pixels>,
#[allow(unused)] #[allow(unused)]
@ -36,7 +37,7 @@ impl CursorLayout {
} }
} }
pub fn paint(&mut self, origin: gpui::Point<Pixels>, cx: &mut ViewContext<Self>) { pub fn paint(&mut self, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
let bounds = self.bounds(origin); let bounds = self.bounds(origin);
let cursor = outline(bounds, self.color); let cursor = outline(bounds, self.color);

View file

@ -1,30 +1,41 @@
use std::{ops::Range, time::Duration}; mod blink_manager;
mod cursor_layout;
use gpui::{ mod text_view;
div, ClipboardItem, Context, EventEmitter, FocusHandle, HighlightStyle, InteractiveElement,
InteractiveText, IntoElement, KeyDownEvent, Model, ParentElement, Render, RenderOnce, Styled,
StyledText, TextStyle, View, ViewContext, VisualContext, WindowContext,
};
use crate::{disableable::Disableable, theme::Theme}; use crate::{disableable::Disableable, theme::Theme};
use blink_manager::BlinkManager;
use gpui::{
div, px, relative, ClipboardItem, Context, Element, EventEmitter, FocusHandle, HighlightStyle,
InteractiveElement, InteractiveText, IntoElement, KeyDownEvent, Model, ParentElement, Pixels,
Render, Style, Styled, StyledText, TextStyle, View, ViewContext, VisualContext, WindowContext,
};
use std::{ops::Range, time::Duration};
use text_view::TextView;
use super::{blink_manager::BlinkManager, cursor_layout::CursorLayout}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(IntoElement, Clone)] #[derive(Clone)]
pub struct TextField { pub struct TextField {
focus_handle: FocusHandle, focus_handle: FocusHandle,
disable: bool, disable: bool,
blink_manager: Model<BlinkManager>,
pub view: View<TextView>, pub view: View<TextView>,
} }
impl TextField { impl TextField {
pub fn new(cx: &mut WindowContext, placeholder: &str, disable: bool) -> Self { pub fn new(placeholder: &str, disable: bool, cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let view = TextView::init(cx, &focus_handle, placeholder, disable); let view = TextView::init(cx, &focus_handle, placeholder, disable);
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
cx.on_focus(&focus_handle, Self::handle_focus).detach();
cx.on_blur(&focus_handle, Self::handle_blur).detach();
Self { Self {
focus_handle, focus_handle,
view, view,
blink_manager,
disable, disable,
} }
} }
@ -32,6 +43,22 @@ impl TextField {
pub fn focus(&self, cx: &mut WindowContext) { pub fn focus(&self, cx: &mut WindowContext) {
cx.focus(&self.focus_handle); cx.focus(&self.focus_handle);
} }
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(TextEvent::Focus);
self.blink_manager.update(cx, BlinkManager::enable);
cx.notify();
}
fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(TextEvent::Blur);
self.blink_manager.update(cx, BlinkManager::disable);
cx.notify();
}
pub fn show_cursor(&self, cx: &mut WindowContext) -> bool {
self.blink_manager.read(cx).visible() && self.focus_handle.is_focused(cx)
}
} }
impl Disableable for TextField { impl Disableable for TextField {
@ -41,12 +68,13 @@ impl Disableable for TextField {
} }
} }
impl RenderOnce for TextField { impl Render for TextField {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement {
cx.focus(&self.focus_handle); cx.focus(&self.focus_handle);
let theme = cx.global::<Theme>(); let theme = cx.global::<Theme>();
let clone = self.view.clone(); let text_view = self.view.clone();
let text_view1 = text_view.clone();
div() div()
.border_color(if self.focus_handle.is_focused(cx) { .border_color(if self.focus_handle.is_focused(cx) {
@ -57,7 +85,7 @@ impl RenderOnce for TextField {
.border_1() .border_1()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_key_down(move |ev, cx| { .on_key_down(move |ev, cx| {
self.view.update(cx, |editor, cx| { text_view1.update(cx, |editor, cx| {
let prev = editor.text.clone(); let prev = editor.text.clone();
cx.emit(TextEvent::KeyDown(ev.clone())); cx.emit(TextEvent::KeyDown(ev.clone()));
let keystroke = &ev.keystroke.key; let keystroke = &ev.keystroke.key;
@ -181,213 +209,15 @@ impl RenderOnce for TextField {
} else { } else {
theme.base theme.base
}) })
.child(clone) .child(text_view)
} }
} }
pub enum TextEvent { pub enum TextEvent {
Input { text: String }, Input { text: String },
Blur, Blur,
Focus,
KeyDown(KeyDownEvent), KeyDown(KeyDownEvent),
} }
impl EventEmitter<TextEvent> for TextView {} impl EventEmitter<TextEvent> for TextField {}
pub struct TextView {
pub text: String,
pub placeholder: String,
pub word_click: (usize, u16),
pub selection: Range<usize>,
pub disable: bool,
pub blink_manager: Model<BlinkManager>,
pub cursor: CursorLayout,
}
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
impl TextView {
pub fn init(
cx: &mut WindowContext,
focus_handle: &FocusHandle,
placeholder: &str,
disable: bool,
) -> View<Self> {
let blink_manager = cx.new_model(|_cx| BlinkManager::new(CURSOR_BLINK_INTERVAL));
let cursor = CursorLayout::new(
gpui::Point::new(gpui::px(0.0), gpui::px(0.0)),
gpui::px(2.0),
gpui::px(20.0),
gpui::Hsla {
h: 0.0,
s: 0.0,
l: 0.0,
a: 1.0,
},
None,
);
let m = Self {
text: String::new(),
placeholder: placeholder.to_string(),
word_click: (0, 0),
selection: 0..0,
blink_manager,
cursor,
disable,
};
let view = cx.new_view(|cx| {
cx.on_blur(
focus_handle,
|view: &mut TextView, cx: &mut ViewContext<'_, TextView>| {
view.blink_manager.update(cx, BlinkManager::disable);
cx.emit(TextEvent::Blur);
},
)
.detach();
cx.on_focus(focus_handle, |view, cx| {
view.blink_manager.update(cx, |bm, cx| {
bm.blink_cursor(0, cx);
});
})
.detach();
m
});
cx.subscribe(
&view,
move |subscriber, emitter: &TextEvent, cx| match emitter {
TextEvent::Input { text: _ } => {
subscriber.update(cx, |editor, cx| {
editor.word_click = (0, 0);
});
}
TextEvent::Blur => {
subscriber.update(cx, |editor, cx| {
editor.blink_manager.update(cx, BlinkManager::disable);
editor.word_click = (0, 0);
});
}
_ => {}
},
)
.detach();
view
}
pub fn select_all(&mut self, cx: &mut ViewContext<Self>) {
self.selection = 0..self.text.len();
cx.notify();
}
pub fn word_ranges(&self) -> Vec<Range<usize>> {
let mut words = Vec::new();
let mut last_was_boundary = true;
let mut word_start = 0;
let s = self.text.clone();
for (i, c) in s.char_indices() {
if c.is_alphanumeric() || c == '_' {
if last_was_boundary {
word_start = i;
}
last_was_boundary = false;
} else {
if !last_was_boundary {
words.push(word_start..i);
}
last_was_boundary = true;
}
}
// Check if the last characters form a word and push it if so
if !last_was_boundary {
words.push(word_start..s.len());
}
words
}
pub fn char_range_to_text_range(&self, text: &str) -> Range<usize> {
let start = text
.chars()
.take(self.selection.start)
.collect::<String>()
.len();
let end = text
.chars()
.take(self.selection.end)
.collect::<String>()
.len();
start..end
}
pub fn set_text(&mut self, text: impl ToString, cx: &mut ViewContext<Self>) {
self.text = text.to_string();
self.selection = self.text.len()..self.text.len();
cx.notify();
cx.emit(TextEvent::Input {
text: self.text.clone(),
});
}
}
impl Render for TextView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.global::<Theme>();
let mut text = self.text.clone();
let mut style = TextStyle {
color: theme.text,
font_family: theme.font_sans.clone(),
..Default::default()
};
let mut selection_style = HighlightStyle::default();
let mut color = theme.lavender;
color.fade_out(0.8);
selection_style.background_color = Some(color);
let highlights = vec![(self.char_range_to_text_range(&text), selection_style)];
let styled_text: StyledText = if text.is_empty() {
text = self.placeholder.to_string();
style.color = theme.subtext0;
StyledText::new(text).with_highlights(&style, highlights)
} else {
StyledText::new(text).with_highlights(&style, highlights)
};
let view = cx.view().clone();
InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| {
view.update(cx, |text_view, cx| {
let (index, mut count) = text_view.word_click;
if index == ev {
count += 1;
} else {
count = 1;
}
match count {
2 => {
let word_ranges = text_view.word_ranges();
text_view.selection = word_ranges.get(ev).unwrap().clone();
}
3 => {
// Should select the line
}
4 => {
count = 0;
text_view.selection = 0..text_view.text.len();
}
_ => {}
}
text_view.word_click = (ev, count);
cx.notify();
});
})
}
}

View file

@ -0,0 +1,270 @@
use std::{ops::Range, time::Duration};
use gpui::{
div, px, relative, ClipboardItem, Context, Element, EventEmitter, FocusHandle, HighlightStyle,
InteractiveElement, InteractiveText, IntoElement, KeyDownEvent, Model, ParentElement, Pixels,
Render, Style, Styled, StyledText, TextStyle, View, ViewContext, VisualContext, WindowContext,
};
use crate::{disableable::Disableable, hls, theme::Theme};
use super::{
blink_manager::BlinkManager, cursor_layout::CursorLayout, TextEvent, CURSOR_BLINK_INTERVAL,
};
pub struct TextView {
pub text: String,
pub placeholder: String,
pub word_click: (usize, u16),
pub selection: Range<usize>,
pub disable: bool,
pub blink_manager: Model<BlinkManager>,
pub cursor: CursorLayout,
}
impl EventEmitter<TextEvent> for TextView {}
impl TextView {
pub fn init(
cx: &mut WindowContext,
focus_handle: &FocusHandle,
placeholder: &str,
disable: bool,
) -> View<Self> {
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let cursor = CursorLayout::new(
gpui::Point::new(gpui::px(0.0), gpui::px(0.0)),
px(2.0),
px(20.0),
hls(212., 92., 45.),
None,
);
let m = Self {
text: String::new(),
placeholder: placeholder.to_string(),
word_click: (0, 0),
selection: 0..0,
blink_manager,
cursor,
disable,
};
let view = cx.new_view(|cx| {
cx.on_blur(
focus_handle,
|view: &mut TextView, cx: &mut ViewContext<'_, TextView>| {
view.blink_manager.update(cx, BlinkManager::disable);
cx.emit(TextEvent::Blur);
},
)
.detach();
cx.on_focus(focus_handle, |view, cx| {
view.blink_manager.update(cx, |bm, cx| {
bm.blink_cursor(0, cx);
});
})
.detach();
m
});
cx.subscribe(
&view,
move |subscriber, emitter: &TextEvent, cx| match emitter {
TextEvent::Input { text: _ } => {
subscriber.update(cx, |editor, cx| {
editor.word_click = (0, 0);
});
}
TextEvent::Blur => {
subscriber.update(cx, |editor, cx| {
editor.blink_manager.update(cx, BlinkManager::disable);
editor.word_click = (0, 0);
});
}
_ => {}
},
)
.detach();
view
}
pub fn select_all(&mut self, cx: &mut ViewContext<Self>) {
self.selection = 0..self.text.len();
cx.notify();
}
pub fn word_ranges(&self) -> Vec<Range<usize>> {
let mut words = Vec::new();
let mut last_was_boundary = true;
let mut word_start = 0;
let s = self.text.clone();
for (i, c) in s.char_indices() {
if c.is_alphanumeric() || c == '_' {
if last_was_boundary {
word_start = i;
}
last_was_boundary = false;
} else {
if !last_was_boundary {
words.push(word_start..i);
}
last_was_boundary = true;
}
}
// Check if the last characters form a word and push it if so
if !last_was_boundary {
words.push(word_start..s.len());
}
words
}
pub fn char_range_to_text_range(&self, text: &str) -> Range<usize> {
let start = text
.chars()
.take(self.selection.start)
.collect::<String>()
.len();
let end = text
.chars()
.take(self.selection.end)
.collect::<String>()
.len();
start..end
}
pub fn set_text(&mut self, text: impl ToString, cx: &mut ViewContext<Self>) {
self.text = text.to_string();
self.selection = self.text.len()..self.text.len();
cx.notify();
cx.emit(TextEvent::Input {
text: self.text.clone(),
});
}
fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) {
let mut cursor = self.cursor.clone();
dbg!("--------- paint_cursors", &cursor);
cursor.paint(layout.content_origin, cx);
}
}
impl IntoElement for TextView {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
pub struct TextLayout {
content_origin: gpui::Point<gpui::Pixels>,
}
impl Element for TextView {
type RequestLayoutState = ();
type PrepaintState = TextLayout;
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn request_layout(
&mut self,
id: Option<&gpui::GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(24.).into();
(cx.request_layout(style, None), ())
}
fn prepaint(
&mut self,
id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
TextLayout {
content_origin: bounds.origin,
}
}
fn paint(
&mut self,
id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
layout: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
self.paint_cursors(layout, cx);
}
}
impl Render for TextView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.global::<Theme>();
let mut text = self.text.clone();
let mut style = TextStyle {
color: theme.text,
font_family: theme.font_sans.clone(),
..Default::default()
};
let mut selection_style = HighlightStyle::default();
let mut color = theme.lavender;
color.fade_out(0.8);
selection_style.background_color = Some(color);
let highlights = vec![(self.char_range_to_text_range(&text), selection_style)];
let styled_text: StyledText = if text.is_empty() {
text = self.placeholder.to_string();
style.color = theme.subtext0;
StyledText::new(text).with_highlights(&style, highlights)
} else {
StyledText::new(text).with_highlights(&style, highlights)
};
let view = cx.view().clone();
InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| {
view.update(cx, |text_view, cx| {
let (index, mut count) = text_view.word_click;
if index == ev {
count += 1;
} else {
count = 1;
}
match count {
2 => {
let word_ranges = text_view.word_ranges();
text_view.selection = word_ranges.get(ev).unwrap().clone();
}
3 => {
// Should select the line
}
4 => {
count = 0;
text_view.selection = 0..text_view.text.len();
}
_ => {}
}
text_view.word_click = (ev, count);
cx.notify();
});
})
}
}

View file

@ -1,13 +1,7 @@
use gpui::{prelude::FluentBuilder, *}; use gpui::{prelude::FluentBuilder, *};
use std::sync::Arc; use std::sync::Arc;
use ui::{ use ui::{button::Button, disableable::Clickable as _, text_field::TextField, theme::Theme, Color};
button::{Button, ButtonStyle},
disableable::Clickable as _,
text_field::TextField,
theme::Theme,
Color,
};
use util::ResultExt as _; use util::ResultExt as _;
mod app_state; mod app_state;
@ -127,10 +121,12 @@ impl Render for Workspace {
.child(ui::story::Stories::view(cx)), .child(ui::story::Stories::view(cx)),
) )
.child({ .child({
let txt = TextField::new(cx, "Enter text here...", false); cx.new_view(|cx| {
txt.view let txt = TextField::new("Enter text here...", false, cx);
.update(cx, |this, cx| this.set_text("This is default text.", cx)); txt.view
txt .update(cx, |this, cx| this.set_text("This is default text.", cx));
txt
})
}) })
} }
} }