Skip to main content

azul_layout/widgets/
text_area.rs

1//! Multi-line text input (text area) widget.
2//!
3//! A multi-line sibling of [`crate::widgets::text_input::TextInput`]: it reuses
4//! the same editable-state / cursor / focus-callback machinery but stores and
5//! renders multiple lines. Pressing Return/Enter inserts a newline; Backspace
6//! deletes the last character; typed/pasted text (including embedded newlines)
7//! is appended. The buffer is a `Vec<char>` (as `U32Vec`) exactly like
8//! `TextInput`, so the `'\n'` characters round-trip through
9//! [`TextAreaState::get_text`].
10//!
11//! The widget reuses [`TextInput`]'s [`OnTextInputReturn`] / [`TextInputValid`]
12//! return types for its `on_text_input` callback so existing host bindings and
13//! validation logic apply unchanged.
14//!
15//! TODO2: this implements the *core* of multi-line editing — multi-line value,
16//! newline insertion, append/backspace, `on_text_input` (a.k.a. on_change) and
17//! `on_focus_lost`. Advanced editing is intentionally NOT implemented and is
18//! not verifiable without a live window: the blinking cursor is a static child
19//! and does not track the caret across lines, there is no selection/range
20//! editing, no mid-buffer insertion (edits append/truncate at the end), and no
21//! vertical (up/down) caret navigation. Line wrapping relies on the text
22//! layout honouring `white-space: pre-wrap`.
23//!
24//! Key types: [`TextArea`], [`TextAreaState`], [`TextAreaOnTextInput`],
25//! [`TextAreaOnVirtualKeyDown`], [`TextAreaOnFocusLost`].
26
27use alloc::{string::String, vec::Vec};
28
29use azul_core::{
30    callbacks::{CoreCallback, CoreCallbackData, Update},
31    dom::Dom,
32    refany::RefAny,
33    window::VirtualKeyCode,
34};
35use azul_css::{
36    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
37    props::{basic::{ColorU, StyleFontFamily, StyleFontFamilyVec, StyleFontSize}, layout::{LayoutPosition, LayoutWidth, LayoutHeight, LayoutBoxSizing, LayoutFlexGrow, LayoutMinHeight, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutPaddingBottom, LayoutOverflow, LayoutDisplay, LayoutTop, LayoutLeft}, property::{CssProperty, StyleWhiteSpaceValue}, style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleOpacity, StyleCursor, StyleTextColor, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleTextAlign, StyleWhiteSpace}},
38    impl_option_inner, AzString, U32Vec, OptionString,
39};
40
41use crate::callbacks::{Callback, CallbackInfo};
42use crate::widgets::text_input::{OnTextInputReturn, TextInputValid};
43
44// ---- colours ----
45const BACKGROUND_COLOR: ColorU = ColorU {
46    r: 255,
47    g: 255,
48    b: 255,
49    a: 255,
50}; // white
51const BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 };
52const COLOR_9B9B9B: ColorU = ColorU {
53    r: 155,
54    g: 155,
55    b: 155,
56    a: 255,
57}; // #9b9b9b border
58const COLOR_4286F4: ColorU = ColorU {
59    r: 66,
60    g: 134,
61    b: 244,
62    a: 255,
63}; // #4286f4 focus/hover
64const COLOR_4C4C4C: ColorU = ColorU {
65    r: 76,
66    g: 76,
67    b: 76,
68    a: 255,
69}; // #4C4C4C text
70
71const CURSOR_COLOR_BLACK: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(BLACK)];
72const CURSOR_COLOR: StyleBackgroundContentVec =
73    StyleBackgroundContentVec::from_const_slice(CURSOR_COLOR_BLACK);
74
75const BACKGROUND_THEME_LIGHT: &[StyleBackgroundContent] =
76    &[StyleBackgroundContent::Color(BACKGROUND_COLOR)];
77const BACKGROUND_COLOR_LIGHT: StyleBackgroundContentVec =
78    StyleBackgroundContentVec::from_const_slice(BACKGROUND_THEME_LIGHT);
79
80const SANS_SERIF_STR: &str = "system:ui";
81const SANS_SERIF: AzString = AzString::from_const_str(SANS_SERIF_STR);
82const SANS_SERIF_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SANS_SERIF)];
83const SANS_SERIF_FAMILY: StyleFontFamilyVec =
84    StyleFontFamilyVec::from_const_slice(SANS_SERIF_FAMILIES);
85
86/// Minimum height of the editable area (~4 lines).
87const MIN_HEIGHT_PX: isize = 64;
88
89// -- cursor style (a static child; does not track the caret — see module TODO2) --
90static TEXT_CURSOR_PROPS: &[CssPropertyWithConditions] = &[
91    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
92    CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(1))),
93    CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(11))),
94    CssPropertyWithConditions::simple(CssProperty::const_background_content(CURSOR_COLOR)),
95    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
96];
97
98// -- container style (cross-platform single style) --
99static TEXT_AREA_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
100    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
101    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
102    CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
103    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
104    CssPropertyWithConditions::simple(CssProperty::const_min_height(LayoutMinHeight::const_px(
105        MIN_HEIGHT_PX,
106    ))),
107    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
108    CssPropertyWithConditions::simple(CssProperty::const_background_content(BACKGROUND_COLOR_LIGHT)),
109    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
110        inner: COLOR_4C4C4C,
111    })),
112    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
113        4,
114    ))),
115    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
116        LayoutPaddingRight::const_px(4),
117    )),
118    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(4))),
119    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
120        LayoutPaddingBottom::const_px(4),
121    )),
122    // border: 1px inset #9b9b9b
123    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
124        LayoutBorderTopWidth::const_px(1),
125    )),
126    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
127        LayoutBorderBottomWidth::const_px(1),
128    )),
129    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
130        LayoutBorderLeftWidth::const_px(1),
131    )),
132    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
133        LayoutBorderRightWidth::const_px(1),
134    )),
135    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
136        inner: BorderStyle::Inset,
137    })),
138    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
139        StyleBorderBottomStyle {
140            inner: BorderStyle::Inset,
141        },
142    )),
143    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
144        inner: BorderStyle::Inset,
145    })),
146    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
147        StyleBorderRightStyle {
148            inner: BorderStyle::Inset,
149        },
150    )),
151    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
152        inner: COLOR_9B9B9B,
153    })),
154    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
155        StyleBorderBottomColor {
156            inner: COLOR_9B9B9B,
157        },
158    )),
159    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
160        inner: COLOR_9B9B9B,
161    })),
162    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
163        StyleBorderRightColor {
164            inner: COLOR_9B9B9B,
165        },
166    )),
167    CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
168    CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Scroll)),
169    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
170    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
171    // Preserve newlines + wrap long lines.
172    CssPropertyWithConditions::simple(CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(
173        StyleWhiteSpace::PreWrap,
174    ))),
175    // Hover / focus border highlight.
176    CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
177        inner: COLOR_4286F4,
178    })),
179    CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
180        StyleBorderBottomColor {
181            inner: COLOR_4286F4,
182        },
183    )),
184    CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(StyleBorderLeftColor {
185        inner: COLOR_4286F4,
186    })),
187    CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
188        StyleBorderRightColor {
189            inner: COLOR_4286F4,
190        },
191    )),
192    CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
193        inner: COLOR_4286F4,
194    })),
195    CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
196        StyleBorderBottomColor {
197            inner: COLOR_4286F4,
198        },
199    )),
200    CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(StyleBorderLeftColor {
201        inner: COLOR_4286F4,
202    })),
203    CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
204        StyleBorderRightColor {
205            inner: COLOR_4286F4,
206        },
207    )),
208];
209
210// -- label style (the rendered multi-line text) --
211static TEXT_AREA_LABEL_PROPS: &[CssPropertyWithConditions] = &[
212    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
213    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
214    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
215    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
216    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
217        inner: COLOR_4C4C4C,
218    })),
219    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
220    CssPropertyWithConditions::simple(CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(
221        StyleWhiteSpace::PreWrap,
222    ))),
223];
224
225// -- placeholder style --
226static TEXT_AREA_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
227    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
228    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
229    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
230    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(4))),
231    CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(4))),
232    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
233    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
234        inner: COLOR_9B9B9B,
235    })),
236    CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
237    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
238];
239
240/// Multi-line text input widget.
241#[derive(Debug, Clone, PartialEq, Eq)]
242#[repr(C)]
243pub struct TextArea {
244    pub text_area_state: TextAreaStateWrapper,
245    pub placeholder_style: CssPropertyWithConditionsVec,
246    pub container_style: CssPropertyWithConditionsVec,
247    pub label_style: CssPropertyWithConditionsVec,
248}
249
250/// Editable state of a text area (text buffer + cursor position).
251#[derive(Debug, Clone, PartialEq, Eq)]
252#[repr(C)]
253pub struct TextAreaState {
254    /// The text buffer as `Vec<char>` (newlines included).
255    pub text: U32Vec,
256    pub placeholder: OptionString,
257    pub max_len: usize,
258    pub cursor_pos: usize,
259}
260
261/// [`TextAreaState`] together with optional user callbacks.
262#[derive(Debug, Clone, PartialEq, Eq)]
263#[repr(C)]
264pub struct TextAreaStateWrapper {
265    pub inner: TextAreaState,
266    pub on_text_input: OptionTextAreaOnTextInput,
267    pub on_focus_lost: OptionTextAreaOnFocusLost,
268    pub update_text_area_before_calling_focus_lost_fn: bool,
269    // appended at the END of the repr(C) struct for ABI stability
270    pub on_virtual_key_down: OptionTextAreaOnVirtualKeyDown,
271}
272
273// -- callbacks --
274
275/// Invoked on each text edit. Returns whether the edit is valid (reusing
276/// [`TextInput`](crate::widgets::text_input::TextInput)'s [`OnTextInputReturn`]).
277pub type TextAreaOnTextInputCallbackType =
278    extern "C" fn(RefAny, CallbackInfo, TextAreaState) -> OnTextInputReturn;
279impl_widget_callback!(
280    TextAreaOnTextInput,
281    OptionTextAreaOnTextInput,
282    TextAreaOnTextInputCallback,
283    TextAreaOnTextInputCallbackType
284);
285
286azul_core::impl_managed_callback! {
287    wrapper:        TextAreaOnTextInputCallback,
288    info_ty:        CallbackInfo,
289    return_ty:      OnTextInputReturn,
290    default_ret:    OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
291    invoker_static: TEXT_AREA_ON_TEXT_INPUT_INVOKER,
292    invoker_ty:     AzTextAreaOnTextInputCallbackInvoker,
293    thunk_fn:       az_text_area_on_text_input_callback_thunk,
294    setter_fn:      AzApp_setTextAreaOnTextInputCallbackInvoker,
295    from_handle_fn: AzTextAreaOnTextInputCallback_createFromHostHandle,
296    extra_args:     [ state: TextAreaState ],
297}
298
299/// Invoked on every virtual-key press while the text area is focused (reusing
300/// [`TextInput`](crate::widgets::text_input::TextInput)'s [`OnTextInputReturn`]).
301pub type TextAreaOnVirtualKeyDownCallbackType =
302    extern "C" fn(RefAny, CallbackInfo, TextAreaState) -> OnTextInputReturn;
303impl_widget_callback!(
304    TextAreaOnVirtualKeyDown,
305    OptionTextAreaOnVirtualKeyDown,
306    TextAreaOnVirtualKeyDownCallback,
307    TextAreaOnVirtualKeyDownCallbackType
308);
309
310azul_core::impl_managed_callback! {
311    wrapper:        TextAreaOnVirtualKeyDownCallback,
312    info_ty:        CallbackInfo,
313    return_ty:      OnTextInputReturn,
314    default_ret:    OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
315    invoker_static: TEXT_AREA_ON_VIRTUAL_KEY_DOWN_INVOKER,
316    invoker_ty:     AzTextAreaOnVirtualKeyDownCallbackInvoker,
317    thunk_fn:       az_text_area_on_virtual_key_down_callback_thunk,
318    setter_fn:      AzApp_setTextAreaOnVirtualKeyDownCallbackInvoker,
319    from_handle_fn: AzTextAreaOnVirtualKeyDownCallback_createFromHostHandle,
320    extra_args:     [ state: TextAreaState ],
321}
322
323/// Invoked when the text area loses focus.
324pub type TextAreaOnFocusLostCallbackType =
325    extern "C" fn(RefAny, CallbackInfo, TextAreaState) -> Update;
326impl_widget_callback!(
327    TextAreaOnFocusLost,
328    OptionTextAreaOnFocusLost,
329    TextAreaOnFocusLostCallback,
330    TextAreaOnFocusLostCallbackType
331);
332
333azul_core::impl_managed_callback! {
334    wrapper:        TextAreaOnFocusLostCallback,
335    info_ty:        CallbackInfo,
336    return_ty:      Update,
337    default_ret:    Update::DoNothing,
338    invoker_static: TEXT_AREA_ON_FOCUS_LOST_INVOKER,
339    invoker_ty:     AzTextAreaOnFocusLostCallbackInvoker,
340    thunk_fn:       az_text_area_on_focus_lost_callback_thunk,
341    setter_fn:      AzApp_setTextAreaOnFocusLostCallbackInvoker,
342    from_handle_fn: AzTextAreaOnFocusLostCallback_createFromHostHandle,
343    extra_args:     [ state: TextAreaState ],
344}
345
346impl Default for TextAreaState {
347    fn default() -> Self {
348        Self {
349            text: Vec::new().into(),
350            placeholder: None.into(),
351            max_len: 1000,
352            cursor_pos: 0,
353        }
354    }
355}
356
357impl TextAreaState {
358    /// Reconstructs the (multi-line) string, including `'\n'` characters.
359    #[must_use] pub fn get_text(&self) -> String {
360        self.text
361            .iter()
362            .filter_map(|c| core::char::from_u32(*c))
363            .collect()
364    }
365}
366
367impl Default for TextAreaStateWrapper {
368    fn default() -> Self {
369        Self {
370            inner: TextAreaState::default(),
371            on_text_input: None.into(),
372            on_focus_lost: None.into(),
373            update_text_area_before_calling_focus_lost_fn: true,
374            on_virtual_key_down: None.into(),
375        }
376    }
377}
378
379impl Default for TextArea {
380    fn default() -> Self {
381        Self {
382            text_area_state: TextAreaStateWrapper::default(),
383            placeholder_style: CssPropertyWithConditionsVec::from_const_slice(
384                TEXT_AREA_PLACEHOLDER_PROPS,
385            ),
386            container_style: CssPropertyWithConditionsVec::from_const_slice(
387                TEXT_AREA_CONTAINER_PROPS,
388            ),
389            label_style: CssPropertyWithConditionsVec::from_const_slice(TEXT_AREA_LABEL_PROPS),
390        }
391    }
392}
393
394impl TextArea {
395    #[must_use] pub fn create() -> Self {
396        Self::default()
397    }
398
399    /// Sets the (multi-line) text. Newlines in `text` are preserved.
400    #[allow(clippy::needless_pass_by_value)] // public by-value setter; builder with_text moves the arg in
401    pub fn set_text(&mut self, text: AzString) {
402        self.text_area_state.inner.text = text
403            .as_str()
404            .chars()
405            .map(|c| c as u32)
406            .collect::<Vec<_>>()
407            .into();
408    }
409
410    #[must_use] pub fn with_text(mut self, text: AzString) -> Self {
411        self.set_text(text);
412        self
413    }
414
415    pub fn set_placeholder(&mut self, placeholder: AzString) {
416        self.text_area_state.inner.placeholder = Some(placeholder).into();
417    }
418
419    #[must_use] pub fn with_placeholder(mut self, placeholder: AzString) -> Self {
420        self.set_placeholder(placeholder);
421        self
422    }
423
424    pub fn set_on_text_input<C: Into<TextAreaOnTextInputCallback>>(
425        &mut self,
426        refany: RefAny,
427        callback: C,
428    ) {
429        self.text_area_state.on_text_input = Some(TextAreaOnTextInput {
430            callback: callback.into(),
431            refany,
432        })
433        .into();
434    }
435
436    #[must_use] pub fn with_on_text_input<C: Into<TextAreaOnTextInputCallback>>(
437        mut self,
438        refany: RefAny,
439        callback: C,
440    ) -> Self {
441        self.set_on_text_input(refany, callback);
442        self
443    }
444
445    pub fn set_on_virtual_key_down<C: Into<TextAreaOnVirtualKeyDownCallback>>(
446        &mut self,
447        refany: RefAny,
448        callback: C,
449    ) {
450        self.text_area_state.on_virtual_key_down = Some(TextAreaOnVirtualKeyDown {
451            callback: callback.into(),
452            refany,
453        })
454        .into();
455    }
456
457    #[must_use] pub fn with_on_virtual_key_down<C: Into<TextAreaOnVirtualKeyDownCallback>>(
458        mut self,
459        refany: RefAny,
460        callback: C,
461    ) -> Self {
462        self.set_on_virtual_key_down(refany, callback);
463        self
464    }
465
466    pub fn set_on_focus_lost<C: Into<TextAreaOnFocusLostCallback>>(
467        &mut self,
468        refany: RefAny,
469        callback: C,
470    ) {
471        self.text_area_state.on_focus_lost = Some(TextAreaOnFocusLost {
472            callback: callback.into(),
473            refany,
474        })
475        .into();
476    }
477
478    #[must_use] pub fn with_on_focus_lost<C: Into<TextAreaOnFocusLostCallback>>(
479        mut self,
480        refany: RefAny,
481        callback: C,
482    ) -> Self {
483        self.set_on_focus_lost(refany, callback);
484        self
485    }
486
487    pub fn set_container_style(&mut self, style: CssPropertyWithConditionsVec) {
488        self.container_style = style;
489    }
490
491    #[must_use] pub fn with_container_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
492        self.set_container_style(style);
493        self
494    }
495
496    #[must_use] pub fn swap_with_default(&mut self) -> Self {
497        let mut s = Self::default();
498        core::mem::swap(&mut s, self);
499        s
500    }
501
502    #[must_use] pub fn dom(mut self) -> Dom {
503        use azul_core::dom::{EventFilter, FocusEventFilter, HoverEventFilter, IdOrClass::Class, TabIndex};
504
505        self.text_area_state.inner.cursor_pos = self.text_area_state.inner.text.len();
506
507        let label_text: String = self
508            .text_area_state
509            .inner
510            .text
511            .iter()
512            .filter_map(|s| core::char::from_u32(*s))
513            .collect();
514
515        let placeholder = self
516            .text_area_state
517            .inner
518            .placeholder
519            .as_ref()
520            .map(|s| s.as_str().to_string())
521            .unwrap_or_default();
522
523        let state_ref = RefAny::new(self.text_area_state);
524
525        Dom::create_div()
526            .with_ids_and_classes(vec![Class("__azul-native-text-area-container".into())].into())
527            .with_css_props(self.container_style)
528            .with_tab_index(TabIndex::Auto)
529            .with_dataset(Some(state_ref.clone()).into())
530            .with_callbacks(
531                vec![
532                    CoreCallbackData {
533                        event: EventFilter::Focus(FocusEventFilter::FocusReceived),
534                        refany: state_ref.clone(),
535                        callback: CoreCallback {
536                            cb: default_on_focus_received as usize,
537                            ctx: azul_core::refany::OptionRefAny::None,
538                        },
539                    },
540                    CoreCallbackData {
541                        event: EventFilter::Focus(FocusEventFilter::FocusLost),
542                        refany: state_ref.clone(),
543                        callback: CoreCallback {
544                            cb: default_on_focus_lost as usize,
545                            ctx: azul_core::refany::OptionRefAny::None,
546                        },
547                    },
548                    CoreCallbackData {
549                        event: EventFilter::Focus(FocusEventFilter::TextInput),
550                        refany: state_ref.clone(),
551                        callback: CoreCallback {
552                            cb: default_on_text_input as usize,
553                            ctx: azul_core::refany::OptionRefAny::None,
554                        },
555                    },
556                    CoreCallbackData {
557                        event: EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
558                        refany: state_ref,
559                        callback: CoreCallback {
560                            cb: default_on_virtual_key_down as usize,
561                            ctx: azul_core::refany::OptionRefAny::None,
562                        },
563                    },
564                ]
565                .into(),
566            )
567            .with_children(
568                vec![
569                    Dom::create_text(placeholder)
570                        .with_ids_and_classes(
571                            vec![Class("__azul-native-text-area-placeholder".into())].into(),
572                        )
573                        .with_css_props(self.placeholder_style),
574                    Dom::create_text(label_text)
575                        .with_ids_and_classes(
576                            vec![Class("__azul-native-text-area-label".into())].into(),
577                        )
578                        .with_css_props(self.label_style)
579                        .with_children(
580                            vec![Dom::create_div()
581                                .with_ids_and_classes(
582                                    vec![Class("__azul-native-text-area-cursor".into())].into(),
583                                )
584                                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
585                                    TEXT_CURSOR_PROPS,
586                                ))]
587                            .into(),
588                        ),
589                ]
590                .into(),
591            )
592    }
593}
594
595extern "C" fn default_on_focus_received(mut text_area: RefAny, mut info: CallbackInfo) -> Update {
596    let Some(mut text_area) = text_area.downcast_mut::<TextAreaStateWrapper>() else {
597        return Update::DoNothing;
598    };
599
600    let text_area = &mut *text_area;
601
602    let Some(placeholder_text_node_id) = info.get_first_child(info.get_hit_node()) else {
603        return Update::DoNothing;
604    };
605
606    // hide the placeholder text
607    if text_area.inner.text.is_empty() {
608        info.set_css_property(
609            placeholder_text_node_id,
610            CssProperty::const_opacity(StyleOpacity::const_new(0)),
611        );
612    }
613
614    text_area.inner.cursor_pos = text_area.inner.text.len();
615
616    Update::DoNothing
617}
618
619extern "C" fn default_on_focus_lost(mut text_area: RefAny, mut info: CallbackInfo) -> Update {
620    let Some(mut text_area) = text_area.downcast_mut::<TextAreaStateWrapper>() else {
621        return Update::DoNothing;
622    };
623
624    let text_area = &mut *text_area;
625
626    let Some(placeholder_text_node_id) = info.get_first_child(info.get_hit_node()) else {
627        return Update::DoNothing;
628    };
629
630    // show the placeholder text
631    if text_area.inner.text.is_empty() {
632        info.set_css_property(
633            placeholder_text_node_id,
634            CssProperty::const_opacity(StyleOpacity::const_new(100)),
635        );
636    }
637
638    let text_area = &mut *text_area;
639    let onfocuslost = &mut text_area.on_focus_lost;
640    let inner = text_area.inner.clone();
641
642    match onfocuslost.as_mut() {
643        Some(TextAreaOnFocusLost { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
644        None => Update::DoNothing,
645    }
646}
647
648extern "C" fn default_on_text_input(text_area: RefAny, info: CallbackInfo) -> Update {
649    default_on_text_input_inner(text_area, info).unwrap_or(Update::DoNothing)
650}
651
652fn default_on_text_input_inner(mut text_area: RefAny, mut info: CallbackInfo) -> Option<Update> {
653    let mut text_area = text_area.downcast_mut::<TextAreaStateWrapper>()?;
654
655    let changeset = info.get_text_changeset()?;
656    let inserted_text = changeset.inserted_text.as_str().to_string();
657
658    if inserted_text.is_empty() {
659        return None;
660    }
661
662    let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
663    let label_node_id = info.get_next_sibling(placeholder_node_id)?;
664    let _cursor_node_id = info.get_first_child(label_node_id)?;
665
666    let result = {
667        let text_area = &mut *text_area;
668        let ontextinput = &mut text_area.on_text_input;
669
670        // inner_clone has the new (would-be) text
671        let mut inner_clone = text_area.inner.clone();
672        inner_clone.cursor_pos = inner_clone.cursor_pos.saturating_add(inserted_text.len());
673        inner_clone.text = {
674            let mut internal = inner_clone.text.clone().into_library_owned_vec();
675            internal.extend(inserted_text.chars().map(|c| c as u32));
676            internal.into()
677        };
678
679        match ontextinput.as_mut() {
680            Some(TextAreaOnTextInput { callback, refany }) => {
681                (callback.cb)(refany.clone(), info, inner_clone)
682            }
683            None => OnTextInputReturn {
684                update: Update::DoNothing,
685                valid: TextInputValid::Yes,
686            },
687        }
688    };
689
690    if result.valid == TextInputValid::Yes {
691        // hide the placeholder text
692        info.set_css_property(
693            placeholder_node_id,
694            CssProperty::const_opacity(StyleOpacity::const_new(0)),
695        );
696
697        // append to the text
698        text_area.inner.text = {
699            let mut internal = text_area.inner.text.clone().into_library_owned_vec();
700            internal.extend(inserted_text.chars().map(|c| c as u32));
701            internal.into()
702        };
703        text_area.inner.cursor_pos = text_area
704            .inner
705            .cursor_pos
706            .saturating_add(inserted_text.len());
707
708        info.change_node_text(label_node_id, text_area.inner.get_text().into());
709    }
710
711    Some(result.update)
712}
713
714extern "C" fn default_on_virtual_key_down(text_area: RefAny, info: CallbackInfo) -> Update {
715    default_on_virtual_key_down_inner(text_area, info).unwrap_or(Update::DoNothing)
716}
717
718fn default_on_virtual_key_down_inner(
719    mut text_area: RefAny,
720    mut info: CallbackInfo,
721) -> Option<Update> {
722    let mut text_area = text_area.downcast_mut::<TextAreaStateWrapper>()?;
723    let keyboard_state = info.get_current_keyboard_state();
724
725    let c = keyboard_state.current_virtual_keycode.into_option()?;
726    let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
727    let label_node_id = info.get_next_sibling(placeholder_node_id)?;
728    let _cursor_node_id = info.get_first_child(label_node_id)?;
729
730    // Dispatch to the user's on_virtual_key_down callback first; a
731    // TextInputValid::No return suppresses the built-in editing behavior.
732    let result = {
733        // rustc doesn't understand the borrowing lifetime here
734        let text_area = &mut *text_area;
735        let inner_clone = text_area.inner.clone();
736        match text_area.on_virtual_key_down.as_mut() {
737            Some(TextAreaOnVirtualKeyDown { callback, refany }) => {
738                (callback.cb)(refany.clone(), info, inner_clone)
739            }
740            None => OnTextInputReturn {
741                update: Update::DoNothing,
742                valid: TextInputValid::Yes,
743            },
744        }
745    };
746
747    if result.valid == TextInputValid::No {
748        return Some(result.update);
749    }
750
751    match c {
752        VirtualKeyCode::Back => {
753            text_area.inner.text = {
754                let mut internal = text_area.inner.text.clone().into_library_owned_vec();
755                internal.pop();
756                internal.into()
757            };
758            text_area.inner.cursor_pos = text_area.inner.cursor_pos.saturating_sub(1);
759            info.change_node_text(label_node_id, text_area.inner.get_text().into());
760
761            // re-show placeholder if the buffer is now empty
762            if text_area.inner.text.is_empty() {
763                info.set_css_property(
764                    placeholder_node_id,
765                    CssProperty::const_opacity(StyleOpacity::const_new(100)),
766                );
767            }
768        }
769        VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => {
770            // insert a newline
771            text_area.inner.text = {
772                let mut internal = text_area.inner.text.clone().into_library_owned_vec();
773                internal.push('\n' as u32);
774                internal.into()
775            };
776            text_area.inner.cursor_pos = text_area.inner.cursor_pos.saturating_add(1);
777            info.change_node_text(label_node_id, text_area.inner.get_text().into());
778            // hide placeholder (buffer is non-empty now)
779            info.set_css_property(
780                placeholder_node_id,
781                CssProperty::const_opacity(StyleOpacity::const_new(0)),
782            );
783        }
784        _ => return Some(result.update),
785    }
786
787    Some(result.update)
788}
789
790impl From<TextArea> for Dom {
791    fn from(t: TextArea) -> Self {
792        t.dom()
793    }
794}