1use std::{
2 borrow::Cow,
3 cell::{
4 Ref,
5 RefCell,
6 },
7 rc::Rc,
8};
9
10use freya_core::prelude::*;
11use freya_edit::*;
12use torin::{
13 prelude::{
14 Alignment,
15 Area,
16 Direction,
17 },
18 size::Size,
19};
20
21use crate::{
22 cursor_blink::use_cursor_blink,
23 get_theme,
24 scrollviews::ScrollView,
25 theming::component_themes::InputThemePartial,
26};
27
28#[derive(Default, Clone, PartialEq)]
29pub enum InputMode {
30 #[default]
31 Shown,
32 Hidden(char),
33}
34
35impl InputMode {
36 pub fn new_password() -> Self {
37 Self::Hidden('*')
38 }
39}
40
41#[derive(Debug, Default, PartialEq, Clone, Copy)]
42pub enum InputStatus {
43 #[default]
45 Idle,
46 Hovering,
48}
49
50#[derive(Clone)]
51pub struct InputValidator {
52 valid: Rc<RefCell<bool>>,
53 text: Rc<RefCell<String>>,
54}
55
56impl InputValidator {
57 pub fn new(text: String) -> Self {
58 Self {
59 valid: Rc::new(RefCell::new(true)),
60 text: Rc::new(RefCell::new(text)),
61 }
62 }
63 pub fn text(&'_ self) -> Ref<'_, String> {
64 self.text.borrow()
65 }
66 pub fn set_valid(&self, is_valid: bool) {
67 *self.valid.borrow_mut() = is_valid;
68 }
69 pub fn is_valid(&self) -> bool {
70 *self.valid.borrow()
71 }
72}
73
74#[cfg_attr(feature = "docs",
104 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png")
105)]
106#[derive(Clone, PartialEq)]
107pub struct Input {
108 pub(crate) theme: Option<InputThemePartial>,
109 value: Cow<'static, str>,
110 placeholder: Option<Cow<'static, str>>,
111 on_change: Option<EventHandler<String>>,
112 on_validate: Option<EventHandler<InputValidator>>,
113 mode: InputMode,
114 auto_focus: bool,
115 width: Size,
116 enabled: bool,
117 key: DiffKey,
118}
119
120impl KeyExt for Input {
121 fn write_key(&mut self) -> &mut DiffKey {
122 &mut self.key
123 }
124}
125
126impl Default for Input {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl Input {
133 pub fn new() -> Self {
134 Input {
135 theme: None,
136 value: Cow::default(),
137 placeholder: None,
138 on_change: None,
139 on_validate: None,
140 mode: InputMode::default(),
141 auto_focus: false,
142 width: Size::px(150.),
143 enabled: true,
144 key: DiffKey::default(),
145 }
146 }
147
148 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
149 self.enabled = enabled.into();
150 self
151 }
152
153 pub fn value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
154 self.value = value.into();
155 self
156 }
157
158 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
159 self.placeholder = Some(placeholder.into());
160 self
161 }
162
163 pub fn on_change(mut self, on_change: impl Into<EventHandler<String>>) -> Self {
164 self.on_change = Some(on_change.into());
165 self
166 }
167
168 pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
169 self.on_validate = Some(on_validate.into());
170 self
171 }
172
173 pub fn mode(mut self, mode: InputMode) -> Self {
174 self.mode = mode;
175 self
176 }
177
178 pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
179 self.auto_focus = auto_focus.into();
180 self
181 }
182
183 pub fn width(mut self, width: impl Into<Size>) -> Self {
184 self.width = width.into();
185 self
186 }
187
188 pub fn theme(mut self, theme: InputThemePartial) -> Self {
189 self.theme = Some(theme);
190 self
191 }
192
193 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
194 self.key = key.into();
195 self
196 }
197}
198
199impl Render for Input {
200 fn render(&self) -> impl IntoElement {
201 let focus = use_focus();
202 let focus_status = use_focus_status(focus);
203 let holder = use_state(ParagraphHolder::default);
204 let mut area = use_state(Area::default);
205 let mut status = use_state(InputStatus::default);
206 let mut editable = use_editable(|| self.value.to_string(), EditableConfig::new);
207 let mut is_dragging = use_state(|| false);
208 let mut ime_preedit = use_state(|| None);
209 let theme = get_theme!(&self.theme, input);
210 let (mut movement_timeout, cursor_color) =
211 use_cursor_blink(focus_status() != FocusStatus::Not, theme.color);
212
213 let enabled = use_reactive(&self.enabled);
214 use_drop(move || {
215 if status() == InputStatus::Hovering && enabled() {
216 Cursor::set(CursorIcon::default());
217 }
218 });
219
220 let display_placeholder = self.value.is_empty() && self.placeholder.is_some();
221 let on_change = self.on_change.clone();
222 let on_validate = self.on_validate.clone();
223
224 if &*self.value != editable.editor().read().rope() {
225 editable.editor_mut().write().set(&self.value);
226 editable.editor_mut().write().editor_history().clear();
227 }
228
229 let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
230 ime_preedit.set(Some(e.data().text.clone()));
231 };
232
233 let on_key_down = move |e: Event<KeyboardEventData>| {
234 if e.key != Key::Named(NamedKey::Enter) && e.key != Key::Named(NamedKey::Tab) {
235 e.stop_propagation();
236 movement_timeout.reset();
237 editable.process_event(EditableEvent::KeyDown {
238 key: &e.key,
239 modifiers: e.modifiers,
240 });
241 let text = editable.editor().peek().to_string();
242
243 let apply_change = if let Some(on_validate) = &on_validate {
244 let editor = editable.editor_mut();
245 let mut editor = editor.write();
246 let validator = InputValidator::new(text.clone());
247 on_validate.call(validator.clone());
248 let is_valid = validator.is_valid();
249
250 if !is_valid {
251 let undo_result = editor.undo();
253 if let Some(idx) = undo_result {
254 editor.move_cursor_to(idx);
255 }
256 editor.editor_history().clear_redos();
257 }
258
259 is_valid
260 } else {
261 true
262 };
263
264 if apply_change && let Some(on_change) = &on_change {
265 on_change.call(text);
266 }
267 }
268 };
269
270 let on_key_up = move |e: Event<KeyboardEventData>| {
271 e.stop_propagation();
272 editable.process_event(EditableEvent::KeyUp { key: &e.key });
273 };
274
275 let on_input_pointer_down = move |e: Event<PointerEventData>| {
276 e.stop_propagation();
277 is_dragging.set(true);
278 movement_timeout.reset();
279 if !display_placeholder {
280 let area = area.read().to_f64();
281 let global_location = e.global_location().clamp(area.min(), area.max());
282 let location = (global_location - area.min()).to_point();
283 editable.process_event(EditableEvent::Down {
284 location,
285 editor_line: EditorLine::SingleParagraph,
286 holder: &holder.read(),
287 });
288 }
289 focus.request_focus();
290 };
291
292 let on_pointer_down = move |e: Event<PointerEventData>| {
293 e.stop_propagation();
294 is_dragging.set(true);
295 movement_timeout.reset();
296 if !display_placeholder {
297 editable.process_event(EditableEvent::Down {
298 location: e.element_location(),
299 editor_line: EditorLine::SingleParagraph,
300 holder: &holder.read(),
301 });
302 }
303 focus.request_focus();
304 };
305
306 let on_global_mouse_move = move |e: Event<MouseEventData>| {
307 if focus.is_focused() && *is_dragging.read() {
308 let mut location = e.global_location;
309 location.x -= area.read().min_x() as f64;
310 location.y -= area.read().min_y() as f64;
311 editable.process_event(EditableEvent::Move {
312 location,
313 editor_line: EditorLine::SingleParagraph,
314 holder: &holder.read(),
315 });
316 }
317 };
318
319 let on_pointer_enter = move |_| {
320 *status.write() = InputStatus::Hovering;
321 if enabled() {
322 Cursor::set(CursorIcon::Text);
323 } else {
324 Cursor::set(CursorIcon::NotAllowed);
325 }
326 };
327
328 let on_pointer_leave = move |_| {
329 if status() == InputStatus::Hovering {
330 Cursor::set(CursorIcon::default());
331 *status.write() = InputStatus::default();
332 }
333 };
334
335 let on_global_mouse_up = move |_| {
336 match *status.read() {
337 InputStatus::Idle if focus.is_focused() => {
338 editable.process_event(EditableEvent::Release);
339 }
340 InputStatus::Hovering => {
341 editable.process_event(EditableEvent::Release);
342 }
343 _ => {}
344 };
345
346 if focus.is_focused() {
347 if *is_dragging.read() {
348 is_dragging.set(false);
350 } else {
351 focus.request_unfocus();
353 }
354 }
355 };
356
357 let a11y_id = focus.a11y_id();
358
359 let (background, cursor_index, text_selection) =
360 if enabled() && focus_status() != FocusStatus::Not {
361 (
362 theme.hover_background,
363 Some(editable.editor().read().cursor_pos()),
364 editable
365 .editor()
366 .read()
367 .get_visible_selection(EditorLine::SingleParagraph),
368 )
369 } else {
370 (theme.background, None, None)
371 };
372
373 let border = if focus_status() == FocusStatus::Keyboard {
374 Border::new()
375 .fill(theme.focus_border_fill)
376 .width(2.)
377 .alignment(BorderAlignment::Inner)
378 } else {
379 Border::new()
380 .fill(theme.border_fill.mul_if(!self.enabled, 0.85))
381 .width(1.)
382 .alignment(BorderAlignment::Inner)
383 };
384
385 let color = if display_placeholder {
386 theme.placeholder_color
387 } else {
388 theme.color
389 };
390
391 let text = match (self.mode.clone(), &self.placeholder) {
392 (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
393 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(self.value.len())),
394 (InputMode::Shown, _) => Cow::Borrowed(self.value.as_ref()),
395 };
396
397 let preedit_text = (!display_placeholder)
398 .then(|| ime_preedit.read().clone())
399 .flatten();
400
401 let a11_role = match self.mode {
402 InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
403 _ => AccessibilityRole::TextInput,
404 };
405
406 rect()
407 .a11y_id(a11y_id)
408 .a11y_focusable(self.enabled)
409 .a11y_auto_focus(self.auto_focus)
410 .a11y_alt(text.clone())
411 .a11y_role(a11_role)
412 .maybe(self.enabled, |rect| {
413 rect.on_key_up(on_key_up)
414 .on_key_down(on_key_down)
415 .on_pointer_down(on_input_pointer_down)
416 .on_ime_preedit(on_ime_preedit)
417 })
418 .on_pointer_enter(on_pointer_enter)
419 .on_pointer_leave(on_pointer_leave)
420 .width(self.width.clone())
421 .background(background.mul_if(!self.enabled, 0.85))
422 .border(border)
423 .corner_radius(theme.corner_radius)
424 .main_align(Alignment::center())
425 .cross_align(Alignment::center())
426 .child(
427 ScrollView::new()
428 .height(Size::Inner)
429 .direction(Direction::Horizontal)
430 .show_scrollbar(false)
431 .child(
432 paragraph()
433 .holder(holder.read().clone())
434 .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
435 .min_width(Size::func(move |context| {
436 Some(context.parent + theme.inner_margin.horizontal())
437 }))
438 .maybe(self.enabled, |rect| {
439 rect.on_pointer_down(on_pointer_down)
440 .on_global_mouse_up(on_global_mouse_up)
441 .on_global_mouse_move(on_global_mouse_move)
442 })
443 .margin(theme.inner_margin)
444 .cursor_index(cursor_index)
445 .cursor_color(cursor_color)
446 .color(color)
447 .max_lines(1)
448 .highlights(text_selection.map(|h| vec![h]))
449 .span(text.to_string())
450 .map(preedit_text, |el, preedit_text| el.span(preedit_text)),
451 ),
452 )
453 }
454
455 fn render_key(&self) -> DiffKey {
456 self.key.clone().or(self.default_key())
457 }
458}