New input (#1)
 Ref: https://github.com/zed-industries/zed/pull/13534
This commit is contained in:
parent
a90f51d165
commit
659f00c9a4
13 changed files with 1964 additions and 955 deletions
1258
Cargo.lock
generated
1258
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
28
crates/ui/src/event.rs
Normal 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
602
crates/ui/src/input.rs
Normal 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(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue