Another project
1use core::fmt::Display;
2use core::marker::PhantomData;
3
4use crate::frame::FrameCtx;
5use crate::input::NamedKey;
6use crate::layout::LayoutRect;
7use crate::strings::StringKey;
8use crate::widget_id::WidgetId;
9
10use super::keys::{TakeKey, take_key};
11use super::paint::WidgetPaint;
12use super::text_input::{
13 Clipboard, TextInput, TextInputResponse, TextInputState, TextInputValidation, show_text_input,
14};
15
16pub trait ParsedValue: Sized + Clone + PartialEq {
17 type Error: Display + Clone + PartialEq;
18
19 fn parse(text: &str) -> Result<Self, Self::Error>;
20}
21
22pub struct ParsedInput<'state, T: ParsedValue> {
23 pub id: WidgetId,
24 pub rect: LayoutRect,
25 pub placeholder: StringKey,
26 pub state: &'state mut TextInputState,
27 pub disabled: bool,
28 _ty: PhantomData<T>,
29}
30
31impl<'state, T: ParsedValue> ParsedInput<'state, T> {
32 #[must_use]
33 pub fn new(
34 id: WidgetId,
35 rect: LayoutRect,
36 placeholder: StringKey,
37 state: &'state mut TextInputState,
38 ) -> Self {
39 Self {
40 id,
41 rect,
42 placeholder,
43 state,
44 disabled: false,
45 _ty: PhantomData,
46 }
47 }
48
49 #[must_use]
50 pub fn disabled(self, disabled: bool) -> Self {
51 Self { disabled, ..self }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq)]
56pub struct ParsedInputResponse<T: ParsedValue> {
57 pub interaction: crate::hit_test::Interaction,
58 pub value: Option<T>,
59 pub committed: Option<T>,
60 pub paint: Vec<WidgetPaint>,
61 pub error: Option<T::Error>,
62}
63
64struct ParsedValidator<T: ParsedValue>(PhantomData<T>);
65
66impl<T: ParsedValue> TextInputValidation for ParsedValidator<T> {
67 type Error = T::Error;
68 fn validate(&self, text: &str) -> Result<(), Self::Error> {
69 if text.trim().is_empty() {
70 return Ok(());
71 }
72 T::parse(text).map(drop)
73 }
74}
75
76#[must_use]
77pub fn show_parsed_input<T: ParsedValue, C: Clipboard + ?Sized>(
78 ctx: &mut FrameCtx<'_>,
79 input: ParsedInput<'_, T>,
80 clipboard: &mut C,
81) -> ParsedInputResponse<T> {
82 let ParsedInput {
83 id,
84 rect,
85 placeholder,
86 state,
87 disabled,
88 ..
89 } = input;
90 let was_focused = state.was_focused;
91 let live_focused = ctx.is_focused(id);
92 let commit_via_enter = !disabled
93 && live_focused
94 && take_key(ctx.input, &[TakeKey::named(NamedKey::Enter)]).is_some();
95 let TextInputResponse {
96 interaction,
97 error,
98 paint,
99 ..
100 } = {
101 let widget = TextInput {
102 id,
103 rect,
104 placeholder,
105 state: &mut *state,
106 disabled,
107 validator: ParsedValidator::<T>(PhantomData),
108 };
109 show_text_input(ctx, widget, clipboard)
110 };
111 let value = if state.text.trim().is_empty() {
112 None
113 } else {
114 T::parse(&state.text).ok()
115 };
116 let lost_focus = was_focused && !live_focused;
117 let committed = if commit_via_enter || lost_focus {
118 value.clone()
119 } else {
120 None
121 };
122 ParsedInputResponse {
123 interaction,
124 value,
125 committed,
126 paint,
127 error,
128 }
129}