Jason Lee 2024-07-17 18:49:41 +08:00 committed by GitHub
parent d71b697f41
commit c7336fd5ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 261 additions and 18 deletions

View file

@ -8,7 +8,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs
## Features
- [x] Theming
- [ ] TextField
- [ ] TextInput
- [x] Ctrl+a, e to move cursor to start/end
- [x] Copy, Cut, Paste by keyboard
- [x] Selection by mouse, drag to select text
@ -16,7 +16,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs
- [x] Input icon
- [ ] Textarea
- [ ] ContextMenu to let user copy, cut, paste
- [ ] InputOTP
- [x] InputOTP
- [x] Button
- [x] Button with Icon
- [x] IconButton

View file

@ -1,10 +1,15 @@
use gpui::{
actions, AppContext, ClickEvent, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
ParentElement as _, Render, Styled, View, ViewContext, VisualContext, WindowContext,
ParentElement as _, Render, SharedString, Styled, View, ViewContext, VisualContext,
WindowContext,
};
use ui::{
button::Button, h_flex, input::TextInput, v_flex, Clickable, FocusableCycle, IconName, Size,
button::Button,
h_flex,
input::{InputOtp, TextInput},
prelude::FluentBuilder as _,
v_flex, Clickable, FocusableCycle, IconName, Size,
};
use crate::section;
@ -28,6 +33,8 @@ pub struct InputStory {
both_input1: View<TextInput>,
large_input: View<TextInput>,
small_input: View<TextInput>,
otp_input: View<InputOtp>,
otp_value: Option<SharedString>,
}
impl InputStory {
@ -35,7 +42,7 @@ impl InputStory {
cx.new_view(|cx| Self::new(cx))
}
fn new(cx: &mut WindowContext) -> Self {
fn new(cx: &mut ViewContext<Self>) -> Self {
let input1 = cx.new_view(|cx| {
let mut input = TextInput::new(cx).cleanable(true);
input.set_text(
@ -72,6 +79,15 @@ impl InputStory {
.placeholder("This input have prefix and suffix.")
});
let view = cx.view().clone();
let otp_input = cx.new_view(|cx| {
InputOtp::new(6, cx).on_change(move |value: &SharedString, cx| {
view.update(cx, |view, _| {
view.otp_value = Some(value.clone());
})
})
});
Self {
input1,
input2: cx.new_view(|cx| TextInput::new(cx).placeholder("Enter text here...")),
@ -95,6 +111,8 @@ impl InputStory {
prefix_input1,
suffix_input1,
both_input1,
otp_input,
otp_value: None,
}
}
@ -115,17 +133,18 @@ impl InputStory {
impl FocusableCycle for InputStory {
fn cycle_focus_handles(&self, cx: &mut ViewContext<Self>) -> Vec<FocusHandle> {
[
&self.input1,
&self.input2,
&self.disabled_input,
&self.mash_input,
&self.prefix_input1,
&self.both_input1,
&self.suffix_input1,
self.input1.focus_handle(cx),
self.input2.focus_handle(cx),
self.disabled_input.focus_handle(cx),
self.mash_input.focus_handle(cx),
self.prefix_input1.focus_handle(cx),
self.both_input1.focus_handle(cx),
self.suffix_input1.focus_handle(cx),
self.large_input.focus_handle(cx),
self.small_input.focus_handle(cx),
self.otp_input.focus_handle(cx),
]
.iter()
.map(|v| v.focus_handle(cx))
.collect()
.to_vec()
}
}
@ -160,6 +179,13 @@ impl Render for InputStory {
.child(self.large_input.clone())
.child(self.small_input.clone()),
)
.child(
section("Input OTP", cx)
.child(self.otp_input.clone())
.when_some(self.otp_value.clone(), |this, otp| {
this.child(format!("You input OTP: {}", otp))
}),
)
.child(
h_flex()
.items_center()

View file

@ -5,12 +5,12 @@
use std::ops::Range;
use super::blink_cursor::BlinkCursor;
use crate::button::{Button, ButtonStyle};
use crate::styled_ext::Sizeful;
use crate::theme::ActiveTheme;
use crate::{event::InterativeElementExt as _, Size};
use crate::{Clickable, IconName, StyledExt as _};
use blink_cursor::BlinkCursor;
use gpui::prelude::FluentBuilder as _;
use gpui::{
actions, div, fill, point, px, relative, rems, size, AnyView, AppContext, Bounds, ClickEvent,
@ -22,8 +22,6 @@ use gpui::{
};
use unicode_segmentation::*;
mod blink_cursor;
actions!(
input,
[
@ -222,6 +220,10 @@ impl TextInput {
self.text.clone()
}
pub fn focus(&self, cx: &mut ViewContext<Self>) {
self.focus_handle.focus(cx);
}
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {

View file

@ -0,0 +1,209 @@
use gpui::{
div, prelude::FluentBuilder as _, AnyElement, Context, Div, FocusHandle, FocusableView,
InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, MouseDownEvent,
ParentElement as _, Render, SharedString, Stateful, Styled as _, ViewContext, WindowContext,
};
use crate::{h_flex, theme::ActiveTheme, v_flex, StyledExt};
use super::blink_cursor::BlinkCursor;
pub struct InputOtp {
focus_handle: FocusHandle,
length: usize,
number_of_groups: usize,
masked: bool,
value: SharedString,
blink_cursor: Model<BlinkCursor>,
on_change: Option<Box<dyn Fn(&SharedString, &mut WindowContext) + 'static>>,
}
impl InputOtp {
pub fn new(length: usize, cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
let blink_cursor = cx.new_model(|cx| BlinkCursor::new(cx));
let input = Self {
focus_handle: focus_handle.clone(),
length,
number_of_groups: 2,
value: SharedString::default(),
masked: true,
on_change: None,
blink_cursor: blink_cursor.clone(),
};
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&blink_cursor, |_, _, cx| cx.notify()).detach();
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(|this, cx| {
if cx.is_window_active() {
let focus_handle = this.focus_handle.clone();
if focus_handle.is_focused(cx) {
this.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
})
.detach();
cx.on_focus(&focus_handle, Self::on_focus).detach();
cx.on_blur(&focus_handle, Self::on_blur).detach();
input
}
/// Set number of groups in the OTP Input.
pub fn groups(mut self, n: usize) -> Self {
self.number_of_groups = n;
self
}
/// Set callback to be called when the value of the OTP Input changes (All OTP input have filled).
pub fn on_change<F>(mut self, f: F) -> Self
where
F: Fn(&SharedString, &mut WindowContext) + 'static,
{
self.on_change = Some(Box::new(f));
self
}
fn on_input_mouse_down(&mut self, _: &MouseDownEvent, cx: &mut ViewContext<Self>) {
cx.focus(&self.focus_handle);
}
fn on_key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
let mut chars: Vec<char> = self.value.chars().collect();
let ix = chars.len();
let key = event.keystroke.key.as_str();
match key {
"backspace" => {
if ix > 0 {
let ix = ix - 1;
chars.remove(ix);
}
}
_ => {
let c = key.chars().next().unwrap();
if !matches!(c, '0'..='9') {
return;
}
if ix >= self.length {
return;
}
chars.push(c);
}
}
self.pause_blink_cursor(cx);
self.value = SharedString::from(chars.iter().collect::<String>());
if self.value.chars().count() == self.length {
if let Some(on_change) = &self.on_change {
on_change(&self.value, cx);
}
}
cx.notify()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.start(cx);
});
}
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.stop(cx);
});
}
fn pause_blink_cursor(&mut self, cx: &mut ViewContext<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.pause(cx);
});
}
}
impl FocusableView for InputOtp {
fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for InputOtp {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let blink_show = self.blink_cursor.read(cx).visible();
let is_focused = self.focus_handle.is_focused(cx);
let mut groups: Vec<Vec<AnyElement>> = Vec::with_capacity(self.number_of_groups);
let mut group_ix = 0;
let group_items_count = self.length / self.number_of_groups;
for _ in 0..self.number_of_groups {
groups.push(vec![]);
}
for i in 0..self.length {
let c = self.value.chars().nth(i);
if i % group_items_count == 0 && i != 0 {
group_ix += 1;
}
let is_input_focused = i == self.value.chars().count() && is_focused;
groups[group_ix].push(
h_flex()
.id(("input-otp", i))
.border_1()
.border_color(cx.theme().input)
.bg(cx.theme().background)
.when(is_input_focused, |this| this.border_color(cx.theme().ring))
.shadow_sm()
.items_center()
.justify_center()
.rounded_md()
.w_8()
.h_8()
.text_lg()
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_input_mouse_down))
.when_some_else(
c,
|this, c| {
this.child(if self.masked {
SharedString::from("")
} else {
SharedString::from(c.to_string())
})
},
|this| {
this.when(is_input_focused && blink_show, |this| {
this.child(
div()
.h_4()
.w_0()
.border_l_3()
.border_color(crate::blue_500()),
)
})
},
)
.into_any_element(),
);
}
v_flex()
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(Self::on_key_down))
.items_center()
.child(
h_flex().items_center().gap_5().children(
groups
.into_iter()
.map(|inputs| h_flex().items_center().gap_1().children(inputs)),
),
)
}
}

View file

@ -0,0 +1,6 @@
mod blink_cursor;
mod input;
mod input_otp;
pub use input::*;
pub use input_otp::*;