gpui-component/crates/ui/src/text_field/text_view.rs
2024-06-24 15:39:26 +08:00

274 lines
7.8 KiB
Rust

use std::ops::Range;
use super::{
blink_manager::BlinkManager, cursor_layout::CursorLayout, TextEvent, CURSOR_BLINK_INTERVAL,
};
use crate::theme::{Colorize as _, Theme};
use gpui::{
px, relative, Context, Element, EventEmitter, FocusHandle, HighlightStyle, InteractiveText,
IntoElement, Model, Render, Style, StyledText, TextStyle, View, ViewContext, VisualContext,
WindowContext,
};
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,
pub masked: bool,
}
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 theme = cx.global::<Theme>();
let cursor = CursorLayout::new(
gpui::Point::new(gpui::px(0.0), gpui::px(0.0)),
px(2.0),
px(20.0),
theme.ring,
None,
);
let m = Self {
text: String::new(),
placeholder: placeholder.to_string(),
word_click: (0, 0),
selection: 0..0,
blink_manager,
cursor,
disable,
masked: false,
};
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();
dbg!("--------- request_layout", &style);
(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();
dbg!("textView render", &text);
let mut style = TextStyle {
color: theme.foreground,
..Default::default()
};
if self.masked {
text = "".repeat(text.len());
}
let mut selection_style = HighlightStyle::default();
let color = theme.foreground.opacity(0.8);
selection_style.background_color = Some(color);
let mut highlights = vec![(self.char_range_to_text_range(&text), selection_style)];
if text.is_empty() {
text = self.placeholder.to_string();
style.color = theme.muted_foreground;
highlights = vec![];
}
let styled_text = 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();
});
})
}
}