Add input

This commit is contained in:
Jason Lee 2024-06-14 19:47:03 +08:00
parent 537edcae99
commit b503d5f7e3
13 changed files with 956 additions and 167 deletions

1
Cargo.lock generated
View file

@ -4228,6 +4228,7 @@ name = "ui"
version = "0.1.0"
dependencies = [
"gpui",
"log",
]
[[package]]

View file

@ -5,3 +5,4 @@ edition = "2021"
[dependencies]
gpui.workspace = true
log.wokrspace = true

View file

@ -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),
),
)
}
}

View file

@ -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),
}
}
}

104
crates/ui/src/cursor.rs Normal file
View file

@ -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<Pixels>,
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<Pixels>,
block_width: Pixels,
line_height: Pixels,
color: Hsla,
shape: CursorShape,
block_text: Option<ShapedLine>,
) -> CursorLayout {
CursorLayout {
origin,
block_width,
line_height,
color,
shape,
}
}
pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
Bounds {
origin: self.origin + origin,
size: size(self.block_width, self.line_height),
}
}
fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
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<Pixels>,
cursor_name: Option<CursorName>,
cx: &mut WindowContext,
) {
}
pub fn paint(&mut self, origin: gpui::Point<Pixels>, 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
}
}

View file

@ -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<usize>,
pub word_click: (usize, u16),
pub placeholder: String,
masked: bool,
}
impl EventEmitter<InputEvent> for TextView {}
impl TextView {
pub fn init(
id: impl Into<ElementId>,
cx: &mut WindowContext,
focus_handle: &FocusHandle,
) -> View<Self> {
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<Self>) {
let len = self.text.chars().count();
self.selection = 0..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 reset(&mut self, cx: &mut ViewContext<Self>) {
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>) {
self.placeholder = placeholder.to_string();
cx.notify();
}
pub fn set_text(&mut self, text: String, cx: &mut ViewContext<Self>) {
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<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
}
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<Self>) -> 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<TextView>,
}
impl Styled for Input {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl Input {
pub fn new(id: impl Into<ElementId>, 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<C: VisualContext>(&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<C: VisualContext>(&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<Self>) {
cx.emit(InputEvent::Focus);
}
fn handle_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
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::<Vec<char>>();
#[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::<String>()
+ &(chars[editor.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
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::<String>()
+ &(chars[editor.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
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<InputEvent> 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<gpui::ElementId> {
// 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<gpui::Pixels>,
// request_layout: &mut Self::RequestLayoutState,
// cx: &mut WindowContext,
// ) -> Self::PrepaintState {
// InputLayout {}
// }
// fn paint(
// &mut self,
// id: Option<&gpui::GlobalElementId>,
// bounds: gpui::Bounds<gpui::Pixels>,
// request_layout: &mut Self::RequestLayoutState,
// prepaint: &mut Self::PrepaintState,
// cx: &mut WindowContext,
// ) {
// }
// }

View file

@ -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::*;

View file

@ -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)
}
}

View file

@ -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<Self>) -> 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),
),
),
)
}
}

View file

@ -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<Self>) -> 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
})
}),
)
}
}

View file

@ -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)
}

View file

@ -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<AnyElement>,
}
impl ParentElement for StoryContainer {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl StoryContainer {
pub fn new(name: impl Into<SharedString>, description: impl Into<SharedString>) -> 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>) {
self.active = ty;
dbg!("--------------------- set_active: {}", ty);
cx.notify();
}
fn render_story_buttons(&self, cx: &mut ViewContext<Self>) -> 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<Self>) -> 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<Self>) -> 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 {})),
})
}
}

View file

@ -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<Self>) -> 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())),
)
}
}