Add cursor_layout
This commit is contained in:
parent
ae5e4f6149
commit
b70f5d6dc7
9 changed files with 339 additions and 239 deletions
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
mod blink_manager;
|
|
||||||
mod cursor_layout;
|
|
||||||
mod text_field;
|
|
||||||
|
|
||||||
pub use text_field::*;
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
270
crates/ui/src/text_field/text_view.rs
Normal file
270
crates/ui/src/text_field/text_view.rs
Normal 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();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue