diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index b6c78c1e..d790f429 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -138,6 +138,7 @@ impl RenderOnce for Button { }) .when(!self.disabled, |this| { this.hover(|this| this.border_color(theme.blue)) + .active(|this| this.bg(theme.blue.lighten(10.0))) }) .when_some( self.on_click.filter(|_| !self.disabled), diff --git a/crates/ui/src/story/input_story.rs b/crates/ui/src/story/input_story.rs index 9083bf47..cac1a6fd 100644 --- a/crates/ui/src/story/input_story.rs +++ b/crates/ui/src/story/input_story.rs @@ -1,15 +1,30 @@ use gpui::{ div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext, - VisualContext as _, WindowContext, + WindowContext, }; use crate::text_field::TextField; use super::story_case; -pub struct InputStory; +pub struct InputStory { + input1: TextField, + input2: TextField, +} impl InputStory { + pub(crate) fn new(cx: &mut ViewContext) -> Self { + let input1 = TextField::new("Enter text here...", false, cx); + input1 + .view + .update(cx, |text_view, cx| text_view.set_text("Hello 世界", cx)); + + Self { + input1, + input2: TextField::new("Enter text here...", true, cx), + } + } + #[allow(unused)] fn on_change(ev: &ClickEvent, cx: &mut WindowContext) { println!("Input changed: {:?}", ev); @@ -18,14 +33,17 @@ impl InputStory { impl Render for InputStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let input1 = self.input1.clone(); + let input2 = self.input2.clone(); + story_case("Input", "A text input field.").child( div() .flex() .flex_col() .justify_start() .gap_3() - .child(cx.new_view(|cx| TextField::new("Enter text here...", false, cx))) - .child(cx.new_view(|cx| TextField::new("Enter text here...", false, cx))), + .child(input1) + .child(input2), ) } } diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs index a1fb2555..9573e5f1 100644 --- a/crates/ui/src/story/mod.rs +++ b/crates/ui/src/story/mod.rs @@ -2,8 +2,8 @@ use core::fmt; use std::fmt::{Display, Formatter}; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, ElementId, IntoElement, ParentElement, - Render, RenderOnce, SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext, + div, prelude::FluentBuilder as _, px, AnyElement, IntoElement, ParentElement, Render, + RenderOnce, SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext, }; mod button_story; @@ -76,22 +76,26 @@ impl Display for StoryType { pub struct Stories { active: StoryType, + + button_story: View, + input_story: View, } impl Stories { - fn new() -> Self { + fn new(cx: &mut ViewContext) -> Self { Self { active: StoryType::Button, + button_story: cx.new_view(|cx| ButtonStory {}), + input_story: cx.new_view(InputStory::new), } } pub fn view(cx: &mut WindowContext) -> View { - cx.new_view(|_cx| Self::new()) + cx.new_view(Self::new) } fn set_active(&mut self, ty: StoryType, cx: &mut ViewContext) { self.active = ty; - dbg!("--------------------- set_active: {}", ty); cx.notify(); } @@ -111,20 +115,12 @@ impl Stories { cx: &mut ViewContext, ) -> impl IntoElement { let name = format!("{}", ty); - Button::new(SharedString::from(id.to_string()), name) - .on_click(move |_e, cx| { - dbg!("--------------------- on_click: {}", ty); - // cx.update_view(self, |this| { - // this.set_active(ty, cx); - // }); - }) - .style(crate::button::ButtonStyle::Secondary) - } -} -impl Default for Stories { - fn default() -> Self { - Self::new() + Button::new(SharedString::from(id.to_string()), name) + .on_click(cx.listener(move |this, _, cx| { + this.set_active(ty, cx); + })) + .style(crate::button::ButtonStyle::Secondary) } } @@ -136,8 +132,8 @@ impl Render for Stories { .gap_4() .child(self.render_story_buttons(cx)) .map(|this| match self.active { - StoryType::Button => this.child(cx.new_view(|_cx| ButtonStory {})), - StoryType::Input => this.child(cx.new_view(|_cx| InputStory {})), + StoryType::Button => this.child(self.button_story.clone()), + StoryType::Input => this.child(self.input_story.clone()), }) } } diff --git a/crates/ui/src/text_field/mod.rs b/crates/ui/src/text_field/mod.rs index 33c09378..74afe83a 100644 --- a/crates/ui/src/text_field/mod.rs +++ b/crates/ui/src/text_field/mod.rs @@ -5,16 +5,16 @@ mod text_view; 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, + div, ClipboardItem, Context, EventEmitter, FocusHandle, InteractiveElement, IntoElement, + KeyDownEvent, Model, ParentElement, Render, RenderOnce, Styled, View, ViewContext, + WindowContext, }; -use std::{ops::Range, time::Duration}; +use std::time::Duration; use text_view::TextView; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -#[derive(Clone)] +#[derive(Clone, IntoElement)] pub struct TextField { focus_handle: FocusHandle, disable: bool, @@ -23,14 +23,14 @@ pub struct TextField { } impl TextField { - pub fn new(placeholder: &str, disable: bool, cx: &mut ViewContext) -> Self { + pub fn new(placeholder: &str, disable: bool, cx: &mut WindowContext) -> Self { let focus_handle = cx.focus_handle(); 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(); + // cx.on_focus(&focus_handle, Self::handle_focus).detach(); + // cx.on_blur(&focus_handle, Self::handle_blur).detach(); Self { focus_handle, @@ -67,42 +67,34 @@ impl Disableable for TextField { self } } - -impl Render for TextField { - fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { +impl RenderOnce for TextField { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { cx.focus(&self.focus_handle); let theme = cx.global::(); - let text_view = self.view.clone(); - let text_view1 = text_view.clone(); + let view = self.view.clone(); div() - .border_color(if self.focus_handle.is_focused(cx) { - theme.blue - } else { - theme.crust - }) - .border_1() .track_focus(&self.focus_handle) .on_key_down(move |ev, cx| { - text_view1.update(cx, |editor, cx| { - let prev = editor.text.clone(); + self.view.update(cx, |text_view, cx| { + let prev = text_view.text.clone(); cx.emit(TextEvent::KeyDown(ev.clone())); let keystroke = &ev.keystroke.key; - let chars = editor.text.chars().collect::>(); + let chars = text_view.text.chars().collect::>(); let m = ev.keystroke.modifiers.secondary(); - dbg!("---------------- {:?}", ev); + dbg!(&text_view.text, &text_view.selection); if m { match keystroke.as_str() { "a" => { - editor.selection = 0..chars.len(); + text_view.selection = 0..chars.len(); } "c" => { - // if !editor.masked { + // if !text_view.masked { let selected_text = - chars[editor.selection.clone()].iter().collect(); + chars[text_view.selection.clone()].iter().collect(); cx.write_to_clipboard(ClipboardItem::new(selected_text)); // } } @@ -110,96 +102,107 @@ impl Render for TextField { let clipboard = cx.read_from_clipboard(); if let Some(clipboard) = clipboard { let text = clipboard.text(); - editor.text.replace_range( - editor.char_range_to_text_range(&editor.text), + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), text, ); - let i = editor.selection.start + text.chars().count(); - editor.selection = i..i; + let i = text_view.selection.start + text.chars().count(); + text_view.selection = i..i; } } "x" => { let selected_text = - chars[editor.selection.clone()].iter().collect(); + chars[text_view.selection.clone()].iter().collect(); cx.write_to_clipboard(ClipboardItem::new(selected_text)); - editor.text.replace_range( - editor.char_range_to_text_range(&editor.text), + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), "", ); - editor.selection.end = editor.selection.start; + text_view.selection.end = text_view.selection.start; } _ => {} } } else if !ev.keystroke.ime_key.clone().unwrap_or_default().is_empty() { let ime_key = &ev.keystroke.ime_key.clone().unwrap_or_default(); - editor - .text - .replace_range(editor.char_range_to_text_range(&editor.text), ime_key); - let i = editor.selection.start + ime_key.chars().count(); - editor.selection = i..i; + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), + ime_key, + ); + let i = text_view.selection.start + ime_key.chars().count(); + text_view.selection = i..i; } else { match keystroke.as_str() { "left" => { - if editor.selection.start > 0 { - let i = if editor.selection.start == editor.selection.end { - editor.selection.start - 1 + if text_view.selection.start > 0 { + let i = if text_view.selection.start == text_view.selection.end + { + text_view.selection.start - 1 } else { - editor.selection.start + text_view.selection.start }; - editor.selection = i..i; + text_view.selection = i..i; } } "right" => { - if editor.selection.end < editor.text.len() { - let i = if editor.selection.start == editor.selection.end { - editor.selection.end + 1 + if text_view.selection.end < text_view.text.len() { + let i = if text_view.selection.start == text_view.selection.end + { + text_view.selection.end + 1 } else { - editor.selection.end + text_view.selection.end }; - editor.selection = i..i; + text_view.selection = i..i; } } "backspace" => { - if editor.text.is_empty() && !ev.is_held { + if text_view.text.is_empty() && !ev.is_held { // cx.emit(TextEvent::Back); - } else if editor.selection.start == editor.selection.end - && editor.selection.start > 0 + } else if text_view.selection.start == text_view.selection.end + && text_view.selection.start > 0 { - let i = (editor.selection.start - 1).min(chars.len()); - editor.text = chars[0..i].iter().collect::() - + &(chars[editor.selection.end.min(chars.len())..] + let i = (text_view.selection.start - 1).min(chars.len()); + text_view.text = chars[0..i].iter().collect::() + + &(chars[text_view.selection.end.min(chars.len())..] .iter() .collect::()); - editor.selection = i..i; + text_view.selection = i..i; } else { - editor.text.replace_range( - editor.char_range_to_text_range(&editor.text), + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), "", ); - editor.selection.end = editor.selection.start; + text_view.selection.end = text_view.selection.start; } } "enter" => { if ev.keystroke.modifiers.shift { - editor.text.insert( - editor.char_range_to_text_range(&editor.text).start, + text_view.text.insert( + text_view.char_range_to_text_range(&text_view.text).start, '\n', ); - let i = editor.selection.start + 1; - editor.selection = i..i; + let i = text_view.selection.start + 1; + text_view.selection = i..i; } } _ => {} }; } - if prev != editor.text { + + if prev != text_view.text { cx.emit(TextEvent::Input { - text: editor.text.clone(), + text: text_view.text.clone(), }); + dbg!(&text_view.text, &text_view.selection); } cx.notify(); }); }) + .border_color(if self.focus_handle.is_focused(cx) { + theme.blue + } else { + theme.crust + }) + .border_1() .rounded_sm() .py_1p5() .px_3() @@ -209,7 +212,7 @@ impl Render for TextField { } else { theme.base }) - .child(text_view) + .child(view) } } diff --git a/crates/ui/src/text_field/text_view.rs b/crates/ui/src/text_field/text_view.rs index b7c4fc07..ec700c95 100644 --- a/crates/ui/src/text_field/text_view.rs +++ b/crates/ui/src/text_field/text_view.rs @@ -1,16 +1,14 @@ -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 std::ops::Range; use super::{ blink_manager::BlinkManager, cursor_layout::CursorLayout, TextEvent, CURSOR_BLINK_INTERVAL, }; +use crate::{hls, theme::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, @@ -20,6 +18,7 @@ pub struct TextView { pub disable: bool, pub blink_manager: Model, pub cursor: CursorLayout, + pub masked: bool, } impl EventEmitter for TextView {} @@ -49,6 +48,7 @@ impl TextView { blink_manager, cursor, disable, + masked: false, }; let view = cx.new_view(|cx| { @@ -74,7 +74,7 @@ impl TextView { &view, move |subscriber, emitter: &TextEvent, cx| match emitter { TextEvent::Input { text: _ } => { - subscriber.update(cx, |editor, cx| { + subscriber.update(cx, |editor, _cx| { editor.word_click = (0, 0); }); } @@ -184,7 +184,7 @@ impl Element for TextView { 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), ()) } @@ -216,6 +216,7 @@ impl Render for TextView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let theme = cx.global::(); let mut text = self.text.clone(); + dbg!("textView render", &text); let mut style = TextStyle { color: theme.text, @@ -223,21 +224,24 @@ impl Render for TextView { ..Default::default() }; + if self.masked { + text = "•".repeat(text.len()); + } + 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 mut highlights = vec![(self.char_range_to_text_range(&text), selection_style)]; - let styled_text: StyledText = if text.is_empty() { + 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) - }; + 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| { diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3e7aa140..4aed7eee 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,7 +1,14 @@ use gpui::{prelude::FluentBuilder, *}; use std::sync::Arc; -use ui::{button::Button, disableable::Clickable as _, text_field::TextField, theme::Theme, Color}; +use ui::{ + button::Button, + disableable::Clickable as _, + story::Stories, + text_field::{self, TextField}, + theme::Theme, + Color, +}; use util::ResultExt as _; mod app_state; @@ -11,6 +18,7 @@ pub use app_state::AppState; pub struct Workspace { weak_self: WeakView, + stories: View, } impl Workspace { @@ -23,6 +31,7 @@ impl Workspace { Workspace { weak_self: weak_handle.clone(), + stories: Stories::view(cx), } } @@ -82,8 +91,6 @@ pub fn open_new( }) } -impl Workspace {} - impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let theme = cx.global::(); @@ -113,20 +120,6 @@ impl Render for Workspace { .on_click(|_e, cx| Theme::change(ui::theme::ThemeMode::Dark, cx)), ), ) - .child( - div() - .flex() - .py_3() - .gap_2() - .child(ui::story::Stories::view(cx)), - ) - .child({ - cx.new_view(|cx| { - let txt = TextField::new("Enter text here...", false, cx); - txt.view - .update(cx, |this, cx| this.set_text("This is default text.", cx)); - txt - }) - }) + .child(div().flex().py_3().gap_2().child(self.stories.clone())) } }