Skip to main content

azul_layout/widgets/
text_input.rs

1//! Single-line text input widget with cursor, placeholder, and two-way data binding.
2//!
3//! The main entry point is [`TextInput`], which holds the editable state
4//! ([`TextInputState`]) together with per-platform default styles.  Call
5//! [`TextInput::dom()`] to obtain a renderable [`Dom`] node.
6//!
7//! For higher-level text-input management (IME, clipboard, undo) see
8//! `layout/src/managers/text_input.rs`.
9
10use alloc::{string::String, vec::Vec};
11
12use azul_core::{
13    callbacks::{CoreCallback, CoreCallbackData, Update},
14    dom::Dom,
15    refany::RefAny,
16    task::OptionTimerId,
17    window::VirtualKeyCode,
18};
19use azul_css::{
20    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
21    props::{
22        basic::*,
23        layout::*,
24        property::{CssProperty, *},
25        style::*,
26    },
27    *,
28};
29use azul_css::css::BoxOrStatic;
30
31use crate::callbacks::{Callback, CallbackInfo};
32
33const BACKGROUND_COLOR: ColorU = ColorU {
34    r: 255,
35    g: 255,
36    b: 255,
37    a: 255,
38}; // white
39const BLACK: ColorU = ColorU {
40    r: 0,
41    g: 0,
42    b: 0,
43    a: 255,
44};
45const TEXT_COLOR: StyleTextColor = StyleTextColor { inner: BLACK }; // black
46const COLOR_9B9B9B: ColorU = ColorU {
47    r: 155,
48    g: 155,
49    b: 155,
50    a: 255,
51}; // #9b9b9b
52const COLOR_4286F4: ColorU = ColorU {
53    r: 66,
54    g: 134,
55    b: 244,
56    a: 255,
57}; // #4286f4
58const COLOR_4C4C4C: ColorU = ColorU {
59    r: 76,
60    g: 76,
61    b: 76,
62    a: 255,
63}; // #4C4C4C
64
65const CURSOR_COLOR_BLACK: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(BLACK)];
66const CURSOR_COLOR: StyleBackgroundContentVec =
67    StyleBackgroundContentVec::from_const_slice(CURSOR_COLOR_BLACK);
68
69const BACKGROUND_THEME_LIGHT: &[StyleBackgroundContent] =
70    &[StyleBackgroundContent::Color(BACKGROUND_COLOR)];
71const BACKGROUND_COLOR_LIGHT: StyleBackgroundContentVec =
72    StyleBackgroundContentVec::from_const_slice(BACKGROUND_THEME_LIGHT);
73
74const SANS_SERIF_STR: &str = "system:ui";
75const SANS_SERIF: AzString = AzString::from_const_str(SANS_SERIF_STR);
76const SANS_SERIF_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SANS_SERIF)];
77const SANS_SERIF_FAMILY: StyleFontFamilyVec =
78    StyleFontFamilyVec::from_const_slice(SANS_SERIF_FAMILIES);
79
80// -- cursor style
81
82const TEXT_CURSOR_TRANSFORM: &[StyleTransform] =
83    &[StyleTransform::Translate(StyleTransformTranslate2D {
84        x: PixelValue::const_px(0),
85        y: PixelValue::const_px(2),
86    })];
87
88static TEXT_CURSOR_PROPS: &[CssPropertyWithConditions] = &[
89    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
90    CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(1))),
91    CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(11))),
92    CssPropertyWithConditions::simple(CssProperty::const_background_content(CURSOR_COLOR)),
93    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
94    CssPropertyWithConditions::simple(CssProperty::const_transform(
95        StyleTransformVec::from_const_slice(TEXT_CURSOR_TRANSFORM),
96    )),
97];
98
99// -- container style
100
101#[cfg(target_os = "windows")]
102static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
103    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
104    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
105    CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
106    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
107    CssPropertyWithConditions::simple(CssProperty::const_background_content(
108        BACKGROUND_COLOR_LIGHT,
109    )),
110    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
111        inner: COLOR_4C4C4C,
112    })),
113    CssPropertyWithConditions::simple(CssProperty::const_padding_left(
114        LayoutPaddingLeft::const_px(2),
115    )),
116    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
117        LayoutPaddingRight::const_px(2),
118    )),
119    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
120        1,
121    ))),
122    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
123        LayoutPaddingBottom::const_px(1),
124    )),
125    // border: 1px solid #484c52;
126    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
127        LayoutBorderTopWidth::const_px(1),
128    )),
129    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
130        LayoutBorderBottomWidth::const_px(1),
131    )),
132    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
133        LayoutBorderLeftWidth::const_px(1),
134    )),
135    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
136        LayoutBorderRightWidth::const_px(1),
137    )),
138    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
139        inner: BorderStyle::Inset,
140    })),
141    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
142        StyleBorderBottomStyle {
143            inner: BorderStyle::Inset,
144        },
145    )),
146    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
147        inner: BorderStyle::Inset,
148    })),
149    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
150        StyleBorderRightStyle {
151            inner: BorderStyle::Inset,
152        },
153    )),
154    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
155        inner: COLOR_9B9B9B,
156    })),
157    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
158        StyleBorderBottomColor {
159            inner: COLOR_9B9B9B,
160        },
161    )),
162    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
163        inner: COLOR_9B9B9B,
164    })),
165    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
166        StyleBorderRightColor {
167            inner: COLOR_9B9B9B,
168        },
169    )),
170    CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
171    CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
172    CssPropertyWithConditions::simple(CssProperty::const_justify_content(
173        LayoutJustifyContent::Center,
174    )),
175    // Hover(border-color: #4c4c4c;)
176    CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
177        inner: COLOR_4C4C4C,
178    })),
179    CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
180        StyleBorderBottomColor {
181            inner: COLOR_4C4C4C,
182        },
183    )),
184    CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
185        StyleBorderLeftColor {
186            inner: COLOR_4C4C4C,
187        },
188    )),
189    CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
190        StyleBorderRightColor {
191            inner: COLOR_4C4C4C,
192        },
193    )),
194    // Focus(border-color: #4286f4;)
195    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
196        inner: COLOR_4286F4,
197    })),
198    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
199        StyleBorderBottomColor {
200            inner: COLOR_4286F4,
201        },
202    )),
203    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
204        StyleBorderLeftColor {
205            inner: COLOR_4286F4,
206        },
207    )),
208    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
209        StyleBorderRightColor {
210            inner: COLOR_4286F4,
211        },
212    )),
213];
214
215#[cfg(target_os = "linux")]
216static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
217    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
218    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
219    CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
220    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
221    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
222    CssPropertyWithConditions::simple(CssProperty::const_background_content(
223        BACKGROUND_COLOR_LIGHT,
224    )),
225    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
226        inner: COLOR_4C4C4C,
227    })),
228    CssPropertyWithConditions::simple(CssProperty::const_padding_left(
229        LayoutPaddingLeft::const_px(2),
230    )),
231    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
232        LayoutPaddingRight::const_px(2),
233    )),
234    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
235        1,
236    ))),
237    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
238        LayoutPaddingBottom::const_px(1),
239    )),
240    // border: 1px solid #484c52;
241    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
242        LayoutBorderTopWidth::const_px(1),
243    )),
244    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
245        LayoutBorderBottomWidth::const_px(1),
246    )),
247    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
248        LayoutBorderLeftWidth::const_px(1),
249    )),
250    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
251        LayoutBorderRightWidth::const_px(1),
252    )),
253    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
254        inner: BorderStyle::Inset,
255    })),
256    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
257        StyleBorderBottomStyle {
258            inner: BorderStyle::Inset,
259        },
260    )),
261    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
262        inner: BorderStyle::Inset,
263    })),
264    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
265        StyleBorderRightStyle {
266            inner: BorderStyle::Inset,
267        },
268    )),
269    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
270        inner: COLOR_9B9B9B,
271    })),
272    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
273        StyleBorderBottomColor {
274            inner: COLOR_9B9B9B,
275        },
276    )),
277    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
278        inner: COLOR_9B9B9B,
279    })),
280    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
281        StyleBorderRightColor {
282            inner: COLOR_9B9B9B,
283        },
284    )),
285    CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
286    CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
287    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
288    CssPropertyWithConditions::simple(CssProperty::const_justify_content(
289        LayoutJustifyContent::Center,
290    )),
291    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
292    // Hover(border-color: #4286f4;)
293    CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
294        inner: COLOR_4286F4,
295    })),
296    CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
297        StyleBorderBottomColor {
298            inner: COLOR_4286F4,
299        },
300    )),
301    CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
302        StyleBorderLeftColor {
303            inner: COLOR_4286F4,
304        },
305    )),
306    CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
307        StyleBorderRightColor {
308            inner: COLOR_4286F4,
309        },
310    )),
311    // Focus(border-color: #4286f4;)
312    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
313        inner: COLOR_4286F4,
314    })),
315    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
316        StyleBorderBottomColor {
317            inner: COLOR_4286F4,
318        },
319    )),
320    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
321        StyleBorderLeftColor {
322            inner: COLOR_4286F4,
323        },
324    )),
325    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
326        StyleBorderRightColor {
327            inner: COLOR_4286F4,
328        },
329    )),
330];
331
332// Mobile (Android / iOS) inherit the macOS-style container — same flex
333// box-sizing and background; touch-target padding is the user's concern.
334#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
335static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
336    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
337    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
338    CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
339    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
340    CssPropertyWithConditions::simple(CssProperty::const_background_content(
341        BACKGROUND_COLOR_LIGHT,
342    )),
343    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
344        inner: COLOR_4C4C4C,
345    })),
346    CssPropertyWithConditions::simple(CssProperty::const_padding_left(
347        LayoutPaddingLeft::const_px(2),
348    )),
349    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
350        LayoutPaddingRight::const_px(2),
351    )),
352    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
353        1,
354    ))),
355    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
356        LayoutPaddingBottom::const_px(1),
357    )),
358    // border: 1px solid #484c52;
359    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
360        LayoutBorderTopWidth::const_px(1),
361    )),
362    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
363        LayoutBorderBottomWidth::const_px(1),
364    )),
365    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
366        LayoutBorderLeftWidth::const_px(1),
367    )),
368    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
369        LayoutBorderRightWidth::const_px(1),
370    )),
371    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
372        inner: BorderStyle::Inset,
373    })),
374    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
375        StyleBorderBottomStyle {
376            inner: BorderStyle::Inset,
377        },
378    )),
379    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
380        inner: BorderStyle::Inset,
381    })),
382    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
383        StyleBorderRightStyle {
384            inner: BorderStyle::Inset,
385        },
386    )),
387    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
388        inner: COLOR_9B9B9B,
389    })),
390    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
391        StyleBorderBottomColor {
392            inner: COLOR_9B9B9B,
393        },
394    )),
395    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
396        inner: COLOR_9B9B9B,
397    })),
398    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
399        StyleBorderRightColor {
400            inner: COLOR_9B9B9B,
401        },
402    )),
403    CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
404    CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
405    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
406    CssPropertyWithConditions::simple(CssProperty::const_justify_content(
407        LayoutJustifyContent::Center,
408    )),
409    // Hover(border-color: #4286f4;)
410    CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
411        inner: COLOR_4286F4,
412    })),
413    CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
414        StyleBorderBottomColor {
415            inner: COLOR_4286F4,
416        },
417    )),
418    CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
419        StyleBorderLeftColor {
420            inner: COLOR_4286F4,
421        },
422    )),
423    CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
424        StyleBorderRightColor {
425            inner: COLOR_4286F4,
426        },
427    )),
428    // Focus(border-color: #4286f4;)
429    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
430        inner: COLOR_4286F4,
431    })),
432    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
433        StyleBorderBottomColor {
434            inner: COLOR_4286F4,
435        },
436    )),
437    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
438        StyleBorderLeftColor {
439            inner: COLOR_4286F4,
440        },
441    )),
442    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
443        StyleBorderRightColor {
444            inner: COLOR_4286F4,
445        },
446    )),
447];
448
449// -- label style
450
451#[cfg(target_os = "windows")]
452static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
453    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
454    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
455    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
456    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
457    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
458        inner: COLOR_4C4C4C,
459    })),
460    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
461];
462
463#[cfg(target_os = "linux")]
464static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
465    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
466    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
467    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
468    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
469    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
470        inner: COLOR_4C4C4C,
471    })),
472    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
473];
474
475#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
476static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
477    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
478    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
479    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
480    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
481    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
482        inner: COLOR_4C4C4C,
483    })),
484    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
485];
486
487// --- placeholder
488
489#[cfg(target_os = "windows")]
490static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
491    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
492    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
493    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
494    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
495    CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
496    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
497    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
498        inner: COLOR_4C4C4C,
499    })),
500    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
501    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
502];
503
504#[cfg(target_os = "linux")]
505static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
506    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
507    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
508    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
509    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
510    CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
511    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
512    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
513        inner: COLOR_4C4C4C,
514    })),
515    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
516    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
517];
518
519#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
520static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
521    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
522    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
523    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
524    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
525    CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
526    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
527    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
528        inner: COLOR_4C4C4C,
529    })),
530    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
531    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
532];
533
534/// Single-line text input widget with platform-native styling.
535///
536/// Use [`TextInput::create()`] to build an instance, configure it with the
537/// `with_*` / `set_*` builder methods, and call [`TextInput::dom()`] to
538/// obtain a renderable DOM tree.
539#[derive(Debug, Clone, PartialEq)]
540#[repr(C)]
541pub struct TextInput {
542    pub text_input_state: TextInputStateWrapper,
543    pub placeholder_style: CssPropertyWithConditionsVec,
544    pub container_style: CssPropertyWithConditionsVec,
545    pub label_style: CssPropertyWithConditionsVec,
546}
547
548/// Editable state of a text input (text buffer, cursor position, selection).
549#[derive(Debug, Clone, PartialEq)]
550#[repr(C)]
551pub struct TextInputState {
552    pub text: U32Vec, // Vec<char>
553    pub placeholder: OptionString,
554    pub max_len: usize,
555    pub selection: OptionTextInputSelection,
556    pub cursor_pos: usize,
557}
558
559/// [`TextInputState`] together with optional user callbacks and cursor animation state.
560#[derive(Debug, Clone, PartialEq)]
561#[repr(C)]
562pub struct TextInputStateWrapper {
563    pub inner: TextInputState,
564    pub on_text_input: OptionTextInputOnTextInput,
565    pub on_virtual_key_down: OptionTextInputOnVirtualKeyDown,
566    pub on_focus_lost: OptionTextInputOnFocusLost,
567    pub update_text_input_before_calling_focus_lost_fn: bool,
568    pub update_text_input_before_calling_vk_down_fn: bool,
569    pub cursor_animation: OptionTimerId,
570}
571
572/// Return value from a text-input callback indicating whether the framework
573/// should update and whether the input was valid.
574#[derive(Debug, Copy, Clone, PartialEq)]
575#[repr(C)]
576pub struct OnTextInputReturn {
577    pub update: Update,
578    pub valid: TextInputValid,
579}
580
581/// Whether the text input accepted or rejected the most recent edit.
582#[derive(Debug, Copy, Clone, PartialEq)]
583#[repr(C)]
584pub enum TextInputValid {
585    Yes,
586    No,
587}
588
589// The text input field has a special return which specifies
590// whether the text input should handle the character
591pub type TextInputOnTextInputCallbackType =
592    extern "C" fn(RefAny, CallbackInfo, TextInputState) -> OnTextInputReturn;
593impl_widget_callback!(
594    TextInputOnTextInput,
595    OptionTextInputOnTextInput,
596    TextInputOnTextInputCallback,
597    TextInputOnTextInputCallbackType
598);
599
600azul_core::impl_managed_callback! {
601    wrapper:        TextInputOnTextInputCallback,
602    info_ty:        CallbackInfo,
603    return_ty:      OnTextInputReturn,
604    default_ret:    OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
605    invoker_static: TEXT_INPUT_ON_TEXT_INPUT_INVOKER,
606    invoker_ty:     AzTextInputOnTextInputCallbackInvoker,
607    thunk_fn:       az_text_input_on_text_input_callback_thunk,
608    setter_fn:      AzApp_setTextInputOnTextInputCallbackInvoker,
609    from_handle_fn: AzTextInputOnTextInputCallback_createFromHostHandle,
610    extra_args:     [ state: TextInputState ],
611}
612
613pub type TextInputOnVirtualKeyDownCallbackType =
614    extern "C" fn(RefAny, CallbackInfo, TextInputState) -> OnTextInputReturn;
615impl_widget_callback!(
616    TextInputOnVirtualKeyDown,
617    OptionTextInputOnVirtualKeyDown,
618    TextInputOnVirtualKeyDownCallback,
619    TextInputOnVirtualKeyDownCallbackType
620);
621
622azul_core::impl_managed_callback! {
623    wrapper:        TextInputOnVirtualKeyDownCallback,
624    info_ty:        CallbackInfo,
625    return_ty:      OnTextInputReturn,
626    default_ret:    OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
627    invoker_static: TEXT_INPUT_ON_VIRTUAL_KEY_DOWN_INVOKER,
628    invoker_ty:     AzTextInputOnVirtualKeyDownCallbackInvoker,
629    thunk_fn:       az_text_input_on_virtual_key_down_callback_thunk,
630    setter_fn:      AzApp_setTextInputOnVirtualKeyDownCallbackInvoker,
631    from_handle_fn: AzTextInputOnVirtualKeyDownCallback_createFromHostHandle,
632    extra_args:     [ state: TextInputState ],
633}
634
635pub type TextInputOnFocusLostCallbackType =
636    extern "C" fn(RefAny, CallbackInfo, TextInputState) -> Update;
637impl_widget_callback!(
638    TextInputOnFocusLost,
639    OptionTextInputOnFocusLost,
640    TextInputOnFocusLostCallback,
641    TextInputOnFocusLostCallbackType
642);
643
644azul_core::impl_managed_callback! {
645    wrapper:        TextInputOnFocusLostCallback,
646    info_ty:        CallbackInfo,
647    return_ty:      Update,
648    default_ret:    Update::DoNothing,
649    invoker_static: TEXT_INPUT_ON_FOCUS_LOST_INVOKER,
650    invoker_ty:     AzTextInputOnFocusLostCallbackInvoker,
651    thunk_fn:       az_text_input_on_focus_lost_callback_thunk,
652    setter_fn:      AzApp_setTextInputOnFocusLostCallbackInvoker,
653    from_handle_fn: AzTextInputOnFocusLostCallback_createFromHostHandle,
654    extra_args:     [ state: TextInputState ],
655}
656
657#[derive(Debug, Clone, Hash, PartialEq, Eq)]
658#[repr(C, u8)]
659pub enum TextInputSelection {
660    All,
661    FromTo(TextInputSelectionRange),
662}
663
664azul_css::impl_option!(
665    TextInputSelection,
666    OptionTextInputSelection,
667    copy = false,
668    [Debug, Clone, Hash, PartialEq, Eq]
669);
670
671#[derive(Debug, Clone, Hash, PartialEq, Eq)]
672#[repr(C)]
673pub struct TextInputSelectionRange {
674    pub dir_from: usize,
675    pub dir_to: usize,
676}
677
678impl Default for TextInput {
679    fn default() -> Self {
680        TextInput {
681            text_input_state: TextInputStateWrapper::default(),
682            placeholder_style: CssPropertyWithConditionsVec::from_const_slice(
683                TEXT_INPUT_PLACEHOLDER_PROPS,
684            ),
685            container_style: CssPropertyWithConditionsVec::from_const_slice(
686                TEXT_INPUT_CONTAINER_PROPS,
687            ),
688            label_style: CssPropertyWithConditionsVec::from_const_slice(TEXT_INPUT_LABEL_PROPS),
689        }
690    }
691}
692
693impl Default for TextInputState {
694    fn default() -> Self {
695        TextInputState {
696            text: Vec::new().into(),
697            placeholder: None.into(),
698            max_len: 50,
699            selection: None.into(),
700            cursor_pos: 0,
701        }
702    }
703}
704
705impl TextInputState {
706    pub fn get_text(&self) -> String {
707        self.text
708            .iter()
709            .filter_map(|c| core::char::from_u32(*c))
710            .collect()
711    }
712}
713
714impl Default for TextInputStateWrapper {
715    fn default() -> Self {
716        TextInputStateWrapper {
717            inner: TextInputState::default(),
718            on_text_input: None.into(),
719            on_virtual_key_down: None.into(),
720            on_focus_lost: None.into(),
721            update_text_input_before_calling_focus_lost_fn: true,
722            update_text_input_before_calling_vk_down_fn: true,
723            cursor_animation: None.into(),
724        }
725    }
726}
727
728impl TextInput {
729    pub fn create() -> Self {
730        Self::default()
731    }
732
733    pub fn with_text(mut self, text: AzString) -> Self {
734        self.set_text(text);
735        self
736    }
737
738    pub fn set_text(&mut self, text: AzString) {
739        self.text_input_state.inner.text = text
740            .as_str()
741            .chars()
742            .map(|c| c as u32)
743            .collect::<Vec<_>>()
744            .into();
745    }
746
747    pub fn set_placeholder(&mut self, placeholder: AzString) {
748        self.text_input_state.inner.placeholder = Some(placeholder).into();
749    }
750
751    pub fn with_placeholder(mut self, placeholder: AzString) -> Self {
752        self.set_placeholder(placeholder);
753        self
754    }
755
756    pub fn set_on_text_input<C: Into<TextInputOnTextInputCallback>>(
757        &mut self,
758        refany: RefAny,
759        callback: C,
760    ) {
761        self.text_input_state.on_text_input = Some(TextInputOnTextInput {
762            callback: callback.into(),
763            refany,
764        })
765        .into();
766    }
767
768    pub fn with_on_text_input<C: Into<TextInputOnTextInputCallback>>(
769        mut self,
770        refany: RefAny,
771        callback: C,
772    ) -> Self {
773        self.set_on_text_input(refany, callback);
774        self
775    }
776
777    pub fn set_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
778        &mut self,
779        refany: RefAny,
780        callback: C,
781    ) {
782        self.text_input_state.on_virtual_key_down = Some(TextInputOnVirtualKeyDown {
783            callback: callback.into(),
784            refany,
785        })
786        .into();
787    }
788
789    pub fn with_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
790        mut self,
791        refany: RefAny,
792        callback: C,
793    ) -> Self {
794        self.set_on_virtual_key_down(refany, callback);
795        self
796    }
797
798    pub fn set_on_focus_lost<C: Into<TextInputOnFocusLostCallback>>(
799        &mut self,
800        refany: RefAny,
801        callback: C,
802    ) {
803        self.text_input_state.on_focus_lost = Some(TextInputOnFocusLost {
804            callback: callback.into(),
805            refany,
806        })
807        .into();
808    }
809
810    pub fn with_on_focus_lost<C: Into<TextInputOnFocusLostCallback>>(
811        mut self,
812        refany: RefAny,
813        callback: C,
814    ) -> Self {
815        self.set_on_focus_lost(refany, callback);
816        self
817    }
818
819    pub fn set_placeholder_style(&mut self, style: CssPropertyWithConditionsVec) {
820        self.placeholder_style = style;
821    }
822
823    pub fn with_placeholder_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
824        self.set_placeholder_style(style);
825        self
826    }
827
828    pub fn set_container_style(&mut self, style: CssPropertyWithConditionsVec) {
829        self.container_style = style;
830    }
831
832    pub fn with_container_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
833        self.set_container_style(style);
834        self
835    }
836
837    pub fn set_label_style(&mut self, style: CssPropertyWithConditionsVec) {
838        self.label_style = style;
839    }
840
841    pub fn with_label_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
842        self.set_label_style(style);
843        self
844    }
845
846    pub fn swap_with_default(&mut self) -> Self {
847        let mut s = Self::default();
848        core::mem::swap(&mut s, self);
849        s
850    }
851
852    pub fn dom(mut self) -> Dom {
853        use azul_core::{
854            callbacks::CoreCallbackData,
855            dom::{EventFilter, FocusEventFilter, HoverEventFilter, IdOrClass::Class, TabIndex},
856        };
857
858        self.text_input_state.inner.cursor_pos = self.text_input_state.inner.text.len();
859
860        let label_text: String = self
861            .text_input_state
862            .inner
863            .text
864            .iter()
865            .filter_map(|s| core::char::from_u32(*s))
866            .collect();
867
868        let placeholder = self
869            .text_input_state
870            .inner
871            .placeholder
872            .as_ref()
873            .map(|s| s.as_str().to_string())
874            .unwrap_or_default();
875
876        let state_ref = RefAny::new(self.text_input_state);
877
878        Dom::create_div()
879            .with_ids_and_classes(vec![Class("__azul-native-text-input-container".into())].into())
880            .with_css_props(self.container_style)
881            .with_tab_index(TabIndex::Auto)
882            .with_dataset(Some(state_ref.clone()).into())
883            .with_callbacks(
884                vec![
885                    CoreCallbackData {
886                        event: EventFilter::Focus(FocusEventFilter::FocusReceived),
887                        refany: state_ref.clone(),
888                        callback: CoreCallback {
889                            cb: default_on_focus_received as usize,
890                            ctx: azul_core::refany::OptionRefAny::None,
891                        },
892                    },
893                    CoreCallbackData {
894                        event: EventFilter::Focus(FocusEventFilter::FocusLost),
895                        refany: state_ref.clone(),
896                        callback: CoreCallback {
897                            cb: default_on_focus_lost as usize,
898                            ctx: azul_core::refany::OptionRefAny::None,
899                        },
900                    },
901                    CoreCallbackData {
902                        event: EventFilter::Focus(FocusEventFilter::TextInput),
903                        refany: state_ref.clone(),
904                        callback: CoreCallback {
905                            cb: default_on_text_input as usize,
906                            ctx: azul_core::refany::OptionRefAny::None,
907                        },
908                    },
909                    CoreCallbackData {
910                        event: EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
911                        refany: state_ref.clone(),
912                        callback: CoreCallback {
913                            cb: default_on_virtual_key_down as usize,
914                            ctx: azul_core::refany::OptionRefAny::None,
915                        },
916                    },
917                    CoreCallbackData {
918                        event: EventFilter::Hover(HoverEventFilter::MouseOver),
919                        refany: state_ref.clone(),
920                        callback: CoreCallback {
921                            cb: default_on_mouse_hover as usize,
922                            ctx: azul_core::refany::OptionRefAny::None,
923                        },
924                    },
925                ]
926                .into(),
927            )
928            .with_children(
929                vec![
930                    Dom::create_text(placeholder)
931                        .with_ids_and_classes(
932                            vec![Class("__azul-native-text-input-placeholder".into())].into(),
933                        )
934                        .with_css_props(self.placeholder_style),
935                    Dom::create_text(label_text)
936                        .with_ids_and_classes(
937                            vec![Class("__azul-native-text-input-label".into())].into(),
938                        )
939                        .with_css_props(self.label_style)
940                        .with_children(
941                            vec![Dom::create_div()
942                                .with_ids_and_classes(
943                                    vec![Class("__azul-native-text-input-cursor".into())].into(),
944                                )
945                                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
946                                    TEXT_CURSOR_PROPS,
947                                ))]
948                            .into(),
949                        ),
950                ]
951                .into(),
952            )
953    }
954}
955
956extern "C" fn default_on_focus_received(mut text_input: RefAny, mut info: CallbackInfo) -> Update {
957    let mut text_input = match text_input.downcast_mut::<TextInputStateWrapper>() {
958        Some(s) => s,
959        None => return Update::DoNothing,
960    };
961
962    let text_input = &mut *text_input;
963
964    let placeholder_text_node_id = match info.get_first_child(info.get_hit_node()) {
965        Some(s) => s,
966        None => return Update::DoNothing,
967    };
968
969    // hide the placeholder text
970    if text_input.inner.text.is_empty() {
971        info.set_css_property(
972            placeholder_text_node_id,
973            CssProperty::const_opacity(StyleOpacity::const_new(0)),
974        );
975    }
976
977    text_input.inner.cursor_pos = text_input.inner.text.len();
978
979    Update::DoNothing
980}
981
982extern "C" fn default_on_focus_lost(mut text_input: RefAny, mut info: CallbackInfo) -> Update {
983    let mut text_input = match text_input.downcast_mut::<TextInputStateWrapper>() {
984        Some(s) => s,
985        None => return Update::DoNothing,
986    };
987
988    let text_input = &mut *text_input;
989
990    let placeholder_text_node_id = match info.get_first_child(info.get_hit_node()) {
991        Some(s) => s,
992        None => return Update::DoNothing,
993    };
994
995    // show the placeholder text
996    if text_input.inner.text.is_empty() {
997        info.set_css_property(
998            placeholder_text_node_id,
999            CssProperty::const_opacity(StyleOpacity::const_new(100)),
1000        );
1001    }
1002
1003    // rustc doesn't understand the borrowing lifetime here
1004    let text_input = &mut *text_input;
1005    let onfocuslost = &mut text_input.on_focus_lost;
1006    let inner = text_input.inner.clone();
1007
1008    match onfocuslost.as_mut() {
1009        Some(TextInputOnFocusLost { callback, refany }) => {
1010            (callback.cb)(refany.clone(), info.clone(), inner)
1011        }
1012        None => Update::DoNothing,
1013    }
1014}
1015
1016extern "C" fn default_on_text_input(text_input: RefAny, info: CallbackInfo) -> Update {
1017    default_on_text_input_inner(text_input, info).unwrap_or(Update::DoNothing)
1018}
1019
1020fn default_on_text_input_inner(mut text_input: RefAny, mut info: CallbackInfo) -> Option<Update> {
1021    let mut text_input = text_input.downcast_mut::<TextInputStateWrapper>()?;
1022
1023    // Get the text changeset (replaces old keyboard_state.current_char API)
1024    let changeset = info.get_text_changeset()?;
1025    let inserted_text = changeset.inserted_text.as_str().to_string();
1026
1027    // Early return if no text to insert
1028    if inserted_text.is_empty() {
1029        return None;
1030    }
1031
1032    let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
1033    let label_node_id = info.get_next_sibling(placeholder_node_id)?;
1034    let _cursor_node_id = info.get_first_child(label_node_id)?;
1035
1036    let result = {
1037        // rustc doesn't understand the borrowing lifetime here
1038        let text_input = &mut *text_input;
1039        let ontextinput = &mut text_input.on_text_input;
1040
1041        // inner_clone has the new text
1042        let mut inner_clone = text_input.inner.clone();
1043        inner_clone.cursor_pos = inner_clone.cursor_pos.saturating_add(inserted_text.len());
1044        inner_clone.text = {
1045            let mut internal = inner_clone.text.clone().into_library_owned_vec();
1046            internal.extend(inserted_text.chars().map(|c| c as u32));
1047            internal.into()
1048        };
1049
1050        match ontextinput.as_mut() {
1051            Some(TextInputOnTextInput { callback, refany }) => {
1052                (callback.cb)(refany.clone(), info.clone(), inner_clone)
1053            }
1054            None => OnTextInputReturn {
1055                update: Update::DoNothing,
1056                valid: TextInputValid::Yes,
1057            },
1058        }
1059    };
1060
1061    if result.valid == TextInputValid::Yes {
1062        // hide the placeholder text
1063        info.set_css_property(
1064            placeholder_node_id,
1065            CssProperty::const_opacity(StyleOpacity::const_new(0)),
1066        );
1067
1068        // append to the text
1069        text_input.inner.text = {
1070            let mut internal = text_input.inner.text.clone().into_library_owned_vec();
1071            internal.extend(inserted_text.chars().map(|c| c as u32));
1072            internal.into()
1073        };
1074        text_input.inner.cursor_pos = text_input
1075            .inner
1076            .cursor_pos
1077            .saturating_add(inserted_text.len());
1078
1079        info.change_node_text(label_node_id, text_input.inner.get_text().into());
1080    }
1081
1082    Some(result.update)
1083}
1084
1085extern "C" fn default_on_virtual_key_down(text_input: RefAny, info: CallbackInfo) -> Update {
1086    default_on_virtual_key_down_inner(text_input, info).unwrap_or(Update::DoNothing)
1087}
1088
1089fn default_on_virtual_key_down_inner(
1090    mut text_input: RefAny,
1091    mut info: CallbackInfo,
1092) -> Option<Update> {
1093    let mut text_input = text_input.downcast_mut::<TextInputStateWrapper>()?;
1094    let keyboard_state = info.get_current_keyboard_state();
1095
1096    let c = keyboard_state.current_virtual_keycode.into_option()?;
1097    let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
1098    let label_node_id = info.get_next_sibling(placeholder_node_id)?;
1099    let _cursor_node_id = info.get_first_child(label_node_id)?;
1100
1101    if c != VirtualKeyCode::Back {
1102        return None;
1103    }
1104
1105    text_input.inner.text = {
1106        let mut internal = text_input.inner.text.clone().into_library_owned_vec();
1107        internal.pop();
1108        internal.into()
1109    };
1110    text_input.inner.cursor_pos = text_input.inner.cursor_pos.saturating_sub(1);
1111
1112    info.change_node_text(label_node_id, text_input.inner.get_text().into());
1113
1114    None
1115}
1116
1117extern "C" fn default_on_mouse_hover(mut text_input: RefAny, _info: CallbackInfo) -> Update {
1118    let _text_input = match text_input.downcast_mut::<TextInputStateWrapper>() {
1119        Some(s) => s,
1120        None => return Update::DoNothing,
1121    };
1122
1123    Update::DoNothing
1124}