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"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
log.wokrspace = true
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
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 {
|
||||
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;
|
||||
mod colors;
|
||||
pub mod cursor;
|
||||
pub mod disableable;
|
||||
pub mod input;
|
||||
pub mod label;
|
||||
pub mod preview;
|
||||
pub mod story;
|
||||
|
||||
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::{
|
||||
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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue