Jason Lee 2024-06-27 15:36:07 +08:00 committed by GitHub
parent a90f51d165
commit 659f00c9a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1964 additions and 955 deletions

1258
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,27 +3,27 @@ use gpui::{
VisualContext, WindowContext,
};
use ui::text_field::TextField;
use ui::input::TextInput;
use super::story_case;
pub struct InputStory {
input1: View<TextField>,
input2: View<TextField>,
mash_input: View<TextField>,
disabled_input: View<TextField>,
input1: View<TextInput>,
input2: View<TextInput>,
mash_input: View<TextInput>,
disabled_input: View<TextInput>,
}
impl InputStory {
pub(crate) fn new(cx: &mut WindowContext) -> Self {
let input1 = cx.new_view(|cx| {
let mut input = TextField::new(cx);
let mut input = TextInput::new(cx);
input.set_text("Hello 世界", cx);
input
});
let mask_input = cx.new_view(|cx| {
let mut input = TextField::new(cx);
let mut input = TextInput::new(cx);
input.set_masked(true, cx);
input.set_text("this-is-password", cx);
input
@ -32,13 +32,13 @@ impl InputStory {
Self {
input1,
input2: cx.new_view(|cx| {
let mut input = TextField::new(cx);
let mut input = TextInput::new(cx);
input.set_placeholder("Enter text here...", cx);
input
}),
mash_input: mask_input,
disabled_input: cx.new_view(|cx| {
let mut input = TextField::new(cx);
let mut input = TextInput::new(cx);
input.set_text("This is disabled input", cx);
input.set_disabled(true, cx);
input

View file

@ -12,3 +12,4 @@ serde_json = "1"
wry = "0"
smallvec = "1.13.2"
windows = "0.57.0"
unicode-segmentation = "1.11.0"

28
crates/ui/src/event.rs Normal file
View file

@ -0,0 +1,28 @@
use gpui::{ClickEvent, Element, Focusable, InteractiveElement, Stateful, WindowContext};
pub trait InterativeElementExt: InteractiveElement {
/// Set the listener for a double click event.
fn on_double_click(
mut self,
listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Self
where
Self: Sized,
{
self.interactivity().on_click(move |event, context| {
if event.up.click_count == 2 {
listener(event, context);
}
});
self
}
}
impl<E: InteractiveElement> InterativeElementExt for Focusable<E> {}
// impl<E> InterativeElementExt for Stateful<E>
// where
// E: Element,
// Self: InteractiveElement,
// {
// }

602
crates/ui/src/input.rs Normal file
View file

@ -0,0 +1,602 @@
use std::ops::Range;
use crate::event::InterativeElementExt as _;
use crate::theme::ActiveTheme;
use gpui::*;
use prelude::FluentBuilder as _;
use unicode_segmentation::*;
actions!(
text_input,
[
Backspace,
Delete,
Left,
Right,
SelectLeft,
SelectRight,
SelectAll,
Home,
End,
ShowCharacterPalette,
Copy,
Cut,
Paste,
MoveToStartOfLine,
MoveToEndOfLine,
]
);
pub fn init(cx: &mut AppContext) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, None),
KeyBinding::new("delete", Delete, None),
KeyBinding::new("left", Left, None),
KeyBinding::new("right", Right, None),
KeyBinding::new("shift-left", SelectLeft, None),
KeyBinding::new("shift-right", SelectRight, None),
KeyBinding::new("cmd-a", SelectAll, None),
KeyBinding::new("home", Home, None),
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
KeyBinding::new("cmd-c", Copy, None),
KeyBinding::new("cmd-x", Cut, None),
KeyBinding::new("cmd-v", Paste, None),
KeyBinding::new("ctrl-a", MoveToStartOfLine, None),
KeyBinding::new("ctrl-e", MoveToEndOfLine, None),
]);
}
pub struct TextInput {
focus_handle: FocusHandle,
text: SharedString,
placeholder: SharedString,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Option<ShapedLine>,
disabled: bool,
masked: bool,
appearance: bool,
}
impl TextInput {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
text: "".into(),
placeholder: "".into(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: None,
disabled: false,
masked: false,
appearance: true,
}
}
/// Set the text of the input field.
pub fn set_text(&mut self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
self.text = text.into();
self.selected_range = self.text.len()..self.text.len();
cx.notify();
}
/// Set the placeholder text of the input field.
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}
/// Set the disabled state of the input field.
pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext<Self>) {
self.disabled = disabled;
cx.notify();
}
/// Set the masked state of the input field.
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
self.masked = masked;
cx.notify();
}
/// Set the appearance of the input field.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
/// Return the text of the input field.
pub fn text(&self) -> SharedString {
self.text.clone()
}
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
}
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
self.select_to(self.next_boundary(self.cursor_offset()), cx);
}
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
self.select_to(self.text.len(), cx)
}
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
}
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
self.move_to(self.text.len(), cx);
}
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", cx)
}
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.next_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", cx)
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
cx.show_character_palette();
}
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
let selected_text = self.text[self.selected_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new(selected_text));
}
fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
let selected_text = self.text[self.selected_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new(selected_text));
self.replace_text_in_range(Some(self.selected_range.clone()), "", cx);
}
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(clipboard) = cx.read_from_clipboard() {
self.replace_text_in_range(Some(self.selected_range.clone()), clipboard.text(), cx);
}
}
fn move_to_start_of_line(&mut self, _: &MoveToStartOfLine, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
}
fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
self.move_to(self.text.len(), cx);
}
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
self.selected_range = offset..offset;
cx.notify()
}
fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
if self.selection_reversed {
self.selected_range.start = offset
} else {
self.selected_range.end = offset
};
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
cx.notify()
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in self.text.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in self.text.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
self.text
.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
fn next_boundary(&self, offset: usize) -> usize {
self.text
.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.text.len())
}
}
impl ViewInputHandler for TextInput {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
_cx: &mut ViewContext<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
Some(self.text[range].to_string())
}
fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
Some(self.range_to_utf16(&self.selected_range))
}
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.range_to_utf16(range))
}
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
cx: &mut ViewContext<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.text =
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
cx: &mut ViewContext<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.text =
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
self.marked_range = Some(range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_cx: &mut ViewContext<Self>,
) -> Option<Bounds<Pixels>> {
let last_layout = self.last_layout.as_ref()?;
let range = self.range_from_utf16(&range_utf16);
Some(Bounds::from_corners(
point(
bounds.left() + last_layout.x_for_index(range.start),
bounds.top(),
),
point(
bounds.left() + last_layout.x_for_index(range.end),
bounds.bottom(),
),
))
}
}
impl FocusableView for TextInput {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
struct TextElement {
input: View<TextInput>,
}
struct PrepaintState {
line: Option<ShapedLine>,
cursor: Option<PaintQuad>,
selection: Option<PaintQuad>,
}
impl IntoElement for TextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = cx.line_height().into();
(cx.request_layout(style, []), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
let input = self.input.read(cx);
let text = input.text.clone();
let placeholder = input.placeholder.clone();
let selected_range = input.selected_range.clone();
let cursor = input.cursor_offset();
let style = cx.text_style();
let (disaplay_text, text_color) = if text.is_empty() {
(placeholder, cx.theme().muted_foreground)
} else if input.masked {
(
"*".repeat(text.chars().count()).into(),
cx.theme().foreground,
)
} else {
(text, cx.theme().foreground)
};
let run = TextRun {
len: input.text.len(),
font: style.font(),
color: text_color,
background_color: None,
underline: None,
strikethrough: None,
};
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
vec![
TextRun {
len: marked_range.start,
..run.clone()
},
TextRun {
len: marked_range.end - marked_range.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone()
},
TextRun {
len: disaplay_text.len() - marked_range.end,
..run.clone()
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect()
} else {
vec![run]
};
let font_size = style.font_size.to_pixels(cx.rem_size());
let line = cx
.text_system()
.shape_line(disaplay_text, font_size, &runs)
.unwrap();
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {
(
None,
Some(fill(
Bounds::new(
point(bounds.left() + cursor_pos, bounds.top()),
size(px(1.5), bounds.bottom() - bounds.top()),
),
gpui::blue(),
)),
)
} else {
(
Some(fill(
Bounds::from_corners(
point(
bounds.left() + line.x_for_index(selected_range.start),
bounds.top(),
),
point(
bounds.left() + line.x_for_index(selected_range.end),
bounds.bottom(),
),
),
cx.theme().selection,
)),
None,
)
};
PrepaintState {
line: Some(line),
cursor,
selection,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
let focus_handle = self.input.read(cx).focus_handle.clone();
let is_focused = focus_handle.is_focused(cx);
cx.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.input.clone()),
);
if let Some(selection) = prepaint.selection.take() {
cx.paint_quad(selection)
}
let line = prepaint.line.take().unwrap();
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
if is_focused {
if let Some(cursor) = prepaint.cursor.take() {
cx.paint_quad(cursor);
}
}
self.input.update(cx, |input, _cx| {
input.last_layout = Some(line);
});
}
}
impl Render for TextInput {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focused = self.focus_handle.is_focused(cx);
div()
.flex()
.key_context("TextInput")
.track_focus(&self.focus_handle)
.when(!self.disabled, |this| {
this.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
})
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::copy))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::cut))
.on_action(cx.listener(Self::move_to_start_of_line))
.on_action(cx.listener(Self::move_to_end_of_line))
// Double click to select all
.on_double_click(cx.listener(|view, _, cx| {
view.select_all(&SelectAll, cx);
}))
.size_full()
.line_height(rems(1.25))
.text_size(rems(0.875))
.py_2()
.h_10()
.when(self.appearance, |this| {
this.bg(cx.theme().input)
.border_color(if focused {
cx.theme().ring
} else {
cx.theme().input
})
.border_1()
.rounded_sm()
.shadow_sm()
.px_3()
.bg(if self.disabled {
cx.theme().muted
} else {
cx.theme().background
})
})
.child(div().w_full().child(TextElement {
input: cx.view().clone(),
}))
}
}

View file

@ -1,5 +1,6 @@
mod clickable;
mod disableable;
mod event;
mod icon;
mod platform;
mod prelude;
@ -11,12 +12,12 @@ pub mod button;
pub mod checkbox;
pub mod empty;
pub mod label;
pub mod text_field;
pub mod theme;
pub mod title_bar;
pub use styled_ext::StyledExt;
pub mod divider;
// pub mod dropdown;
pub mod input;
pub mod list;
pub mod picker;
pub mod switch;

View file

@ -24,8 +24,8 @@ actions!(
);
use crate::{
divider::Divider, empty::Empty, label::Label, stock::*, text_field::TextField,
theme::ActiveTheme, StyledExt as _,
divider::Divider, empty::Empty, input::TextInput, label::Label, stock::*, theme::ActiveTheme,
StyledExt as _,
};
enum ElementContainer {
@ -58,7 +58,7 @@ pub trait PickerDelegate: Sized + 'static {
fn should_dismiss(&self) -> bool {
true
}
fn render_query(&self, input: &View<TextField>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
fn render_query(&self, input: &View<TextInput>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
v_flex()
.child(
h_flex()
@ -86,7 +86,7 @@ pub trait PickerDelegate: Sized + 'static {
}
fn finalize_update_matches(
&mut self,
_query: String,
_query: SharedString,
_duration: Duration,
_cx: &mut ViewContext<Picker<Self>>,
) -> bool {
@ -117,7 +117,7 @@ struct PendingUpdateMatches {
pub struct Picker<D: PickerDelegate> {
delegate: D,
element_container: ElementContainer,
query_input: Option<View<TextField>>,
query_input: Option<View<TextInput>>,
width: Option<Length>,
max_height: Option<Length>,
is_modal: bool,
@ -130,7 +130,7 @@ impl<D: PickerDelegate> Picker<D> {
fn new(
delegate: D,
kind: ContainerKind,
query_input: Option<View<TextField>>,
query_input: Option<View<TextInput>>,
cx: &mut ViewContext<Self>,
) -> Self {
let element_container = match kind {
@ -172,9 +172,9 @@ impl<D: PickerDelegate> Picker<D> {
fn new_query_input(
placehoder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) -> View<TextField> {
) -> View<TextInput> {
cx.new_view(|cx| {
let mut input = TextField::new(cx).appearance(false);
let mut input = TextInput::new(cx).appearance(false);
input.set_placeholder(placehoder, cx);
input
})
@ -212,16 +212,16 @@ impl<D: PickerDelegate> Picker<D> {
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
if let Some(input) = &self.query_input {
input.update(cx, |this, cx| this.set_text(query, cx));
input.update(cx, |this, cx| this.set_text(query.to_string(), cx));
}
}
/// Return the query input string.
pub fn query(&self, cx: &AppContext) -> String {
pub fn query(&self, cx: &AppContext) -> SharedString {
if let Some(input) = &self.query_input {
input.read(cx).text(cx)
input.read(cx).text()
} else {
String::new()
"".into()
}
}

View file

@ -1,71 +0,0 @@
use std::time::Duration;
use gpui::{ModelContext, Timer};
pub struct BlinkManager {
blink_interval: Duration,
blink_epoch: usize,
blinking_paused: bool,
visible: bool,
enabled: bool,
}
impl BlinkManager {
pub fn new(blink_interval: Duration, _cx: &mut ModelContext<Self>) -> Self {
Self {
blink_interval,
blink_epoch: 0,
blinking_paused: false,
visible: true,
enabled: true,
}
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
self.show_cursor(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| async move {
Timer::after(interval).await;
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
})
.detach();
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursor(epoch, cx);
}
}
pub fn show_cursor(&self, _cx: &mut ModelContext<'_, Self>) -> bool {
self.enabled && (!self.blinking_paused || self.visible)
}
pub fn blink_cursor(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if self.blink_epoch != epoch {
self.blink_epoch = epoch;
self.visible = !self.visible;
cx.refresh();
}
}
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = false;
}
pub fn enable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = true;
}
pub fn visible(&self) -> bool {
self.visible
}
}

View file

@ -1,66 +0,0 @@
use gpui::{
fill, outline, px, size, Bounds, Hsla, Pixels, ShapedLine, Size, ViewContext, WindowContext,
};
#[derive(Debug, Clone)]
pub struct CursorLayout {
origin: gpui::Point<Pixels>,
#[allow(unused)]
block_width: Pixels,
line_height: Pixels,
color: Hsla,
block_text: Option<ShapedLine>,
}
impl CursorLayout {
pub fn new(
origin: gpui::Point<Pixels>,
block_width: Pixels,
line_height: Pixels,
color: Hsla,
block_text: Option<ShapedLine>,
) -> CursorLayout {
CursorLayout {
origin,
block_width,
line_height,
color,
block_text,
}
}
fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
// Return a bar sharp cursor
Bounds {
origin: self.origin + origin,
size: size(px(2.0), self.line_height),
}
}
pub fn layout(&mut self, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
let bounds = self.bounds(origin);
let cursor = fill(bounds, self.color);
cx.paint_quad(cursor);
if let Some(block_text) = &self.block_text {
block_text
.paint(self.origin + origin, self.line_height, cx)
.unwrap()
}
}
pub fn paint(&mut self, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
let bounds = self.bounds(origin);
let cursor = outline(bounds, self.color);
cx.paint_quad(cursor);
if let Some(block_text) = &self.block_text {
block_text
.paint(self.origin + origin, self.line_height, cx)
.unwrap()
}
}
}

View file

@ -1,269 +0,0 @@
mod blink_manager;
mod cursor_layout;
mod text_view;
use crate::{h_flex, theme::ActiveTheme};
use gpui::{
div, prelude::FluentBuilder as _, AppContext, ClipboardItem, Div, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, Interactivity, IntoElement, KeyDownEvent, MouseButton,
ParentElement, Render, RenderOnce, SharedString, Style, StyleRefinement, Styled, View,
ViewContext, WindowContext,
};
use std::{sync::Arc, time::Duration};
use text_view::TextView;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
pub struct TextField {
focus_handle: FocusHandle,
appearance: bool,
pub view: View<TextView>,
}
impl TextField {
pub fn new(cx: &mut WindowContext) -> Self {
let focus_handle = cx.focus_handle();
let view = TextView::init(cx, &focus_handle);
Self {
focus_handle,
view,
appearance: true,
}
}
pub fn focus(&mut self, cx: &mut WindowContext) {
cx.focus(&self.focus_handle);
}
/// Set the appearance of the text field.
///
/// If false, the text field will not have a border, background and focus ring.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut WindowContext,
) {
self.view.update(cx, |text_view, cx| {
text_view.set_placeholder(placeholder, cx)
});
}
pub fn set_disabled(&mut self, disabled: bool, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_disabled(disabled, cx));
}
pub fn set_text(&mut self, text: &str, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_text(text, cx));
}
pub fn text(&self, cx: &AppContext) -> String {
self.view.read(cx).text.clone()
}
pub fn set_masked(&mut self, masked: bool, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_masked(masked, cx));
}
}
impl FocusableView for TextField {
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for TextField {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let theme = cx.theme();
let view = self.view.clone();
let text_view = view.read(cx);
let focused = self.focus_handle.is_focused(cx);
let disabled = text_view.disabled;
h_flex()
.w_full()
.track_focus(&focus_handle)
.when(!text_view.disabled, |this| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |view, _, cx| {
cx.prevent_default();
view.focus_handle.focus(cx)
}),
)
})
.when(!disabled, |this| {
this.on_key_down(cx.listener(move |this, ev: &KeyDownEvent, cx| {
this.view.update(cx, |text_view, cx| {
let prev = text_view.text.clone();
cx.emit(TextEvent::KeyDown(ev.clone()));
let keystroke = ev.keystroke.key.as_str();
let chars = text_view.text.chars().collect::<Vec<char>>();
let m = ev.keystroke.modifiers.secondary();
if m {
match keystroke {
"a" => {
text_view.selection = 0..chars.len();
}
"c" => {
// if !text_view.masked {
let selected_text =
chars[text_view.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();
text_view.text.replace_range(
text_view.char_range_to_text_range(&text_view.text),
text,
);
let i = text_view.selection.start + text.chars().count();
text_view.selection = i..i;
}
}
"x" => {
let selected_text =
chars[text_view.selection.clone()].iter().collect();
cx.write_to_clipboard(ClipboardItem::new(selected_text));
text_view.text.replace_range(
text_view.char_range_to_text_range(&text_view.text),
"",
);
text_view.selection.end = text_view.selection.start;
}
_ => {}
}
} else if ev.keystroke.modifiers.control {
// On macOS, ctrl+a, ctrl+e are used for moving cursor to start/end of line
match keystroke {
"a" => {
// Move cursor to first of line
text_view.selection = 0..0;
}
"e" => {
// Move cursor to end of line
text_view.selection = chars.len()..chars.len();
}
_ => {}
}
} else if !ev.keystroke.ime_key.clone().unwrap_or_default().is_empty() {
let ime_key = &ev.keystroke.ime_key.clone().unwrap_or_default();
text_view.text.replace_range(
text_view.char_range_to_text_range(&text_view.text),
ime_key,
);
let i = text_view.selection.start + ime_key.chars().count();
text_view.selection = i..i;
} else {
match keystroke {
"left" => {
if text_view.selection.start > 0 {
let i = if text_view.selection.start
== text_view.selection.end
{
text_view.selection.start - 1
} else {
text_view.selection.start
};
text_view.selection = i..i;
}
}
"right" => {
if text_view.selection.end < text_view.text.len() {
let i = if text_view.selection.start
== text_view.selection.end
{
text_view.selection.end + 1
} else {
text_view.selection.end
};
text_view.selection = i..i;
}
}
"backspace" => {
if text_view.text.is_empty() && !ev.is_held {
// cx.emit(TextEvent::Back);
} else if text_view.selection.start == text_view.selection.end
&& text_view.selection.start > 0
{
let i = (text_view.selection.start - 1).min(chars.len());
text_view.text = chars[0..i].iter().collect::<String>()
+ &(chars[text_view.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
text_view.selection = i..i;
} else {
text_view.text.replace_range(
text_view.char_range_to_text_range(&text_view.text),
"",
);
text_view.selection.end = text_view.selection.start;
}
}
"enter" => {
if ev.keystroke.modifiers.shift {
text_view.text.insert(
text_view
.char_range_to_text_range(&text_view.text)
.start,
'\n',
);
let i = text_view.selection.start + 1;
text_view.selection = i..i;
}
}
_ => {}
};
}
if prev != text_view.text {
cx.emit(TextEvent::Input {
text: text_view.text.clone(),
});
}
cx.notify();
});
}))
})
.when(self.appearance, |this| {
this.border_color(if focused { theme.ring } else { theme.input })
.border_1()
.rounded_sm()
.py_1()
.px_3()
.h_9()
.shadow_sm()
.bg(if disabled {
theme.muted
} else {
theme.background
})
})
.min_w_20()
.child(view)
}
}
pub enum TextEvent {
Input { text: String },
Blur,
Focus,
KeyDown(KeyDownEvent),
}
impl EventEmitter<TextEvent> for TextField {}

View file

@ -1,376 +0,0 @@
use std::ops::Range;
use super::{
blink_manager::BlinkManager, cursor_layout::CursorLayout, TextEvent, CURSOR_BLINK_INTERVAL,
};
use crate::theme::{ActiveTheme, Colorize as _};
use gpui::{
px, relative, ContentMask, Context, Element, EventEmitter, FocusHandle, HighlightStyle, Hsla,
InteractiveText, IntoElement, Model, Point, Render, SharedString, Style, StyledText, TextStyle,
TextStyleRefinement, View, ViewContext, VisualContext, WindowContext,
};
#[derive(Clone)]
pub struct TextFieldStyle {
pub background: Hsla,
pub text: TextStyle,
}
pub struct TextView {
pub text: String,
pub style: TextFieldStyle,
pub placeholder: SharedString,
pub word_click: (usize, u16),
pub selection: Range<usize>,
pub disabled: bool,
pub blink_manager: Model<BlinkManager>,
pub masked: bool,
pub focused: bool,
}
impl EventEmitter<TextEvent> for TextView {}
impl TextView {
pub fn init(cx: &mut WindowContext, focus_handle: &FocusHandle) -> View<Self> {
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let theme = cx.theme();
let line_height = px(20.0);
let style = TextFieldStyle {
background: theme.transparent,
text: TextStyle {
color: theme.foreground,
line_height: line_height.into(),
..Default::default()
},
};
let m = Self {
text: String::new(),
style,
placeholder: "".into(),
word_click: (0, 0),
selection: 0..0,
blink_manager: blink_manager.clone(),
disabled: false,
masked: false,
focused: false,
};
let view = cx.new_view(|cx| {
cx.on_blur(focus_handle, |view: &mut TextView, cx| {
view.blur(cx);
})
.detach();
cx.on_focus(focus_handle, |view, cx| {
view.focus(cx);
})
.detach();
cx.observe(&blink_manager, |_, _, cx| cx.notify()).detach();
cx.observe_window_activation(|view, cx| {
let active = cx.is_window_active();
view.blink_manager.update(cx, |blink_manager, cx| {
if active {
blink_manager.enable(cx);
} else {
blink_manager.show_cursor(cx);
blink_manager.disable(cx);
}
})
})
.detach();
m
});
cx.subscribe(
&view,
move |subscriber, emitter: &TextEvent, cx| match emitter {
TextEvent::Input { text: _ } => {
subscriber.update(cx, |editor, _cx| {
editor.word_click = (0, 0);
});
}
TextEvent::Blur => {
subscriber.update(cx, |editor, cx| {
editor.blink_manager.update(cx, BlinkManager::disable);
editor.word_click = (0, 0);
});
}
_ => {}
},
)
.detach();
view
}
pub fn select_all(&mut self, cx: &mut ViewContext<Self>) {
self.selection = 0..self.text.len();
cx.notify();
}
pub fn blur(&mut self, cx: &mut ViewContext<Self>) {
self.focused = false;
self.blink_manager.update(cx, BlinkManager::disable);
cx.notify();
cx.emit(TextEvent::Blur);
}
pub fn focus(&mut self, cx: &mut ViewContext<Self>) {
self.focused = true;
self.blink_manager.update(cx, |bm, cx| {
bm.blink_cursor(0, cx);
});
cx.notify();
cx.emit(TextEvent::Focus);
}
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
}
/// Converts a character range to a text range (in bytes)
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
}
pub fn set_text(&mut self, text: impl ToString, cx: &mut ViewContext<Self>) {
self.text = text.to_string();
self.selection = self.text.len()..self.text.len();
cx.notify();
cx.emit(TextEvent::Input {
text: self.text.clone(),
});
}
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
self.masked = masked;
cx.notify();
}
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}
pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext<Self>) {
self.disabled = disabled;
cx.notify();
}
fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) {
let mut cursor = layout.visible_cursor.clone();
cursor.paint(layout.content_origin, cx);
}
pub fn show_cursor(&self, cx: &mut WindowContext) -> bool {
self.blink_manager.read(cx).visible() && self.focused
}
fn layout_visible_cursors(&self, cx: &mut WindowContext) -> CursorLayout {
let theme = cx.theme();
let selection = &self.selection;
let x = px(selection.end as f32);
let y = px(0.);
CursorLayout::new(Point::new(x, y), px(0.), px(20.0), theme.ring, None)
}
}
impl IntoElement for TextView {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
pub struct TextLayout {
content_origin: gpui::Point<gpui::Pixels>,
visible_cursor: CursorLayout,
}
impl Element for TextView {
type RequestLayoutState = ();
type PrepaintState = TextLayout;
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&gpui::GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
let rem_size = cx.rem_size();
style.size.width = relative(1.).into();
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
(cx.request_layout(style, None), ())
}
fn prepaint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
let text_style = TextStyleRefinement {
font_size: Some(self.style.text.font_size),
line_height: Some(self.style.text.line_height),
..Default::default()
};
cx.with_text_style(Some(text_style), |cx| {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
let cursor = self.layout_visible_cursors(cx);
TextLayout {
content_origin: bounds.origin,
visible_cursor: cursor,
}
})
})
}
fn paint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
layout: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
let text_style = TextStyleRefinement {
font_size: Some(self.style.text.font_size),
line_height: Some(self.style.text.line_height),
..Default::default()
};
cx.with_text_style(Some(text_style), |cx| {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
self.paint_cursors(layout, cx);
})
});
}
}
impl Render for TextView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.theme();
let view = cx.view().clone();
let mut text = self.text.clone();
let mut style = self.style.text.clone();
if self.masked {
text = "".repeat(text.len());
}
let selection_style = HighlightStyle {
background_color: Some(theme.ring),
color: Some(theme.ring.invert()),
..Default::default()
};
let mut highlights = vec![(self.char_range_to_text_range(&text), selection_style)];
if text.is_empty() {
text = self.placeholder.to_string();
style.color = theme.muted_foreground;
} else {
style.color = theme.foreground;
}
if !self.focused {
highlights = vec![];
}
let styled_text = StyledText::new(text).with_highlights(&style, highlights);
InteractiveText::new("text", styled_text).on_click(
self.word_ranges(),
move |range_ix, cx| {
view.update(cx, |text_view, cx| {
let (index, mut count) = text_view.word_click;
if index == range_ix {
count += 1;
} else {
count = 1;
}
match count {
1 => {
// Position cursor at the beginning of the word
}
2 => {
if text_view.masked {
text_view.selection = 0..text_view.text.len();
} else {
let word_ranges = text_view.word_ranges();
text_view.selection = word_ranges.get(range_ix).unwrap().clone();
}
}
3 | 4 => {
count = 0;
text_view.selection = 0..text_view.text.len();
}
_ => {}
}
text_view.word_click = (range_ix, count);
cx.notify();
});
},
)
}
}

View file

@ -1,7 +1,4 @@
use std::{cmp::min, sync::Arc};
use gpui::{AppContext, Global, Hsla, Rgba};
use serde_json::json;
use gpui::{hsla, AppContext, Global, Hsla};
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
@ -13,13 +10,11 @@ impl ActiveTheme for AppContext {
}
}
fn hex(color: &str) -> Hsla {
let color: Rgba = serde_json::from_value(json!(color)).unwrap();
color.into()
}
/// h - 0 - 360.0
/// s - 0.0 - 100.0
/// l - 0.0 - 100.0
fn hsl(h: f32, s: f32, l: f32) -> Hsla {
Hsla { h, s, l, a: 1.0 }
hsla(h / 360., s / 100.0, l / 100.0, 1.0)
}
pub trait Colorize {
@ -74,53 +69,6 @@ impl Colorize for Hsla {
}
}
// @layer base {
// :root {
// --background: 0 0% 100%;
// --foreground: 240 10% 3.9%;
// --card: 0 0% 100%;
// --card-foreground: 240 10% 3.9%;
// --popover: 0 0% 100%;
// --popover-foreground: 240 10% 3.9%;
// --primary: 240 5.9% 10%;
// --primary-foreground: 0 0% 98%;
// --secondary: 240 4.8% 95.9%;
// --secondary-foreground: 240 5.9% 10%;
// --muted: 240 4.8% 95.9%;
// --muted-foreground: 240 3.8% 46.1%;
// --accent: 240 4.8% 95.9%;
// --accent-foreground: 240 5.9% 10%;
// --destructive: 0 84.2% 60.2%;
// --destructive-foreground: 0 0% 98%;
// --border: 240 5.9% 90%;
// --input: 240 5.9% 90%;
// --ring: 240 5.9% 10%;
// --radius: 0rem;
// }
// .dark {
// --background: 240 10% 3.9%;
// --foreground: 0 0% 98%;
// --card: 240 10% 3.9%;
// --card-foreground: 0 0% 98%;
// --popover: 240 10% 3.9%;
// --popover-foreground: 0 0% 98%;
// --primary: 0 0% 98%;
// --primary-foreground: 240 5.9% 10%;
// --secondary: 240 3.7% 15.9%;
// --secondary-foreground: 0 0% 98%;
// --muted: 240 3.7% 15.9%;
// --muted-foreground: 240 5% 64.9%;
// --accent: 240 3.7% 15.9%;
// --accent-foreground: 0 0% 98%;
// --destructive: 0 62.8% 30.6%;
// --destructive-foreground: 0 0% 98%;
// --border: 240 3.7% 15.9%;
// --input: 240 3.7% 15.9%;
// --ring: 240 4.9% 83.9%;
// }
// }
#[derive(Debug, Clone, Copy)]
struct Colors {
pub title_bar_background: Hsla,
@ -144,59 +92,106 @@ struct Colors {
pub border: Hsla,
pub input: Hsla,
pub ring: Hsla,
pub radius: f32,
pub selection: Hsla,
}
impl Colors {
// .light {
// --title_bar_background: 0 0% 100%;
// --background: 0 0% 100%;
// --foreground: 240 10% 3.9%;
// --card: 0 0% 100%;
// --card-foreground: 240 10% 3.9%;
// --popover: 0 0% 100%;
// --popover-foreground: 240 10% 3.9%;
// --primary: 240 5.9% 10%;
// --primary-foreground: 0 0% 98%;
// --secondary: 240 4.8% 95.9%;
// --secondary-foreground: 240 5.9% 10%;
// --muted: 240 4.8% 95.9%;
// --muted-foreground: 240 3.8% 46.1%;
// --accent: 240 4.8% 95.9%;
// --accent-foreground: 240 5.9% 10%;
// --destructive: 0 84.2% 60.2%;
// --destructive-foreground: 0 0% 98%;
// --border: 240 5.9% 90%;
// --input: 240 5.9% 90%;
// --ring: 240 5.9% 10%;
// --radius: 0rem;
// --selection: 211 97% 85%;
// }
fn light() -> Colors {
Colors {
title_bar_background: hsl(0.0, 0.0, 1.0),
background: hsl(0.0, 0.0, 1.0),
foreground: hsl(240.0, 0.1, 0.039),
card: hsl(0.0, 0.0, 1.0),
card_foreground: hsl(240.0, 0.1, 0.039),
title_bar_background: hsl(0.0, 0.0, 100.),
background: hsl(0.0, 0.0, 100.),
foreground: hsl(240.0, 10., 3.9),
card: hsl(0.0, 0.0, 100.0),
card_foreground: hsl(240.0, 10.0, 3.9),
popover: hsl(0.0, 0.0, 1.0),
popover_foreground: hsl(240.0, 0.1, 0.039),
primary: hsl(240.0, 0.059, 0.1),
primary_foreground: hsl(0.0, 0.0, 0.98),
secondary: hsl(240.0, 0.048, 0.959),
secondary_foreground: hsl(240.0, 0.059, 0.1),
muted: hsl(240.0, 0.048, 0.959),
muted_foreground: hsl(240.0, 0.038, 0.461),
accent: hsl(240.0, 0.05, 0.96),
accent_foreground: hsl(240.0, 0.059, 0.1),
destructive: hsl(0.0, 0.842, 0.602),
destructive_foreground: hsl(0.0, 0.0, 0.98),
border: hsl(240.0, 0.059, 0.9),
input: hsl(240.0, 0.059, 0.9),
ring: hsl(240.0, 0.059, 0.1),
radius: 0.0,
popover_foreground: hsl(240.0, 10.0, 3.9),
primary: hsl(240.0, 5.9, 10.0),
primary_foreground: hsl(0.0, 0.0, 98.0),
secondary: hsl(240.0, 4.8, 95.9),
secondary_foreground: hsl(240.0, 59.0, 10.0),
muted: hsl(240.0, 4.8, 95.9),
muted_foreground: hsl(240.0, 3.8, 46.1),
accent: hsl(240.0, 5.0, 96.0),
accent_foreground: hsl(240.0, 5.9, 10.0),
destructive: hsl(0.0, 84.2, 60.2),
destructive_foreground: hsl(0.0, 0.0, 98.0),
border: hsl(240.0, 5.9, 90.0),
input: hsl(240.0, 5.9, 90.0),
ring: hsl(240.0, 5.9, 10.0),
selection: hsl(211.0, 97.0, 85.0),
}
}
// .dark {
// --title_bar_background: 0 0% 12%;
// --background: 240 10% 3.9%;
// --foreground: 0 0% 98%;
// --card: 240 10% 3.9%;
// --card-foreground: 0 0% 98%;
// --popover: 240 10% 3.9%;
// --popover-foreground: 0 0% 98%;
// --primary: 0 0% 98%;
// --primary-foreground: 240 5.9% 10%;
// --secondary: 240 3.7% 15.9%;
// --secondary-foreground: 0 0% 98%;
// --muted: 240 3.7% 15.9%;
// --muted-foreground: 240 5% 64.9%;
// --accent: 240 3.7% 15.9%;
// --accent-foreground: 0 0% 98%;
// --destructive: 0 62.8% 30.6%;
// --destructive-foreground: 0 0% 98%;
// --border: 240 3.7% 15.9%;
// --input: 240 3.7% 15.9%;
// --ring: 240 4.9% 83.9%;
// --selection: 211 97% 85%;
// }
fn dark() -> Colors {
Colors {
title_bar_background: hsl(0.0, 0.0, 0.12),
background: hsl(0.0, 0.0, 0.06),
foreground: hsl(0.0, 0.0, 0.98),
card: hsl(299.0, 0.02, 0.09),
card_foreground: hsl(0.0, 0.0, 0.98),
popover: hsl(240.0, 0.1, 0.039),
popover_foreground: hsl(0.0, 0.0, 0.98),
primary: hsl(0.0, 0.0, 0.98),
primary_foreground: hsl(240.0, 0.059, 0.1),
secondary: hsl(0.0, 0.0, 0.12),
secondary_foreground: hsl(0.0, 0.0, 0.98),
muted: hsl(240.0, 0.037, 0.159),
muted_foreground: hsl(240.0, 0.05, 0.649),
accent: hsl(240.0, 0.037, 0.159),
accent_foreground: hsl(0.0, 0.0, 0.98),
destructive: hsl(0.0, 0.628, 0.306),
destructive_foreground: hsl(0.0, 0.0, 0.98),
border: hsl(0.0, 0.0, 0.17),
input: hsl(240.0, 0.037, 0.159),
ring: hsl(240.0, 0.049, 0.839),
radius: 0.0,
title_bar_background: hsl(0., 0., 12.),
background: hsl(0.0, 0.0, 6.0),
foreground: hsl(0., 0., 98.),
card: hsl(299.0, 2., 9.),
card_foreground: hsl(0.0, 0.0, 98.0),
popover: hsl(240.0, 10.0, 3.9),
popover_foreground: hsl(0.0, 0.0, 98.0),
primary: hsl(0.0, 0.0, 98.0),
primary_foreground: hsl(240.0, 5.9, 10.0),
secondary: hsl(240.0, 3.7, 15.9),
secondary_foreground: hsl(0.0, 0.0, 98.0),
muted: hsl(240.0, 3.7, 15.9),
muted_foreground: hsl(240.0, 5.0, 64.9),
accent: hsl(240.0, 3.7, 15.9),
accent_foreground: hsl(0.0, 0.0, 98.0),
destructive: hsl(0.0, 62.8, 30.6),
destructive_foreground: hsl(0.0, 0.0, 98.0),
border: hsl(240.0, 3.7, 15.9),
input: hsl(240.0, 3.7, 15.9),
ring: hsl(240.0, 4.9, 83.9),
selection: hsl(211.0, 97.0, 85.0),
}
}
}
@ -227,6 +222,7 @@ pub struct Theme {
pub border: Hsla,
pub input: Hsla,
pub ring: Hsla,
pub selection: Hsla,
pub radius: f32,
}
@ -264,7 +260,8 @@ impl From<Colors> for Theme {
border: colors.border,
input: colors.input,
ring: colors.ring,
radius: colors.radius,
selection: colors.selection,
radius: 0.0,
}
}
}

View file

@ -3,12 +3,12 @@ use prelude::FluentBuilder as _;
use std::sync::Arc;
use ui::{
button::{Button, ButtonSize},
button::ButtonSize,
input,
label::Label,
switch::{LabelSide, Switch},
theme::{ActiveTheme, Theme},
title_bar::TitleBar,
Clickable as _, StyledExt as _,
};
use ui_story::Stories;
use util::ResultExt as _;
@ -42,6 +42,8 @@ impl Workspace {
) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
let window_bounds = Bounds::centered(None, size(px(1200.0), px(900.0)), cx);
input::init(cx);
cx.spawn(|mut cx| async move {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(window_bounds)),