From b503d5f7e3d00de4c06bb6574565dea1ef4e89da Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 14 Jun 2024 19:47:03 +0800 Subject: [PATCH] Add input --- Cargo.lock | 1 + crates/ui/Cargo.toml | 1 + crates/ui/src/button.rs | 119 +----- crates/ui/src/colors.rs | 3 + crates/ui/src/cursor.rs | 104 ++++++ crates/ui/src/input.rs | 548 +++++++++++++++++++++++++++- crates/ui/src/lib.rs | 3 +- crates/ui/src/preview.rs | 55 --- crates/ui/src/story/button_story.rs | 98 +++++ crates/ui/src/story/input_story.rs | 43 +++ crates/ui/src/story/mod.rs | 10 + crates/ui/src/story/story.rs | 127 +++++++ crates/workspace/src/lib.rs | 11 +- 13 files changed, 956 insertions(+), 167 deletions(-) create mode 100644 crates/ui/src/cursor.rs delete mode 100644 crates/ui/src/preview.rs create mode 100644 crates/ui/src/story/button_story.rs create mode 100644 crates/ui/src/story/input_story.rs create mode 100644 crates/ui/src/story/mod.rs create mode 100644 crates/ui/src/story/story.rs diff --git a/Cargo.lock b/Cargo.lock index d390d92a..62aeea0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4228,6 +4228,7 @@ name = "ui" version = "0.1.0" dependencies = [ "gpui", + "log", ] [[package]] diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 389b13bb..9173de83 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] gpui.workspace = true +log.wokrspace = true diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 6fe4bf32..73f626a7 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,6 +1,6 @@ use gpui::{ - div, prelude::FluentBuilder as _, rems, rgb, ClickEvent, DefiniteLength, Div, ElementId, Hsla, - InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + div, prelude::FluentBuilder as _, ClickEvent, DefiniteLength, Div, ElementId, Hsla, + InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, }; @@ -8,7 +8,6 @@ use crate::{ colors::Color, disableable::{Clickable, Disableable, Selectable}, label::Label, - preview::Preview, HlsaExt as _, }; @@ -123,8 +122,8 @@ impl RenderOnce for Button { self.base .id(self.id) + .group("") .flex() - .flex_row() .items_center() .justify_center() .child( @@ -156,6 +155,17 @@ impl RenderOnce for Button { this.bg(active_style.bg).border_color(active_style.border) }) }) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| { + this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default()) + .on_click(move |event, cx| { + dbg!("---------- button click"); + cx.stop_propagation(); + (on_click)(event, cx) + }) + }, + ) .when(self.disabled, |this| { let disabled_style = style.disabled(cx); this.cursor_not_allowed() @@ -231,104 +241,3 @@ impl ButtonStyle { ButtonStyles { bg, border, fg } } } - -#[derive(IntoElement)] -pub struct ButtonPreview {} - -impl ButtonPreview { - fn on_click(ev: &ClickEvent, cx: &mut WindowContext) { - println!("Button clicked! {:?}", ev); - } -} - -impl Preview for ButtonPreview { - fn new() -> Self { - Self {} - } - - fn name(&self) -> &'static str { - "Button" - } - - fn description(&self) -> &'static str { - "Displays a button or a component that looks like a button." - } -} - -impl RenderOnce for ButtonPreview { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - div() - .flex() - .flex_col() - .justify_start() - .gap_3() - .child( - Button::new("button-1", "Primary Button") - .style(ButtonStyle::Primary) - .on_click(Self::on_click) - .render(cx), - ) - .child( - Button::new("button-2", "Secondary Button") - .style(ButtonStyle::Secondary) - .on_click(Self::on_click) - .render(cx), - ) - .child( - Button::new("button-4", "Danger Button") - .style(ButtonStyle::Danger) - .on_click(Self::on_click), - ) - .child( - div() - .flex() - .items_center() - .gap_3() - .child( - Button::new("button-disabled1", "Disabled Button") - .style(ButtonStyle::Primary) - .on_click(Self::on_click) - .disabled(true), - ) - .child( - Button::new("button-disabled1", "Disabled Button") - .style(ButtonStyle::Secondary) - .on_click(Self::on_click) - .disabled(true), - ) - .child( - Button::new("button-disabled1", "Disabled Button") - .style(ButtonStyle::Danger) - .on_click(Self::on_click) - .disabled(true), - ), - ) - .child( - div() - .flex() - .items_center() - .gap_3() - .child( - Button::new("button-6", "Primary Button") - .style(ButtonStyle::Primary) - .size(ButtonSize::Small) - .on_click(Self::on_click) - .render(cx), - ) - .child( - Button::new("button-7", "Secondary Button") - .style(ButtonStyle::Secondary) - .size(ButtonSize::Small) - .on_click(Self::on_click) - .render(cx), - ) - .child( - Button::new("button-8", "Danger Button") - .style(ButtonStyle::Danger) - .size(ButtonSize::Small) - .on_click(Self::on_click) - .render(cx), - ), - ) - } -} diff --git a/crates/ui/src/colors.rs b/crates/ui/src/colors.rs index d33ab909..1ab506cb 100644 --- a/crates/ui/src/colors.rs +++ b/crates/ui/src/colors.rs @@ -48,6 +48,7 @@ impl HlsaExt for Hsla { // --border: 240 3.7% 15.9%; // --input: 240 3.7% 15.9%; // --ring: 240 4.9% 83.9%; +// --selection: 240 21.6% 50.6%; // } pub enum Color { @@ -70,6 +71,7 @@ pub enum Color { Border, Input, Ring, + Selection, } impl Color { @@ -94,6 +96,7 @@ impl Color { Color::Border => hls(240., 3.7, 15.9), Color::Input => hls(240., 3.7, 15.9), Color::Ring => hls(240., 4.9, 83.9), + Color::Selection => hls(213., 21.6, 50.6), } } } diff --git a/crates/ui/src/cursor.rs b/crates/ui/src/cursor.rs new file mode 100644 index 00000000..33a72d76 --- /dev/null +++ b/crates/ui/src/cursor.rs @@ -0,0 +1,104 @@ +#![allow(unused)] +use gpui::{ + div, fill, outline, point, px, size, AnyElement, AvailableSpace, Bounds, Hsla, IntoElement, + ParentElement, Pixels, ShapedLine, SharedString, Styled, WindowContext, +}; + +/// The shape of a selection cursor. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub enum CursorShape { + /// A vertical bar + #[default] + Bar, + /// A block that surrounds the following character + Block, + /// An underline that runs along the following character + Underscore, + /// A box drawn around the following character + Hollow, +} + +pub struct CursorLayout { + origin: gpui::Point, + block_width: Pixels, + line_height: Pixels, + color: Hsla, + shape: CursorShape, +} + +#[derive(Debug)] +pub struct CursorName { + string: SharedString, + color: Hsla, + is_top_row: bool, +} + +impl CursorLayout { + pub fn new( + origin: gpui::Point, + block_width: Pixels, + line_height: Pixels, + color: Hsla, + shape: CursorShape, + block_text: Option, + ) -> CursorLayout { + CursorLayout { + origin, + block_width, + line_height, + color, + shape, + } + } + + pub fn bounding_rect(&self, origin: gpui::Point) -> Bounds { + Bounds { + origin: self.origin + origin, + size: size(self.block_width, self.line_height), + } + } + + fn bounds(&self, origin: gpui::Point) -> Bounds { + match self.shape { + CursorShape::Bar => Bounds { + origin: self.origin + origin, + size: size(px(2.0), self.line_height), + }, + CursorShape::Block | CursorShape::Hollow => Bounds { + origin: self.origin + origin, + size: size(self.block_width, self.line_height), + }, + CursorShape::Underscore => Bounds { + origin: self.origin + + origin + + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)), + size: size(self.block_width, px(2.0)), + }, + } + } + + pub fn layout( + &mut self, + origin: gpui::Point, + cursor_name: Option, + cx: &mut WindowContext, + ) { + } + + pub fn paint(&mut self, origin: gpui::Point, cx: &mut WindowContext) { + let bounds = self.bounds(origin); + + //Draw background or border quad + let cursor = if matches!(self.shape, CursorShape::Hollow) { + outline(bounds, self.color) + } else { + fill(bounds, self.color) + }; + + cx.paint_quad(cursor); + } + + pub fn shape(&self) -> CursorShape { + self.shape + } +} diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index ec1a526c..2edf25df 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -1,5 +1,549 @@ -use gpui::SharedString; +use std::{f32::INFINITY, ops::Range}; + +use gpui::{ + div, px, AppContext, ClipboardItem, Div, Element, ElementId, EventEmitter, FocusHandle, + FocusableElement as _, FocusableView, HighlightStyle, Hsla, InteractiveElement as _, + InteractiveText, IntoElement, KeyDownEvent, MouseDownEvent, ParentElement as _, Point, Render, + RenderOnce, Style, Styled, StyledText, TextStyle, View, ViewContext, VisualContext, + WindowContext, +}; +use log::debug; + +use crate::{ + cursor::{CursorLayout, CursorShape}, + Color, +}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum InputMode { + SingleLine, + AutoHeight { max_lines: usize }, + Full, +} + +#[derive(Clone, Debug)] +pub enum SoftWrap { + None, + PreferLine, + EditorWidth, + Column(u32), +} + +pub struct TextView { + id: ElementId, + pub text: String, + pub selection: Range, + pub word_click: (usize, u16), + pub placeholder: String, + masked: bool, +} + +impl EventEmitter for TextView {} + +impl TextView { + pub fn init( + id: impl Into, + cx: &mut WindowContext, + focus_handle: &FocusHandle, + ) -> View { + let view = Self { + id: id.into(), + text: String::new(), + selection: 0..0, + word_click: (0, 0), + placeholder: String::new(), + masked: false, + }; + + let view = cx.new_view(|cx| { + #[cfg(debug_assertions)] + cx.on_release(|_, _, _| debug!("Text Input released")) + .detach(); + + cx.on_blur(focus_handle, |_: &mut TextView, cx| { + cx.emit(InputEvent::Blur); + }) + .detach(); + + cx.on_focus(focus_handle, |view, cx| { + view.select_all(cx); + }) + .detach(); + + view + }); + cx.subscribe(&view, |subscriber, emitter: &InputEvent, cx| { + if let InputEvent::Input { text: _ } = emitter { + subscriber.update(cx, |editor, _cx| { + editor.word_click = (0, 0); + }); + } + }) + .detach(); + + view + } + + pub fn select_all(&mut self, cx: &mut ViewContext) { + let len = self.text.chars().count(); + self.selection = 0..len; + cx.notify() + } + + pub fn word_ranges(&self) -> Vec> { + 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 reset(&mut self, cx: &mut ViewContext) { + self.text.clear(); + self.selection = 0..0; + cx.notify(); + cx.emit(InputEvent::Input { + text: self.text.clone(), + }); + } + + pub fn set_placeholder(&mut self, placeholder: String, cx: &mut ViewContext) { + self.placeholder = placeholder.to_string(); + cx.notify(); + } + + pub fn set_text(&mut self, text: String, cx: &mut ViewContext) { + self.text = text; + let len = self.text.chars().count(); + self.selection = len..len; + cx.notify(); + cx.emit(InputEvent::Input { + text: self.text.clone(), + }); + } + + pub fn char_range_to_text_range(&self, text: &str) -> Range { + let start = text + .chars() + .take(self.selection.start) + .collect::() + .len(); + let end = text + .chars() + .take(self.selection.end) + .collect::() + .len(); + start..end + } + + fn cursor_layout(&self, color: Hsla) -> CursorLayout { + CursorLayout::new( + Point { + x: px(0.), + y: px(0.), + }, + px(0.0), + px(20.0), + color, + CursorShape::Bar, + None, + ) + } +} + +impl Render for TextView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let selection_style = HighlightStyle { + fade_out: Some(0.6), + background_color: Some(Color::Selection.color(cx)), + ..Default::default() + }; + let mut highlights = vec![(self.char_range_to_text_range(&self.text), selection_style)]; + + let (display_text, _is_empty, style) = match self.text.is_empty() { + true => { + let style = TextStyle { + color: Color::Input.color(cx), + ..TextStyle::default() + }; + + highlights = vec![]; + + (self.placeholder.clone(), true, style) + } + false => { + let style = TextStyle { + color: Color::Foreground.color(cx), + ..TextStyle::default() + }; + + (self.text.clone(), false, style) + } + }; + + let styled_text = StyledText::new(display_text + " ").with_highlights(&style, highlights); + let view = cx.view().clone(); + + InteractiveText::new(self.id.clone(), styled_text).on_click( + self.word_ranges(), + move |ev, cx| { + view.update(cx, |editor, cx| { + let (index, mut count) = editor.word_click; + if index == ev { + count += 1; + } else { + count = 1; + } + match count { + 2 => { + let word_ranges = editor.word_ranges(); + editor.selection = word_ranges.get(ev).unwrap().clone(); + } + 3 => { + // Should select the line + let line_start = editor.text[..ev].rfind('\n').map_or(0, |i| i + 1); + let line_end = editor.text[ev..] + .find('\n') + .map_or(editor.text.len(), |i| ev + i); + editor.selection = line_start..line_end; + } + 4 => { + count = 0; + editor.selection = 0..editor.text.len(); + } + _ => { + editor.selection = 0..0; + } + } + editor.word_click = (ev, count); + cx.notify(); + }); + }, + ) + } +} pub struct Input { - placeholder: SharedString, + id: ElementId, + base: Div, + focus_handle: FocusHandle, + mode: InputMode, + wrap: SoftWrap, + view: View, } + +impl Styled for Input { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() + } +} + +impl Input { + pub fn new(id: impl Into, cx: &mut WindowContext) -> Self { + let focus_handle = cx.focus_handle(); + let id = id.into(); + let input = Self { + id: id.clone(), + base: div(), + view: TextView::init(id, cx, &focus_handle), + focus_handle, + wrap: SoftWrap::None, + mode: InputMode::SingleLine, + }; + + input + } + + pub fn wrap(mut self, wrap: SoftWrap) -> Self { + self.wrap = wrap; + self + } + + pub fn mode(mut self, mode: InputMode) -> Self { + self.mode = mode; + self + } + + pub fn set_placeholder(&self, placeholder: impl ToString, cx: &mut C) { + cx.update_view(&self.view, |editor: &mut TextView, cx| { + editor.set_placeholder(placeholder.to_string(), cx); + }); + } + + pub fn set_text(&self, text: impl ToString, cx: &mut C) { + cx.update_view(&self.view, |editor: &mut TextView, cx| { + editor.set_text(text.to_string(), cx); + }); + } + + fn handle_focus(&mut self, cx: &mut ViewContext) { + cx.emit(InputEvent::Focus); + } + + fn handle_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext) { + cx.emit(InputEvent::KeyDown(ev.clone())); + let keystroke = &ev.keystroke.key; + + #[cfg(target_os = "macos")] + let modifier = ev.keystroke.modifiers.platform; + #[cfg(not(target_os = "macos"))] + let modifier = ev.keystroke.modifiers.control; + + self.view.update(cx, |editor, cx| { + let prev = editor.text.clone(); + let chars = editor.text.chars().collect::>(); + + #[cfg(not(target_os = "macos"))] + if ev.keystroke.modifiers.control { + if self.mode == InputMode::SingleLine { + match keystroke.as_str() { + "a" => { + // Move cursor to the beginning of the line + editor.selection = 0..0; + } + "e" => { + // Move cursor to the end of the line + editor.selection = chars.len()..chars.len(); + } + _ => {} + } + } + } + + if modifier { + match keystroke.as_str() { + "a" => { + editor.selection = 0..chars.len(); + } + "c" => { + if !editor.masked { + let selected_text = chars[editor.selection.clone()].iter().collect(); + cx.write_to_clipboard(ClipboardItem::new(selected_text)); + } + } + "v" => { + 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); + let i = editor.selection.start + text.chars().count(); + editor.selection = i..i; + } + } + "x" => { + let selected_text = chars[editor.selection.clone()].iter().collect(); + cx.write_to_clipboard(ClipboardItem::new(selected_text)); + editor + .text + .replace_range(editor.char_range_to_text_range(&editor.text), ""); + editor.selection.end = editor.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; + } else { + match keystroke.as_str() { + "backspace" => { + if editor.text.is_empty() && !ev.is_held { + cx.emit(InputEvent::Back); + } else if editor.selection.start == editor.selection.end + && editor.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())..] + .iter() + .collect::()); + editor.selection = i..i; + } else { + editor + .text + .replace_range(editor.char_range_to_text_range(&editor.text), ""); + editor.selection.end = editor.selection.start; + } + } + "delete" => { + if editor.text.is_empty() && !ev.is_held { + cx.emit(InputEvent::Back); + } else if editor.selection.start == editor.selection.end + && editor.selection.end < editor.text.len() + { + let i = editor.selection.start.min(chars.len()); + editor.text = chars[0..i].iter().collect::() + + &(chars[editor.selection.end.min(chars.len())..] + .iter() + .collect::()); + editor.selection = i..i; + } else { + editor + .text + .replace_range(editor.char_range_to_text_range(&editor.text), ""); + editor.selection.end = editor.selection.start; + } + } + "left" => { + if editor.selection.start > 0 { + let i = if editor.selection.start == editor.selection.end { + editor.selection.start - 1 + } else { + editor.selection.start + }; + editor.selection = i..i; + } + } + "right" => { + if editor.selection.end < editor.text.len() { + let i = if editor.selection.start == editor.selection.end { + editor.selection.end + 1 + } else { + editor.selection.end + }; + editor.selection = i..i; + } + } + "up" => {} + "down" => {} + "enter" => { + if self.mode == InputMode::SingleLine { + cx.emit(InputEvent::Blur); + } else { + editor.text.insert(editor.selection.start, '\n'); + editor.selection.start += 1; + editor.selection.end += 1; + } + } + "tab" => { + if self.mode == InputMode::SingleLine { + cx.emit(InputEvent::Blur); + } else { + editor.text.insert(editor.selection.start, '\t'); + editor.selection.start += 1; + editor.selection.end += 1; + } + } + _ => {} + } + } + + debug!( + "------------------- {} {:?}", + &editor.text, &editor.selection + ); + if prev != editor.text { + cx.emit(InputEvent::Input { + text: editor.text.clone(), + }); + } + cx.notify(); + }) + } +} + +pub enum InputEvent { + Input { text: String }, + Focus, + Blur, + Back, + KeyDown(KeyDownEvent), +} + +impl EventEmitter for Input {} + +impl FocusableView for Input { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Input { + fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { + let text_view = self.view.clone(); + + div() + .track_focus(&self.focus_handle) + .on_key_down(cx.listener(Self::handle_key_down)) + .on_any_mouse_down(cx.listener(|this, _, cx| { + debug!("on_any_mouse_down"); + cx.focus(&this.focus_handle); + })) + .cursor_text() + .border_1() + .border_color(Color::Input.color(cx)) + .bg(Color::Background.color(cx)) + .focus(|style| style.border_color(Color::Ring.color(cx))) + .py_1() + .px_3() + .rounded_md() + .child(text_view) + } +} + +pub struct InputLayout {} + +// impl Element for Input { +// type RequestLayoutState = (); + +// type PrepaintState = InputLayout; + +// fn id(&self) -> Option { +// None +// } + +// fn request_layout( +// &mut self, +// id: Option<&gpui::GlobalElementId>, +// cx: &mut WindowContext, +// ) -> (gpui::LayoutId, Self::RequestLayoutState) { +// let style = Style::default(); +// let layout_id = cx.request_layout(style, None); +// (layout_id, ()) +// } + +// fn prepaint( +// &mut self, +// id: Option<&gpui::GlobalElementId>, +// bounds: gpui::Bounds, +// request_layout: &mut Self::RequestLayoutState, +// cx: &mut WindowContext, +// ) -> Self::PrepaintState { +// InputLayout {} +// } + +// fn paint( +// &mut self, +// id: Option<&gpui::GlobalElementId>, +// bounds: gpui::Bounds, +// request_layout: &mut Self::RequestLayoutState, +// prepaint: &mut Self::PrepaintState, +// cx: &mut WindowContext, +// ) { +// } +// } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 6e1b1c28..47a93071 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,8 +1,9 @@ pub mod button; mod colors; +pub mod cursor; pub mod disableable; pub mod input; pub mod label; -pub mod preview; +pub mod story; pub use colors::*; diff --git a/crates/ui/src/preview.rs b/crates/ui/src/preview.rs deleted file mode 100644 index efefa5f3..00000000 --- a/crates/ui/src/preview.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{div, px, IntoElement, ParentElement as _, RenderOnce, Styled as _}; - -use crate::{button::ButtonPreview, label::Label}; - -pub trait Preview { - fn name(&self) -> &'static str; - fn description(&self) -> &'static str; - fn new() -> Self; -} - -enum PreviewStory { - Button, -} - -#[derive(IntoElement)] -pub struct PreviewBox { - active_preview: PreviewStory, -} - -impl PreviewBox { - pub fn new() -> Self { - Self { - active_preview: PreviewStory::Button, - } - } - - pub fn active_preview(&self) -> impl Preview + IntoElement { - match self.active_preview { - PreviewStory::Button => ButtonPreview::new(), - } - } -} - -impl RenderOnce for PreviewBox { - fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - let component_preview = self.active_preview(); - - let heading = Label::new(component_preview.name()).text_size(px(24.0)); - let description = Label::new(component_preview.description()).multiple_lines(); - - div() - .flex() - .flex_col() - .gap_4() - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(heading) - .child(description), - ) - .child(component_preview) - } -} diff --git a/crates/ui/src/story/button_story.rs b/crates/ui/src/story/button_story.rs new file mode 100644 index 00000000..9dcde8d5 --- /dev/null +++ b/crates/ui/src/story/button_story.rs @@ -0,0 +1,98 @@ +use gpui::{ + div, ClickEvent, IntoElement, ParentElement as _, Render, RenderOnce, Styled as _, ViewContext, + WindowContext, +}; + +use crate::{ + button::{Button, ButtonSize, ButtonStyle}, + disableable::{Clickable as _, Disableable as _}, +}; + +use super::story_case; + +pub struct ButtonStory; + +impl ButtonStory { + fn on_click(ev: &ClickEvent, cx: &mut WindowContext) { + println!("Button clicked! {:?}", ev); + } +} + +impl Render for ButtonStory { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + story_case( + "Button", + "Displays a button or a component that looks like a button.", + ) + .child( + div() + .flex() + .flex_col() + .justify_start() + .gap_3() + .child( + Button::new("button-1", "Primary Button") + .style(ButtonStyle::Primary) + .on_click(Self::on_click), + ) + .child( + Button::new("button-2", "Secondary Button") + .style(ButtonStyle::Secondary) + .on_click(Self::on_click), + ) + .child( + Button::new("button-4", "Danger Button") + .style(ButtonStyle::Danger) + .on_click(Self::on_click), + ) + .child( + div() + .flex() + .items_center() + .gap_3() + .child( + Button::new("button-disabled1", "Disabled Button") + .style(ButtonStyle::Primary) + .on_click(Self::on_click) + .disabled(true), + ) + .child( + Button::new("button-disabled1", "Disabled Button") + .style(ButtonStyle::Secondary) + .on_click(Self::on_click) + .disabled(true), + ) + .child( + Button::new("button-disabled1", "Disabled Button") + .style(ButtonStyle::Danger) + .on_click(Self::on_click) + .disabled(true), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_3() + .child( + Button::new("button-6", "Primary Button") + .style(ButtonStyle::Primary) + .size(ButtonSize::Small) + .on_click(Self::on_click), + ) + .child( + Button::new("button-7", "Secondary Button") + .style(ButtonStyle::Secondary) + .size(ButtonSize::Small) + .on_click(Self::on_click), + ) + .child( + Button::new("button-8", "Danger Button") + .style(ButtonStyle::Danger) + .size(ButtonSize::Small) + .on_click(Self::on_click), + ), + ), + ) + } +} diff --git a/crates/ui/src/story/input_story.rs b/crates/ui/src/story/input_story.rs new file mode 100644 index 00000000..b5c9517d --- /dev/null +++ b/crates/ui/src/story/input_story.rs @@ -0,0 +1,43 @@ +use gpui::{ + div, prelude::FluentBuilder as _, ClickEvent, IntoElement, ParentElement as _, Render, + Styled as _, ViewContext, VisualContext, WindowContext, +}; + +use crate::input::Input; + +use super::story_case; + +pub struct InputStory; + +impl InputStory { + fn on_change(ev: &ClickEvent, cx: &mut WindowContext) { + println!("Input changed: {:?}", ev); + } +} + +impl Render for InputStory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + story_case("Input", "A text input field.").child( + div() + .flex() + .flex_col() + .justify_start() + .gap_3() + .child({ + cx.new_view(|cx| { + let input = Input::new("input1", cx); + input.set_placeholder("Enter text here...", cx); + input + }) + }) + .child({ + cx.new_view(|cx| { + let input = Input::new("input1", cx); + input.set_placeholder("Enter text here...", cx); + input.set_text("Hello, world!", cx); + input + }) + }), + ) + } +} diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs new file mode 100644 index 00000000..cbcad143 --- /dev/null +++ b/crates/ui/src/story/mod.rs @@ -0,0 +1,10 @@ +mod button_story; +mod input_story; +mod story; + +pub use story::Stories; +use story::StoryContainer; + +pub fn story_case(name: &'static str, description: &'static str) -> StoryContainer { + StoryContainer::new(name, description) +} diff --git a/crates/ui/src/story/story.rs b/crates/ui/src/story/story.rs new file mode 100644 index 00000000..95a19bb0 --- /dev/null +++ b/crates/ui/src/story/story.rs @@ -0,0 +1,127 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + +use gpui::{ + div, prelude::FluentBuilder as _, px, AnyElement, ElementId, InputHandler, IntoElement, + ParentElement, Render, RenderOnce, SharedString, Styled as _, ViewContext, VisualContext, + WindowContext, +}; + +use crate::{button::Button, disableable::Clickable as _, label::Label}; + +use super::{button_story::ButtonStory, input_story::InputStory}; + +#[derive(IntoElement)] +pub struct StoryContainer { + name: SharedString, + description: SharedString, + children: Vec, +} + +impl ParentElement for StoryContainer { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl StoryContainer { + pub fn new(name: impl Into, description: impl Into) -> Self { + Self { + name: name.into(), + description: description.into(), + children: Vec::new(), + } + } +} + +impl RenderOnce for StoryContainer { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_4() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(Label::new(self.name).text_size(px(24.0))) + .child(Label::new(self.description).text_size(px(16.0))), + ) + .children(self.children) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum StoryType { + Button, + Input, +} + +impl Display for StoryType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Button => write!(f, "Button"), + Self::Input => write!(f, "Input"), + } + } +} + +pub struct Stories { + active: StoryType, +} + +impl Stories { + pub fn new() -> Self { + Self { + active: StoryType::Button, + } + } + + fn set_active(&mut self, ty: StoryType, cx: &mut ViewContext) { + self.active = ty; + dbg!("--------------------- set_active: {}", ty); + cx.notify(); + } + + fn render_story_buttons(&self, cx: &mut ViewContext) -> impl IntoElement { + div() + .flex() + .items_center() + .gap_4() + .child(self.swith_button(StoryType::Button, cx)) + .child(self.swith_button(StoryType::Input, cx)) + } + + fn swith_button(&self, ty: StoryType, cx: &mut ViewContext) -> impl IntoElement { + let name = format!("{}", ty); + Button::new(ElementId::Name(SharedString::from(name.clone())), name) + .on_click(cx.listener(move |this, _, cx| { + dbg!("--------------------- on_click: {}", ty); + this.set_active(ty, cx); + })) + .style(crate::button::ButtonStyle::Secondary) + } +} + +impl Default for Stories { + fn default() -> Self { + Self::new() + } +} + +impl Render for Stories { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let nav_buttons = self.render_story_buttons(cx); + + div() + .flex() + .flex_col() + .gap_4() + .child(nav_buttons) + .map(|this| match self.active { + StoryType::Button => this.child(cx.new_view(|cx| ButtonStory {})), + StoryType::Input => this.child(cx.new_view(|cx| InputStory {})), + }) + } +} diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 6b698c6f..6eadf576 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use ui::{ button::{Button, ButtonStyle}, disableable::Clickable as _, - preview::PreviewBox, Color, }; use util::ResultExt as _; @@ -98,8 +97,6 @@ impl Workspace { impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let preview_box = PreviewBox::new(); - div() .relative() .flex() @@ -108,6 +105,12 @@ impl Render for Workspace { .size_full() .p_4() .bg(Color::Background.color(cx)) - .child(div().flex().py_3().gap_2().child(preview_box)) + .child( + div() + .flex() + .py_3() + .gap_2() + .child(cx.new_view(|_| ui::story::Stories::new())), + ) } }