Add input
This commit is contained in:
parent
537edcae99
commit
b503d5f7e3
13 changed files with 956 additions and 167 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4228,6 +4228,7 @@ name = "ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,4 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
log.wokrspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, rems, rgb, ClickEvent, DefiniteLength, Div, ElementId, Hsla,
|
div, prelude::FluentBuilder as _, ClickEvent, DefiniteLength, Div, ElementId, Hsla,
|
||||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled, WindowContext,
|
StatefulInteractiveElement as _, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -8,7 +8,6 @@ use crate::{
|
||||||
colors::Color,
|
colors::Color,
|
||||||
disableable::{Clickable, Disableable, Selectable},
|
disableable::{Clickable, Disableable, Selectable},
|
||||||
label::Label,
|
label::Label,
|
||||||
preview::Preview,
|
|
||||||
HlsaExt as _,
|
HlsaExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -123,8 +122,8 @@ impl RenderOnce for Button {
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
|
.group("")
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -156,6 +155,17 @@ impl RenderOnce for Button {
|
||||||
this.bg(active_style.bg).border_color(active_style.border)
|
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| {
|
.when(self.disabled, |this| {
|
||||||
let disabled_style = style.disabled(cx);
|
let disabled_style = style.disabled(cx);
|
||||||
this.cursor_not_allowed()
|
this.cursor_not_allowed()
|
||||||
|
|
@ -231,104 +241,3 @@ impl ButtonStyle {
|
||||||
ButtonStyles { bg, border, fg }
|
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ impl HlsaExt for Hsla {
|
||||||
// --border: 240 3.7% 15.9%;
|
// --border: 240 3.7% 15.9%;
|
||||||
// --input: 240 3.7% 15.9%;
|
// --input: 240 3.7% 15.9%;
|
||||||
// --ring: 240 4.9% 83.9%;
|
// --ring: 240 4.9% 83.9%;
|
||||||
|
// --selection: 240 21.6% 50.6%;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub enum Color {
|
pub enum Color {
|
||||||
|
|
@ -70,6 +71,7 @@ pub enum Color {
|
||||||
Border,
|
Border,
|
||||||
Input,
|
Input,
|
||||||
Ring,
|
Ring,
|
||||||
|
Selection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
|
|
@ -94,6 +96,7 @@ impl Color {
|
||||||
Color::Border => hls(240., 3.7, 15.9),
|
Color::Border => hls(240., 3.7, 15.9),
|
||||||
Color::Input => hls(240., 3.7, 15.9),
|
Color::Input => hls(240., 3.7, 15.9),
|
||||||
Color::Ring => hls(240., 4.9, 83.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
104
crates/ui/src/cursor.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
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,
|
||||||
|
// ) {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
pub mod button;
|
pub mod button;
|
||||||
mod colors;
|
mod colors;
|
||||||
|
pub mod cursor;
|
||||||
pub mod disableable;
|
pub mod disableable;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod preview;
|
pub mod story;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
98
crates/ui/src/story/button_story.rs
Normal file
98
crates/ui/src/story/button_story.rs
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/ui/src/story/input_story.rs
Normal file
43
crates/ui/src/story/input_story.rs
Normal 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
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
crates/ui/src/story/mod.rs
Normal file
10
crates/ui/src/story/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
127
crates/ui/src/story/story.rs
Normal file
127
crates/ui/src/story/story.rs
Normal 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 {})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ use std::sync::Arc;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonStyle},
|
button::{Button, ButtonStyle},
|
||||||
disableable::Clickable as _,
|
disableable::Clickable as _,
|
||||||
preview::PreviewBox,
|
|
||||||
Color,
|
Color,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
@ -98,8 +97,6 @@ impl Workspace {
|
||||||
|
|
||||||
impl Render for Workspace {
|
impl Render for Workspace {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let preview_box = PreviewBox::new();
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.flex()
|
.flex()
|
||||||
|
|
@ -108,6 +105,12 @@ impl Render for Workspace {
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_4()
|
.p_4()
|
||||||
.bg(Color::Background.color(cx))
|
.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())),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue