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,
|
VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ui::text_field::TextField;
|
use ui::input::TextInput;
|
||||||
|
|
||||||
use super::story_case;
|
use super::story_case;
|
||||||
|
|
||||||
pub struct InputStory {
|
pub struct InputStory {
|
||||||
input1: View<TextField>,
|
input1: View<TextInput>,
|
||||||
input2: View<TextField>,
|
input2: View<TextInput>,
|
||||||
mash_input: View<TextField>,
|
mash_input: View<TextInput>,
|
||||||
disabled_input: View<TextField>,
|
disabled_input: View<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputStory {
|
impl InputStory {
|
||||||
pub(crate) fn new(cx: &mut WindowContext) -> Self {
|
pub(crate) fn new(cx: &mut WindowContext) -> Self {
|
||||||
let input1 = cx.new_view(|cx| {
|
let input1 = cx.new_view(|cx| {
|
||||||
let mut input = TextField::new(cx);
|
let mut input = TextInput::new(cx);
|
||||||
input.set_text("Hello 世界", cx);
|
input.set_text("Hello 世界", cx);
|
||||||
input
|
input
|
||||||
});
|
});
|
||||||
|
|
||||||
let mask_input = cx.new_view(|cx| {
|
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_masked(true, cx);
|
||||||
input.set_text("this-is-password", cx);
|
input.set_text("this-is-password", cx);
|
||||||
input
|
input
|
||||||
|
|
@ -32,13 +32,13 @@ impl InputStory {
|
||||||
Self {
|
Self {
|
||||||
input1,
|
input1,
|
||||||
input2: cx.new_view(|cx| {
|
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.set_placeholder("Enter text here...", cx);
|
||||||
input
|
input
|
||||||
}),
|
}),
|
||||||
mash_input: mask_input,
|
mash_input: mask_input,
|
||||||
disabled_input: cx.new_view(|cx| {
|
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_text("This is disabled input", cx);
|
||||||
input.set_disabled(true, cx);
|
input.set_disabled(true, cx);
|
||||||
input
|
input
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ serde_json = "1"
|
||||||
wry = "0"
|
wry = "0"
|
||||||
smallvec = "1.13.2"
|
smallvec = "1.13.2"
|
||||||
windows = "0.57.0"
|
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 clickable;
|
||||||
mod disableable;
|
mod disableable;
|
||||||
|
mod event;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod prelude;
|
mod prelude;
|
||||||
|
|
@ -11,12 +12,12 @@ pub mod button;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod empty;
|
pub mod empty;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod text_field;
|
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod title_bar;
|
pub mod title_bar;
|
||||||
pub use styled_ext::StyledExt;
|
pub use styled_ext::StyledExt;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
// pub mod dropdown;
|
// pub mod dropdown;
|
||||||
|
pub mod input;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod picker;
|
pub mod picker;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ actions!(
|
||||||
);
|
);
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
divider::Divider, empty::Empty, label::Label, stock::*, text_field::TextField,
|
divider::Divider, empty::Empty, input::TextInput, label::Label, stock::*, theme::ActiveTheme,
|
||||||
theme::ActiveTheme, StyledExt as _,
|
StyledExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ElementContainer {
|
enum ElementContainer {
|
||||||
|
|
@ -58,7 +58,7 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
fn should_dismiss(&self) -> bool {
|
fn should_dismiss(&self) -> bool {
|
||||||
true
|
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()
|
v_flex()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -86,7 +86,7 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
}
|
}
|
||||||
fn finalize_update_matches(
|
fn finalize_update_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
_query: String,
|
_query: SharedString,
|
||||||
_duration: Duration,
|
_duration: Duration,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
|
@ -117,7 +117,7 @@ struct PendingUpdateMatches {
|
||||||
pub struct Picker<D: PickerDelegate> {
|
pub struct Picker<D: PickerDelegate> {
|
||||||
delegate: D,
|
delegate: D,
|
||||||
element_container: ElementContainer,
|
element_container: ElementContainer,
|
||||||
query_input: Option<View<TextField>>,
|
query_input: Option<View<TextInput>>,
|
||||||
width: Option<Length>,
|
width: Option<Length>,
|
||||||
max_height: Option<Length>,
|
max_height: Option<Length>,
|
||||||
is_modal: bool,
|
is_modal: bool,
|
||||||
|
|
@ -130,7 +130,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
fn new(
|
fn new(
|
||||||
delegate: D,
|
delegate: D,
|
||||||
kind: ContainerKind,
|
kind: ContainerKind,
|
||||||
query_input: Option<View<TextField>>,
|
query_input: Option<View<TextInput>>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let element_container = match kind {
|
let element_container = match kind {
|
||||||
|
|
@ -172,9 +172,9 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
fn new_query_input(
|
fn new_query_input(
|
||||||
placehoder: impl Into<SharedString>,
|
placehoder: impl Into<SharedString>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> View<TextField> {
|
) -> View<TextInput> {
|
||||||
cx.new_view(|cx| {
|
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.set_placeholder(placehoder, cx);
|
||||||
input
|
input
|
||||||
})
|
})
|
||||||
|
|
@ -212,16 +212,16 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
|
|
||||||
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(input) = &self.query_input {
|
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.
|
/// 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 {
|
if let Some(input) = &self.query_input {
|
||||||
input.read(cx).text(cx)
|
input.read(cx).text()
|
||||||
} else {
|
} 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::{hsla, AppContext, Global, Hsla};
|
||||||
|
|
||||||
use gpui::{AppContext, Global, Hsla, Rgba};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
pub trait ActiveTheme {
|
pub trait ActiveTheme {
|
||||||
fn theme(&self) -> &Theme;
|
fn theme(&self) -> &Theme;
|
||||||
|
|
@ -13,13 +10,11 @@ impl ActiveTheme for AppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hex(color: &str) -> Hsla {
|
/// h - 0 - 360.0
|
||||||
let color: Rgba = serde_json::from_value(json!(color)).unwrap();
|
/// s - 0.0 - 100.0
|
||||||
color.into()
|
/// l - 0.0 - 100.0
|
||||||
}
|
|
||||||
|
|
||||||
fn hsl(h: f32, s: f32, l: f32) -> Hsla {
|
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 {
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct Colors {
|
struct Colors {
|
||||||
pub title_bar_background: Hsla,
|
pub title_bar_background: Hsla,
|
||||||
|
|
@ -144,59 +92,106 @@ struct Colors {
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
pub radius: f32,
|
pub selection: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Colors {
|
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 {
|
fn light() -> Colors {
|
||||||
Colors {
|
Colors {
|
||||||
title_bar_background: hsl(0.0, 0.0, 1.0),
|
title_bar_background: hsl(0.0, 0.0, 100.),
|
||||||
background: hsl(0.0, 0.0, 1.0),
|
background: hsl(0.0, 0.0, 100.),
|
||||||
foreground: hsl(240.0, 0.1, 0.039),
|
foreground: hsl(240.0, 10., 3.9),
|
||||||
card: hsl(0.0, 0.0, 1.0),
|
card: hsl(0.0, 0.0, 100.0),
|
||||||
card_foreground: hsl(240.0, 0.1, 0.039),
|
card_foreground: hsl(240.0, 10.0, 3.9),
|
||||||
popover: hsl(0.0, 0.0, 1.0),
|
popover: hsl(0.0, 0.0, 1.0),
|
||||||
popover_foreground: hsl(240.0, 0.1, 0.039),
|
popover_foreground: hsl(240.0, 10.0, 3.9),
|
||||||
primary: hsl(240.0, 0.059, 0.1),
|
primary: hsl(240.0, 5.9, 10.0),
|
||||||
primary_foreground: hsl(0.0, 0.0, 0.98),
|
primary_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
secondary: hsl(240.0, 0.048, 0.959),
|
secondary: hsl(240.0, 4.8, 95.9),
|
||||||
secondary_foreground: hsl(240.0, 0.059, 0.1),
|
secondary_foreground: hsl(240.0, 59.0, 10.0),
|
||||||
muted: hsl(240.0, 0.048, 0.959),
|
muted: hsl(240.0, 4.8, 95.9),
|
||||||
muted_foreground: hsl(240.0, 0.038, 0.461),
|
muted_foreground: hsl(240.0, 3.8, 46.1),
|
||||||
accent: hsl(240.0, 0.05, 0.96),
|
accent: hsl(240.0, 5.0, 96.0),
|
||||||
accent_foreground: hsl(240.0, 0.059, 0.1),
|
accent_foreground: hsl(240.0, 5.9, 10.0),
|
||||||
destructive: hsl(0.0, 0.842, 0.602),
|
destructive: hsl(0.0, 84.2, 60.2),
|
||||||
destructive_foreground: hsl(0.0, 0.0, 0.98),
|
destructive_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
border: hsl(240.0, 0.059, 0.9),
|
border: hsl(240.0, 5.9, 90.0),
|
||||||
input: hsl(240.0, 0.059, 0.9),
|
input: hsl(240.0, 5.9, 90.0),
|
||||||
ring: hsl(240.0, 0.059, 0.1),
|
ring: hsl(240.0, 5.9, 10.0),
|
||||||
radius: 0.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 {
|
fn dark() -> Colors {
|
||||||
Colors {
|
Colors {
|
||||||
title_bar_background: hsl(0.0, 0.0, 0.12),
|
title_bar_background: hsl(0., 0., 12.),
|
||||||
background: hsl(0.0, 0.0, 0.06),
|
background: hsl(0.0, 0.0, 6.0),
|
||||||
foreground: hsl(0.0, 0.0, 0.98),
|
foreground: hsl(0., 0., 98.),
|
||||||
card: hsl(299.0, 0.02, 0.09),
|
card: hsl(299.0, 2., 9.),
|
||||||
card_foreground: hsl(0.0, 0.0, 0.98),
|
card_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
popover: hsl(240.0, 0.1, 0.039),
|
popover: hsl(240.0, 10.0, 3.9),
|
||||||
popover_foreground: hsl(0.0, 0.0, 0.98),
|
popover_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
primary: hsl(0.0, 0.0, 0.98),
|
primary: hsl(0.0, 0.0, 98.0),
|
||||||
primary_foreground: hsl(240.0, 0.059, 0.1),
|
primary_foreground: hsl(240.0, 5.9, 10.0),
|
||||||
secondary: hsl(0.0, 0.0, 0.12),
|
secondary: hsl(240.0, 3.7, 15.9),
|
||||||
secondary_foreground: hsl(0.0, 0.0, 0.98),
|
secondary_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
muted: hsl(240.0, 0.037, 0.159),
|
muted: hsl(240.0, 3.7, 15.9),
|
||||||
muted_foreground: hsl(240.0, 0.05, 0.649),
|
muted_foreground: hsl(240.0, 5.0, 64.9),
|
||||||
accent: hsl(240.0, 0.037, 0.159),
|
accent: hsl(240.0, 3.7, 15.9),
|
||||||
accent_foreground: hsl(0.0, 0.0, 0.98),
|
accent_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
destructive: hsl(0.0, 0.628, 0.306),
|
destructive: hsl(0.0, 62.8, 30.6),
|
||||||
destructive_foreground: hsl(0.0, 0.0, 0.98),
|
destructive_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
border: hsl(0.0, 0.0, 0.17),
|
border: hsl(240.0, 3.7, 15.9),
|
||||||
input: hsl(240.0, 0.037, 0.159),
|
input: hsl(240.0, 3.7, 15.9),
|
||||||
ring: hsl(240.0, 0.049, 0.839),
|
ring: hsl(240.0, 4.9, 83.9),
|
||||||
radius: 0.0,
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -227,6 +222,7 @@ pub struct Theme {
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
|
pub selection: Hsla,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +260,8 @@ impl From<Colors> for Theme {
|
||||||
border: colors.border,
|
border: colors.border,
|
||||||
input: colors.input,
|
input: colors.input,
|
||||||
ring: colors.ring,
|
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 std::sync::Arc;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonSize},
|
button::ButtonSize,
|
||||||
|
input,
|
||||||
label::Label,
|
label::Label,
|
||||||
switch::{LabelSide, Switch},
|
switch::{LabelSide, Switch},
|
||||||
theme::{ActiveTheme, Theme},
|
theme::{ActiveTheme, Theme},
|
||||||
title_bar::TitleBar,
|
title_bar::TitleBar,
|
||||||
Clickable as _, StyledExt as _,
|
|
||||||
};
|
};
|
||||||
use ui_story::Stories;
|
use ui_story::Stories;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
@ -42,6 +42,8 @@ impl Workspace {
|
||||||
) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
|
) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
|
||||||
let window_bounds = Bounds::centered(None, size(px(1200.0), px(900.0)), cx);
|
let window_bounds = Bounds::centered(None, size(px(1200.0), px(900.0)), cx);
|
||||||
|
|
||||||
|
input::init(cx);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let options = WindowOptions {
|
let options = WindowOptions {
|
||||||
window_bounds: Some(WindowBounds::Windowed(window_bounds)),
|
window_bounds: Some(WindowBounds::Windowed(window_bounds)),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue