From 3a745d2abef292fccf608835f853d5a3d621940f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 24 Jun 2024 19:01:11 +0800 Subject: [PATCH] Add invert method to Hsla. --- crates/ui/src/text_field/blink_manager.rs | 26 ++++- crates/ui/src/text_field/cursor_layout.rs | 23 +++- crates/ui/src/text_field/mod.rs | 40 +------ crates/ui/src/text_field/text_view.rs | 130 ++++++++++++++++------ crates/ui/src/theme.rs | 11 ++ 5 files changed, 153 insertions(+), 77 deletions(-) diff --git a/crates/ui/src/text_field/blink_manager.rs b/crates/ui/src/text_field/blink_manager.rs index e178ad9c..6a09c1ae 100644 --- a/crates/ui/src/text_field/blink_manager.rs +++ b/crates/ui/src/text_field/blink_manager.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use gpui::ModelContext; +use gpui::{ModelContext, Timer}; pub struct BlinkManager { blink_interval: Duration, @@ -21,6 +21,30 @@ impl BlinkManager { } } + pub fn pause_blinking(&mut self, cx: &mut ModelContext) { + self.show_cursor(cx); + + let epoch = self.next_blink_epoch(); + let interval = self.blink_interval; + cx.spawn(|this, mut cx| async move { + Timer::after(interval).await; + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) + }) + .detach(); + } + + fn next_blink_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext) { + if epoch == self.blink_epoch { + self.blinking_paused = false; + self.blink_cursor(epoch, cx); + } + } + pub fn show_cursor(&self, _cx: &mut ModelContext<'_, Self>) -> bool { self.enabled && (!self.blinking_paused || self.visible) } diff --git a/crates/ui/src/text_field/cursor_layout.rs b/crates/ui/src/text_field/cursor_layout.rs index 6e9cb643..16bf6e89 100644 --- a/crates/ui/src/text_field/cursor_layout.rs +++ b/crates/ui/src/text_field/cursor_layout.rs @@ -1,4 +1,6 @@ -use gpui::{outline, px, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext, WindowContext}; +use gpui::{ + fill, outline, px, size, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext, WindowContext, +}; #[derive(Debug, Clone)] pub struct CursorLayout { @@ -28,12 +30,23 @@ impl CursorLayout { } fn bounds(&self, origin: gpui::Point) -> Bounds { + // Return a bar sharp cursor Bounds { origin: self.origin + origin, - size: Size { - width: px(2.0), - height: self.line_height, - }, + size: size(px(2.0), self.line_height), + } + } + + pub fn layout(&mut self, origin: gpui::Point, cx: &mut WindowContext) { + let bounds = self.bounds(origin); + let cursor = fill(bounds, self.color); + + cx.paint_quad(cursor); + + if let Some(block_text) = &self.block_text { + block_text + .paint(self.origin + origin, self.line_height, cx) + .unwrap() } } diff --git a/crates/ui/src/text_field/mod.rs b/crates/ui/src/text_field/mod.rs index 5337a649..30033231 100644 --- a/crates/ui/src/text_field/mod.rs +++ b/crates/ui/src/text_field/mod.rs @@ -2,15 +2,10 @@ mod blink_manager; mod cursor_layout; mod text_view; -use crate::{ - disableable::Disableable, - theme::{Colorize as _, Theme}, -}; -use blink_manager::BlinkManager; +use crate::theme::Theme; use gpui::{ - div, prelude::FluentBuilder as _, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, - InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, ParentElement, RenderOnce, - Styled, View, ViewContext, WindowContext, + div, prelude::FluentBuilder as _, ClipboardItem, EventEmitter, FocusHandle, InteractiveElement, + IntoElement, KeyDownEvent, MouseButton, ParentElement, RenderOnce, Styled, View, WindowContext, }; use std::time::Duration; use text_view::TextView; @@ -20,7 +15,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); #[derive(Clone, IntoElement)] pub struct TextField { focus_handle: FocusHandle, - blink_manager: Model, pub view: View, } @@ -29,13 +23,7 @@ impl TextField { let focus_handle = cx.focus_handle(); let view = TextView::init(cx, &focus_handle); - let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - - Self { - focus_handle, - view, - blink_manager, - } + Self { focus_handle, view } } pub fn focus(&mut self, cx: &mut WindowContext) { @@ -66,22 +54,6 @@ impl TextField { .update(cx, |text_view, cx| text_view.set_masked(masked, cx)); self } - - fn handle_focus(&mut self, cx: &mut ViewContext) { - cx.emit(TextEvent::Focus); - self.blink_manager.update(cx, BlinkManager::enable); - cx.notify(); - } - - fn handle_blur(&mut self, cx: &mut ViewContext) { - 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 RenderOnce for TextField { @@ -105,10 +77,6 @@ impl RenderOnce for TextField { }) .when(!disabled, |this| { this.on_key_down(move |ev, cx| { - if !focused { - return; - } - self.view.update(cx, |text_view, cx| { let prev = text_view.text.clone(); cx.emit(TextEvent::KeyDown(ev.clone())); diff --git a/crates/ui/src/text_field/text_view.rs b/crates/ui/src/text_field/text_view.rs index 5c5787ea..34dd3ce3 100644 --- a/crates/ui/src/text_field/text_view.rs +++ b/crates/ui/src/text_field/text_view.rs @@ -4,20 +4,27 @@ use super::{ blink_manager::BlinkManager, cursor_layout::CursorLayout, TextEvent, CURSOR_BLINK_INTERVAL, }; use crate::theme::{Colorize as _, Theme}; +use catppuccin::Hsl; use gpui::{ - px, relative, Context, Element, EventEmitter, FocusHandle, HighlightStyle, InteractiveText, - IntoElement, Model, Render, Style, StyledText, TextStyle, View, ViewContext, VisualContext, - WindowContext, + px, relative, ContentMask, Context, Element, EventEmitter, FocusHandle, HighlightStyle, Hsla, + InteractiveText, IntoElement, Model, Point, Render, Style, StyledText, TextStyle, + TextStyleRefinement, View, ViewContext, VisualContext, WindowContext, }; +#[derive(Clone)] +pub struct TextFieldStyle { + pub background: Hsla, + pub text: TextStyle, +} + pub struct TextView { pub text: String, + pub style: TextFieldStyle, pub placeholder: String, pub word_click: (usize, u16), pub selection: Range, pub disabled: bool, pub blink_manager: Model, - pub cursor: CursorLayout, pub masked: bool, pub focused: bool, } @@ -30,21 +37,23 @@ impl TextView { let theme = cx.global::(); - 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 line_height = px(20.0); + let style = TextFieldStyle { + background: theme.transparent, + text: TextStyle { + color: theme.foreground, + line_height: line_height.into(), + ..Default::default() + }, + }; let m = Self { text: String::new(), + style, placeholder: "".to_string(), word_click: (0, 0), selection: 0..0, - blink_manager, - cursor, + blink_manager: blink_manager.clone(), disabled: false, masked: false, focused: false, @@ -60,6 +69,22 @@ impl TextView { view.focus(cx); }) .detach(); + + cx.observe(&blink_manager, |_, _, cx| cx.notify()).detach(); + + cx.observe_window_activation(|view, cx| { + let active = cx.is_window_active(); + view.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.show_cursor(cx); + blink_manager.disable(cx); + } + }) + }) + .detach(); + m }); @@ -134,6 +159,7 @@ impl TextView { words } + /// Converts a character range to a text range (in bytes) pub fn char_range_to_text_range(&self, text: &str) -> Range { let start = text .chars() @@ -173,10 +199,23 @@ impl TextView { } fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) { - let mut cursor = self.cursor.clone(); - dbg!("--------- paint_cursors", &cursor); + let mut cursor = layout.visible_cursor.clone(); cursor.paint(layout.content_origin, cx); } + + pub fn show_cursor(&self, cx: &mut WindowContext) -> bool { + self.blink_manager.read(cx).visible() && self.focused + } + + fn layout_visible_cursors(&self, cx: &mut WindowContext) -> CursorLayout { + let theme = cx.global::(); + let selection = &self.selection; + + let x = px(selection.end as f32); + let y = px(0.); + + CursorLayout::new(Point::new(x, y), px(0.), px(20.0), theme.ring, None) + } } impl IntoElement for TextView { @@ -189,6 +228,7 @@ impl IntoElement for TextView { pub struct TextLayout { content_origin: gpui::Point, + visible_cursor: CursorLayout, } impl Element for TextView { @@ -202,37 +242,62 @@ impl Element for TextView { fn request_layout( &mut self, - id: Option<&gpui::GlobalElementId>, + _id: Option<&gpui::GlobalElementId>, cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); + let rem_size = cx.rem_size(); + style.size.width = relative(1.).into(); - style.size.height = relative(24.).into(); - dbg!("--------- request_layout", &style); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + (cx.request_layout(style, None), ()) } fn prepaint( &mut self, - id: Option<&gpui::GlobalElementId>, + _id: Option<&gpui::GlobalElementId>, bounds: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, + _request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { - TextLayout { - content_origin: bounds.origin, - } + let text_style = TextStyleRefinement { + font_size: Some(self.style.text.font_size), + line_height: Some(self.style.text.line_height), + ..Default::default() + }; + + cx.with_text_style(Some(text_style), |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let cursor = self.layout_visible_cursors(cx); + + TextLayout { + content_origin: bounds.origin, + visible_cursor: cursor, + } + }) + }) } fn paint( &mut self, - id: Option<&gpui::GlobalElementId>, + _id: Option<&gpui::GlobalElementId>, bounds: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, + _request_layout: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, cx: &mut WindowContext, ) { - self.paint_cursors(layout, cx); + let text_style = TextStyleRefinement { + font_size: Some(self.style.text.font_size), + line_height: Some(self.style.text.line_height), + ..Default::default() + }; + + cx.with_text_style(Some(text_style), |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + self.paint_cursors(layout, cx); + }) + }); } } @@ -243,32 +308,27 @@ impl Render for TextView { let view = cx.view().clone(); let mut text = self.text.clone(); - let mut style = TextStyle { - color: theme.foreground, - ..Default::default() - }; - + let mut style = self.style.text.clone(); 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); + selection_style.background_color = Some(theme.ring); + selection_style.color = Some(theme.ring.invert()); 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![]; } if !self.focused { highlights = vec![]; } - let styled_text = StyledText::new(text + " ").with_highlights(&style, highlights); + let styled_text = StyledText::new(text).with_highlights(&style, highlights); InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| { view.update(cx, |text_view, cx| { diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 5577d74d..ffaace7a 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -13,6 +13,7 @@ fn hsl(h: f32, s: f32, l: f32) -> Hsla { pub trait Colorize { fn opacity(&self, opacity: f32) -> Hsla; fn divide(&self, divisor: f32) -> Hsla; + fn invert(&self) -> Hsla; } impl Colorize for Hsla { @@ -35,6 +36,16 @@ impl Colorize for Hsla { ..*self } } + + /// Return inverted color + fn invert(&self) -> Hsla { + Hsla { + h: (self.h + 1.8) % 3.6, + s: 1.0 - self.s, + l: 1.0 - self.l, + a: self.a, + } + } } // @layer base {