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