1use 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
44const BACKGROUND_COLOR: ColorU = ColorU {
46 r: 255,
47 g: 255,
48 b: 255,
49 a: 255,
50}; const 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}; const COLOR_4286F4: ColorU = ColorU {
59 r: 66,
60 g: 134,
61 b: 244,
62 a: 255,
63}; const COLOR_4C4C4C: ColorU = ColorU {
65 r: 76,
66 g: 76,
67 b: 76,
68 a: 255,
69}; const 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
86const MIN_HEIGHT_PX: isize = 64;
88
89static 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
98static 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 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 CssPropertyWithConditions::simple(CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(
173 StyleWhiteSpace::PreWrap,
174 ))),
175 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
210static 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
225static 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
252#[repr(C)]
253pub struct TextAreaState {
254 pub text: U32Vec,
256 pub placeholder: OptionString,
257 pub max_len: usize,
258 pub cursor_pos: usize,
259}
260
261#[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 pub on_virtual_key_down: OptionTextAreaOnVirtualKeyDown,
271}
272
273pub 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
299pub 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
323pub 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 #[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 #[allow(clippy::needless_pass_by_value)] 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 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 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 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 info.set_css_property(
693 placeholder_node_id,
694 CssProperty::const_opacity(StyleOpacity::const_new(0)),
695 );
696
697 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 let result = {
733 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 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 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 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}
795
796#[cfg(test)]
797#[allow(clippy::redundant_closure)]
804mod autotest_generated {
805 use std::{
806 collections::{BTreeMap, HashMap},
807 sync::{Arc, Mutex},
808 };
809
810 use azul_core::{
811 dom::{DomId, DomNodeId, EventFilter, FocusEventFilter, NodeId, NodeType},
812 geom::{LogicalRect, OptionLogicalPosition},
813 gl::OptionGlContextPtr,
814 hit_test::ScrollPosition,
815 refany::OptionRefAny,
816 resources::RendererResources,
817 styled_dom::{NodeHierarchyItemId, StyledDom},
818 window::{MonitorVec, RawWindowHandle},
819 };
820 use rust_fontconfig::FcFontCache;
821
822 use super::*;
823 #[cfg(feature = "icu")]
824 use crate::icu::IcuLocalizerHandle;
825 use crate::{
826 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
827 managers::text_input::PendingTextEdit,
828 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
829 window::{DomLayoutResult, LayoutWindow},
830 window_state::FullWindowState,
831 };
832
833 const ROUND_TRIP: [&str; 22] = [
842 "",
843 " ",
844 "a",
845 "hello",
846 "\n",
847 "\n\n\n",
848 "a\nb",
849 "a\r\nb", "trailing\n",
851 "\nleading",
852 "\t\ttabbed",
853 "\0", "line1\nline2\nline3",
855 "ünïcödé",
856 "e\u{301}", "😀", "👩👩👧👦", "🇩🇪", "مرحبا", "日本語",
862 "a\u{200b}b", "\u{10FFFF}", ];
865
866 const NON_SCALAR: [u32; 6] = [
870 0xD800, 0xDBFF,
872 0xDC00, 0xDFFF,
874 0x0011_0000, u32::MAX,
876 ];
877
878 fn buffer(text: &str) -> U32Vec {
884 text.chars().map(|c| c as u32).collect::<Vec<_>>().into()
885 }
886
887 fn wrapper(text: &str) -> TextAreaStateWrapper {
889 TextAreaStateWrapper {
890 inner: TextAreaState {
891 text: buffer(text),
892 ..TextAreaState::default()
893 },
894 ..TextAreaStateWrapper::default()
895 }
896 }
897
898 fn read(state: &RefAny) -> TextAreaState {
900 let mut handle = state.clone();
901 let w = handle
902 .downcast_ref::<TextAreaStateWrapper>()
903 .expect("the payload must still be a TextAreaStateWrapper");
904 w.inner.clone()
905 }
906
907 fn poke(state: &RefAny, f: impl FnOnce(&mut TextAreaStateWrapper)) {
910 let mut handle = state.clone();
911 let mut w = handle
912 .downcast_mut::<TextAreaStateWrapper>()
913 .expect("the payload must still be a TextAreaStateWrapper");
914 f(&mut w);
915 }
916
917 fn style(n: usize) -> CssPropertyWithConditionsVec {
920 let all: Vec<CssPropertyWithConditions> =
921 TextArea::default().container_style.as_ref().to_vec();
922 assert!(n <= all.len(), "not enough default properties to slice");
923 CssPropertyWithConditionsVec::from_vec(all.into_iter().take(n).collect())
924 }
925
926 fn text_of(node: &Dom) -> Option<&str> {
928 match node.root.get_node_type() {
929 NodeType::Text(s) => Some(s.as_ref().as_str()),
930 _ => None,
931 }
932 }
933
934 struct EditLog {
943 seen: Vec<TextAreaState>,
944 ret: OnTextInputReturn,
945 }
946
947 struct FocusLog {
949 seen: Vec<TextAreaState>,
950 ret: Update,
951 }
952
953 extern "C" fn record_text_input(
954 mut data: RefAny,
955 _: CallbackInfo,
956 state: TextAreaState,
957 ) -> OnTextInputReturn {
958 let Some(mut log) = data.downcast_mut::<EditLog>() else {
959 return OnTextInputReturn {
960 update: Update::DoNothing,
961 valid: TextInputValid::Yes,
962 };
963 };
964 log.seen.push(state);
965 log.ret
966 }
967
968 extern "C" fn record_virtual_key(
969 mut data: RefAny,
970 _: CallbackInfo,
971 state: TextAreaState,
972 ) -> OnTextInputReturn {
973 match data.downcast_mut::<EditLog>() {
974 Some(mut log) => {
975 log.seen.push(state.clone());
976 log.ret
977 }
978 None => OnTextInputReturn {
979 update: Update::RefreshDom,
980 valid: TextInputValid::Yes,
981 },
982 }
983 }
984
985 extern "C" fn record_focus_lost(
986 mut data: RefAny,
987 _: CallbackInfo,
988 state: TextAreaState,
989 ) -> Update {
990 let mut update = Update::DoNothing;
991 if let Some(mut log) = data.downcast_mut::<FocusLog>() {
992 log.seen.push(state);
993 update = log.ret;
994 }
995 update
996 }
997
998 fn edit_log(ret: OnTextInputReturn) -> RefAny {
999 RefAny::new(EditLog {
1000 seen: Vec::new(),
1001 ret,
1002 })
1003 }
1004
1005 fn focus_log(ret: Update) -> RefAny {
1006 RefAny::new(FocusLog {
1007 seen: Vec::new(),
1008 ret,
1009 })
1010 }
1011
1012 fn edits_seen(log: &RefAny) -> Vec<TextAreaState> {
1013 let mut handle = log.clone();
1014 let l = handle
1015 .downcast_ref::<EditLog>()
1016 .expect("the payload must still be an EditLog");
1017 l.seen.clone()
1018 }
1019
1020 fn focus_seen(log: &RefAny) -> Vec<TextAreaState> {
1021 let mut handle = log.clone();
1022 let l = handle
1023 .downcast_ref::<FocusLog>()
1024 .expect("the payload must still be a FocusLog");
1025 l.seen.clone()
1026 }
1027
1028 const ACCEPT: OnTextInputReturn = OnTextInputReturn {
1029 update: Update::RefreshDom,
1030 valid: TextInputValid::Yes,
1031 };
1032 const REJECT: OnTextInputReturn = OnTextInputReturn {
1033 update: Update::RefreshDomAllWindows,
1034 valid: TextInputValid::No,
1035 };
1036
1037 #[derive(Copy, Clone, Debug)]
1043 struct Nodes {
1044 container: usize,
1045 placeholder: usize,
1046 label: usize,
1047 cursor: usize,
1048 }
1049
1050 #[derive(Copy, Clone, Debug)]
1052 enum Hit {
1053 Nothing,
1055 Container,
1056 Placeholder,
1057 Cursor,
1059 }
1060
1061 fn nodes_with_class(styled: &StyledDom, class: &str) -> Vec<usize> {
1063 styled
1064 .node_data
1065 .as_ref()
1066 .iter()
1067 .enumerate()
1068 .filter(|(_, nd)| nd.has_class(class))
1069 .map(|(i, _)| i)
1070 .collect()
1071 }
1072
1073 fn skeleton() -> (StyledDom, Nodes) {
1078 let styled = StyledDom::create_from_dom(TextArea::create().dom());
1079
1080 fn one(styled: &StyledDom, class: &str) -> usize {
1081 let found = nodes_with_class(styled, class);
1082 assert_eq!(found.len(), 1, "expected exactly one `{class}` node");
1083 found[0]
1084 }
1085
1086 let nodes = Nodes {
1087 container: one(&styled, "__azul-native-text-area-container"),
1088 placeholder: one(&styled, "__azul-native-text-area-placeholder"),
1089 label: one(&styled, "__azul-native-text-area-label"),
1090 cursor: one(&styled, "__azul-native-text-area-cursor"),
1091 };
1092 (styled, nodes)
1093 }
1094
1095 fn dom_node(idx: usize) -> DomNodeId {
1096 DomNodeId {
1097 dom: DomId::ROOT_ID,
1098 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
1099 }
1100 }
1101
1102 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
1104 DomLayoutResult {
1105 styled_dom,
1106 layout_tree: LayoutTree {
1107 nodes: Vec::new(),
1108 warm: Vec::new(),
1109 cold: Vec::new(),
1110 root: 0,
1111 dom_to_layout: BTreeMap::new(),
1112 children_arena: Vec::new(),
1113 children_offsets: Vec::new(),
1114 subtree_needs_intrinsic: Vec::new(),
1115 },
1116 calculated_positions: Vec::new(),
1117 viewport: LogicalRect::zero(),
1118 display_list: DisplayList::default(),
1119 scroll_ids: HashMap::new(),
1120 scroll_id_to_node_id: HashMap::new(),
1121 }
1122 }
1123
1124 struct Env {
1126 with_dom: bool,
1129 changeset: Option<PendingTextEdit>,
1130 keycode: Option<VirtualKeyCode>,
1131 hit: Hit,
1132 }
1133
1134 impl Default for Env {
1135 fn default() -> Self {
1136 Self {
1137 with_dom: true,
1138 changeset: None,
1139 keycode: None,
1140 hit: Hit::Container,
1141 }
1142 }
1143 }
1144
1145 impl Env {
1146 fn typed(text: &str) -> Self {
1147 Self {
1148 changeset: Some(PendingTextEdit {
1149 node: dom_node(0),
1150 inserted_text: AzString::from(text),
1151 old_text: AzString::from(""),
1152 }),
1153 ..Self::default()
1154 }
1155 }
1156
1157 fn key(code: VirtualKeyCode) -> Self {
1158 Self {
1159 keycode: Some(code),
1160 ..Self::default()
1161 }
1162 }
1163
1164 fn hitting(mut self, hit: Hit) -> Self {
1165 self.hit = hit;
1166 self
1167 }
1168 }
1169
1170 fn run<R>(
1173 env: Env,
1174 data: &RefAny,
1175 call: impl FnOnce(RefAny, CallbackInfo) -> R,
1176 ) -> (R, Vec<CallbackChange>, Nodes) {
1177 let (styled, nodes) = skeleton();
1178
1179 let mut layout_window =
1180 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
1181 if env.with_dom {
1182 layout_window
1183 .layout_results
1184 .insert(DomId::ROOT_ID, layout_result(styled));
1185 }
1186 layout_window.text_input_manager.pending_changeset = env.changeset;
1187
1188 let renderer_resources = RendererResources::default();
1189 let previous_window_state: Option<FullWindowState> = None;
1190 let mut current_window_state = FullWindowState::default();
1191 current_window_state.keyboard_state.current_virtual_keycode = env.keycode.into();
1192 let gl_context = OptionGlContextPtr::None;
1193 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
1194 BTreeMap::new();
1195 let window_handle = RawWindowHandle::Unsupported;
1196 let system_callbacks = ExternalSystemCallbacks::rust_internal();
1197
1198 let ref_data = CallbackInfoRefData {
1199 layout_window: &layout_window,
1200 renderer_resources: &renderer_resources,
1201 previous_window_state: &previous_window_state,
1202 current_window_state: ¤t_window_state,
1203 gl_context: &gl_context,
1204 current_scroll_manager: &scroll_states,
1205 current_window_handle: &window_handle,
1206 system_callbacks: &system_callbacks,
1207 system_style: Arc::new(azul_css::system::SystemStyle::default()),
1208 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
1209 #[cfg(feature = "icu")]
1210 icu_localizer: IcuLocalizerHandle::default(),
1211 ctx: OptionRefAny::None,
1212 };
1213
1214 let hit = match env.hit {
1215 Hit::Nothing => DomNodeId {
1216 dom: DomId::ROOT_ID,
1217 node: NodeHierarchyItemId::NONE,
1218 },
1219 Hit::Container => dom_node(nodes.container),
1220 Hit::Placeholder => dom_node(nodes.placeholder),
1221 Hit::Cursor => dom_node(nodes.cursor),
1222 };
1223
1224 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
1225 let info = CallbackInfo::new(
1226 &ref_data,
1227 &changes,
1228 hit,
1229 OptionLogicalPosition::None,
1230 OptionLogicalPosition::None,
1231 );
1232
1233 let out = call(data.clone(), info);
1234 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
1235 (out, recorded, nodes)
1236 }
1237
1238 fn opacity_writes(changes: &[CallbackChange]) -> Vec<(usize, f32)> {
1240 let mut out = Vec::new();
1241 for change in changes {
1242 if let CallbackChange::ChangeNodeCssProperties {
1243 node_id, properties, ..
1244 } = change
1245 {
1246 for p in properties.as_ref() {
1247 if let CssProperty::Opacity(v) = p {
1248 if let Some(o) = v.get_property() {
1249 out.push((node_id.index(), o.inner.normalized()));
1250 }
1251 }
1252 }
1253 }
1254 }
1255 out
1256 }
1257
1258 fn text_writes(changes: &[CallbackChange]) -> Vec<(usize, String)> {
1260 changes
1261 .iter()
1262 .filter_map(|change| match change {
1263 CallbackChange::ChangeNodeText { node_id, text } => Some((
1264 node_id
1265 .node
1266 .into_crate_internal()
1267 .expect("a text write always targets a real node")
1268 .index(),
1269 text.as_str().to_string(),
1270 )),
1271 _ => None,
1272 })
1273 .collect()
1274 }
1275
1276 #[test]
1281 fn get_text_on_a_default_state_is_empty() {
1282 let state = TextAreaState::default();
1283 assert_eq!(state.get_text(), "");
1284 assert!(state.text.is_empty());
1285 assert_eq!(state.cursor_pos, 0);
1286 assert_eq!(state.max_len, 1000);
1287 assert!(state.placeholder.is_none());
1288 }
1289
1290 #[test]
1291 fn get_text_round_trips_every_sample_string() {
1292 for s in ROUND_TRIP {
1293 let area = TextArea::create().with_text(AzString::from(s));
1294 assert_eq!(
1295 area.text_area_state.inner.get_text(),
1296 s,
1297 "set_text -> get_text must be lossless for {s:?}"
1298 );
1299 assert_eq!(
1300 area.text_area_state.inner.text.len(),
1301 s.chars().count(),
1302 "the buffer counts chars, not bytes, for {s:?}"
1303 );
1304 }
1305 }
1306
1307 #[test]
1308 fn get_text_drops_code_units_that_are_not_scalar_values() {
1309 for unit in NON_SCALAR {
1312 let state = TextAreaState {
1313 text: vec![unit].into(),
1314 ..TextAreaState::default()
1315 };
1316 assert_eq!(
1317 state.get_text(),
1318 "",
1319 "0x{unit:X} is not a scalar value and must not reach the string"
1320 );
1321 assert_eq!(state.text.len(), 1, "the raw buffer keeps the unit");
1322 }
1323 }
1324
1325 #[test]
1326 fn get_text_keeps_the_scalars_around_dropped_units() {
1327 let mut units = vec!['a' as u32];
1328 units.extend(NON_SCALAR);
1329 units.push('b' as u32);
1330 let state = TextAreaState {
1331 text: units.into(),
1332 ..TextAreaState::default()
1333 };
1334
1335 assert_eq!(state.get_text(), "ab", "only the non-scalars may be dropped");
1336 assert_eq!(state.text.len(), NON_SCALAR.len() + 2);
1337 }
1338
1339 #[test]
1340 fn get_text_accepts_the_boundary_scalars() {
1341 let units = vec![0x0000, 0xD7FF, 0xE000, 0x0010_FFFF];
1344 let state = TextAreaState {
1345 text: units.clone().into(),
1346 ..TextAreaState::default()
1347 };
1348 assert_eq!(
1349 state.get_text().chars().count(),
1350 units.len(),
1351 "every boundary scalar must survive"
1352 );
1353 }
1354
1355 #[test]
1356 fn get_text_handles_a_very_large_buffer() {
1357 let big: String = "line 😀 ünicode\n".repeat(20_000);
1358 let area = TextArea::create().with_text(AzString::from(big.as_str()));
1359
1360 assert_eq!(area.text_area_state.inner.text.len(), big.chars().count());
1361 assert_eq!(area.text_area_state.inner.get_text(), big);
1362 }
1363
1364 #[test]
1369 fn create_equals_default() {
1370 assert_eq!(TextArea::create(), TextArea::default());
1371 }
1372
1373 #[test]
1374 fn create_starts_empty_with_no_hooks() {
1375 let area = TextArea::create();
1376 let s = &area.text_area_state;
1377
1378 assert!(s.inner.text.is_empty());
1379 assert!(s.inner.placeholder.is_none());
1380 assert_eq!(s.inner.max_len, 1000);
1381 assert_eq!(s.inner.cursor_pos, 0);
1382 assert!(s.on_text_input.is_none());
1383 assert!(s.on_virtual_key_down.is_none());
1384 assert!(s.on_focus_lost.is_none());
1385 assert!(s.update_text_area_before_calling_focus_lost_fn);
1386 }
1387
1388 #[test]
1389 fn create_ships_all_three_style_vectors_non_empty() {
1390 let area = TextArea::create();
1391 assert!(!area.container_style.as_ref().is_empty());
1392 assert!(!area.label_style.as_ref().is_empty());
1393 assert!(!area.placeholder_style.as_ref().is_empty());
1394 }
1395
1396 #[test]
1397 fn create_is_repeatable_and_unshared() {
1398 let mut a = TextArea::create();
1401 let b = TextArea::create();
1402 a.set_text(AzString::from("mutated"));
1403
1404 assert_ne!(a, b);
1405 assert_eq!(b.text_area_state.inner.get_text(), "");
1406 }
1407
1408 #[test]
1413 fn set_text_preserves_newlines() {
1414 let mut area = TextArea::create();
1415 area.set_text(AzString::from("a\nb\n\nc\n"));
1416
1417 assert_eq!(area.text_area_state.inner.get_text(), "a\nb\n\nc\n");
1418 assert_eq!(
1419 area.text_area_state
1420 .inner
1421 .text
1422 .iter()
1423 .filter(|c| **c == '\n' as u32)
1424 .count(),
1425 4,
1426 "all four newlines have to be stored"
1427 );
1428 }
1429
1430 #[test]
1431 fn set_text_replaces_rather_than_appends() {
1432 let mut area = TextArea::create();
1433 area.set_text(AzString::from("first"));
1434 area.set_text(AzString::from("second"));
1435
1436 assert_eq!(area.text_area_state.inner.get_text(), "second");
1437 assert_eq!(area.text_area_state.inner.text.len(), 6);
1438 }
1439
1440 #[test]
1441 fn set_text_with_an_empty_string_clears_the_buffer() {
1442 let mut area = TextArea::create().with_text(AzString::from("something"));
1443 area.set_text(AzString::from(""));
1444
1445 assert!(area.text_area_state.inner.text.is_empty());
1446 assert_eq!(area.text_area_state.inner.get_text(), "");
1447 }
1448
1449 #[test]
1450 fn with_text_is_exactly_set_text() {
1451 for s in ROUND_TRIP {
1452 let mut a = TextArea::create();
1453 a.set_text(AzString::from(s));
1454 let b = TextArea::create().with_text(AzString::from(s));
1455 assert_eq!(a, b, "the builder and the setter must agree for {s:?}");
1456 }
1457 }
1458
1459 #[test]
1460 fn set_text_ignores_max_len() {
1461 let mut area = TextArea::create();
1465 area.text_area_state.inner.max_len = 3;
1466 area.set_text(AzString::from("far past the limit"));
1467
1468 assert_eq!(area.text_area_state.inner.text.len(), 18);
1469 assert_eq!(area.text_area_state.inner.max_len, 3);
1470 }
1471
1472 #[test]
1473 fn set_text_leaves_a_stale_cursor_behind() {
1474 let mut area = TextArea::create().with_text(AzString::from("0123456789"));
1477 area.text_area_state.inner.cursor_pos = 10;
1478 area.set_text(AzString::from(""));
1479
1480 assert_eq!(
1481 area.text_area_state.inner.cursor_pos, 10,
1482 "the setter deliberately leaves the cursor alone"
1483 );
1484 assert!(area.text_area_state.inner.text.is_empty());
1485
1486 let dom = area.dom();
1487 let mut dataset = dom
1488 .root
1489 .get_dataset()
1490 .cloned()
1491 .expect("dom() must attach the state");
1492 let w = dataset
1493 .downcast_ref::<TextAreaStateWrapper>()
1494 .expect("the dataset must be a TextAreaStateWrapper");
1495 assert_eq!(w.inner.cursor_pos, 0, "dom() must repair the stale cursor");
1496 }
1497
1498 #[test]
1499 fn set_text_does_not_disturb_the_other_fields() {
1500 let area = TextArea::create()
1501 .with_placeholder(AzString::from("type here"))
1502 .with_container_style(style(3))
1503 .with_text(AzString::from("body"));
1504
1505 assert_eq!(
1506 area.text_area_state.inner.placeholder.as_ref().map(AzString::as_str),
1507 Some("type here")
1508 );
1509 assert_eq!(area.container_style.len(), 3);
1510 assert_eq!(area.text_area_state.inner.get_text(), "body");
1511 }
1512
1513 #[test]
1518 fn placeholder_round_trips_every_sample_string() {
1519 for s in ROUND_TRIP {
1520 let area = TextArea::create().with_placeholder(AzString::from(s));
1521 assert_eq!(
1522 area.text_area_state.inner.placeholder.as_ref().map(AzString::as_str),
1523 Some(s)
1524 );
1525 }
1526 }
1527
1528 #[test]
1529 fn an_empty_placeholder_is_some_not_none() {
1530 let area = TextArea::create().with_placeholder(AzString::from(""));
1533 assert!(area.text_area_state.inner.placeholder.is_some());
1534 assert_eq!(
1535 area.text_area_state.inner.placeholder.as_ref().map(AzString::as_str),
1536 Some("")
1537 );
1538 }
1539
1540 #[test]
1541 fn set_placeholder_overwrites_the_previous_one() {
1542 let mut area = TextArea::create();
1543 area.set_placeholder(AzString::from("one"));
1544 area.set_placeholder(AzString::from("two"));
1545
1546 assert_eq!(
1547 area.text_area_state.inner.placeholder.as_ref().map(AzString::as_str),
1548 Some("two")
1549 );
1550 }
1551
1552 #[test]
1553 fn with_placeholder_is_exactly_set_placeholder() {
1554 let mut a = TextArea::create();
1555 a.set_placeholder(AzString::from("hint"));
1556 let b = TextArea::create().with_placeholder(AzString::from("hint"));
1557 assert_eq!(a, b);
1558 }
1559
1560 #[test]
1561 fn set_placeholder_does_not_touch_the_text() {
1562 let area = TextArea::create()
1563 .with_text(AzString::from("body\ntext"))
1564 .with_placeholder(AzString::from("hint"));
1565
1566 assert_eq!(area.text_area_state.inner.get_text(), "body\ntext");
1567 assert_eq!(area.text_area_state.inner.cursor_pos, 0);
1568 }
1569
1570 #[test]
1575 fn each_hook_setter_touches_only_its_own_slot() {
1576 let text_in = TextArea::create()
1577 .with_on_text_input(RefAny::new(1u32), record_text_input as TextAreaOnTextInputCallbackType);
1578 assert!(text_in.text_area_state.on_text_input.is_some());
1579 assert!(text_in.text_area_state.on_virtual_key_down.is_none());
1580 assert!(text_in.text_area_state.on_focus_lost.is_none());
1581
1582 let key_down = TextArea::create().with_on_virtual_key_down(
1583 RefAny::new(2u32),
1584 record_virtual_key as TextAreaOnVirtualKeyDownCallbackType,
1585 );
1586 assert!(key_down.text_area_state.on_text_input.is_none());
1587 assert!(key_down.text_area_state.on_virtual_key_down.is_some());
1588 assert!(key_down.text_area_state.on_focus_lost.is_none());
1589
1590 let focus = TextArea::create()
1591 .with_on_focus_lost(RefAny::new(3u32), record_focus_lost as TextAreaOnFocusLostCallbackType);
1592 assert!(focus.text_area_state.on_text_input.is_none());
1593 assert!(focus.text_area_state.on_virtual_key_down.is_none());
1594 assert!(focus.text_area_state.on_focus_lost.is_some());
1595 }
1596
1597 #[test]
1598 fn hook_setters_keep_the_user_payload_reachable() {
1599 let payload = RefAny::new(0xDEAD_BEEF_u32);
1600 let area = TextArea::create()
1601 .with_on_text_input(payload.clone(), record_text_input as TextAreaOnTextInputCallbackType);
1602
1603 let mut stored = area
1604 .text_area_state
1605 .on_text_input
1606 .as_ref()
1607 .expect("the hook must be stored")
1608 .refany
1609 .clone();
1610 assert_eq!(
1611 *stored.downcast_ref::<u32>().expect("payload type must survive"),
1612 0xDEAD_BEEF_u32
1613 );
1614 }
1615
1616 #[test]
1617 fn setting_a_hook_twice_replaces_it_and_keeps_the_old_payload_alive() {
1618 let first = RefAny::new(11u32);
1619 let second = RefAny::new(22u32);
1620 let mut area = TextArea::create();
1621 area.set_on_text_input(first.clone(), record_text_input as TextAreaOnTextInputCallbackType);
1622 area.set_on_text_input(second, record_text_input as TextAreaOnTextInputCallbackType);
1623
1624 let mut stored = area
1625 .text_area_state
1626 .on_text_input
1627 .as_ref()
1628 .expect("the hook must be stored")
1629 .refany
1630 .clone();
1631 assert_eq!(*stored.downcast_ref::<u32>().expect("payload"), 22);
1632
1633 let mut first = first;
1635 assert_eq!(*first.downcast_ref::<u32>().expect("payload"), 11);
1636 }
1637
1638 #[test]
1639 fn with_on_hooks_are_exactly_their_setters() {
1640 let payload = RefAny::new(7u32);
1641
1642 let mut a = TextArea::create();
1643 a.set_on_focus_lost(payload.clone(), record_focus_lost as TextAreaOnFocusLostCallbackType);
1644 let b = TextArea::create()
1645 .with_on_focus_lost(payload, record_focus_lost as TextAreaOnFocusLostCallbackType);
1646 assert_eq!(a, b);
1647 }
1648
1649 #[test]
1650 fn all_three_hooks_can_coexist() {
1651 let area = TextArea::create()
1652 .with_on_text_input(RefAny::new(1u32), record_text_input as TextAreaOnTextInputCallbackType)
1653 .with_on_virtual_key_down(
1654 RefAny::new(2u32),
1655 record_virtual_key as TextAreaOnVirtualKeyDownCallbackType,
1656 )
1657 .with_on_focus_lost(RefAny::new(3u32), record_focus_lost as TextAreaOnFocusLostCallbackType)
1658 .with_text(AzString::from("still here"));
1659
1660 assert!(area.text_area_state.on_text_input.is_some());
1661 assert!(area.text_area_state.on_virtual_key_down.is_some());
1662 assert!(area.text_area_state.on_focus_lost.is_some());
1663 assert_eq!(area.text_area_state.inner.get_text(), "still here");
1664 }
1665
1666 #[test]
1667 fn hook_setters_leave_a_zero_sized_payload_usable() {
1668 struct Zst;
1671 let area = TextArea::create()
1672 .with_on_text_input(RefAny::new(Zst), record_text_input as TextAreaOnTextInputCallbackType);
1673
1674 let mut stored = area
1675 .text_area_state
1676 .on_text_input
1677 .as_ref()
1678 .expect("the hook must be stored")
1679 .refany
1680 .clone();
1681 assert!(stored.downcast_ref::<Zst>().is_some());
1682 assert!(stored.downcast_ref::<u32>().is_none(), "the type tag must still discriminate");
1683 }
1684
1685 #[test]
1690 fn set_container_style_replaces_the_whole_vector() {
1691 let mut area = TextArea::create();
1692 let before = area.container_style.len();
1693 area.set_container_style(style(2));
1694
1695 assert_eq!(area.container_style.len(), 2);
1696 assert_ne!(before, 2, "the fixture has to actually change something");
1697 }
1698
1699 #[test]
1700 fn an_empty_container_style_is_accepted() {
1701 let area = TextArea::create()
1702 .with_container_style(CssPropertyWithConditionsVec::from_vec(Vec::new()));
1703 assert!(area.container_style.as_ref().is_empty());
1704
1705 let dom = area.dom();
1707 assert_eq!(dom.children.as_ref().len(), 2);
1708 }
1709
1710 #[test]
1711 fn container_style_does_not_leak_into_the_other_style_slots() {
1712 let default_label = TextArea::create().label_style;
1713 let default_placeholder = TextArea::create().placeholder_style;
1714 let area = TextArea::create().with_container_style(style(1));
1715
1716 assert_eq!(area.label_style, default_label);
1717 assert_eq!(area.placeholder_style, default_placeholder);
1718 }
1719
1720 #[test]
1725 fn swap_with_default_hands_back_the_old_value_and_resets_self() {
1726 let mut area = TextArea::create()
1727 .with_text(AzString::from("keep\nme"))
1728 .with_placeholder(AzString::from("hint"));
1729
1730 let old = area.swap_with_default();
1731
1732 assert_eq!(old.text_area_state.inner.get_text(), "keep\nme");
1733 assert_eq!(
1734 old.text_area_state.inner.placeholder.as_ref().map(AzString::as_str),
1735 Some("hint")
1736 );
1737 assert_eq!(area, TextArea::default(), "self must be a fresh default");
1738 }
1739
1740 #[test]
1741 fn swap_with_default_twice_yields_a_default_the_second_time() {
1742 let mut area = TextArea::create().with_text(AzString::from("x"));
1743 let first = area.swap_with_default();
1744 let second = area.swap_with_default();
1745
1746 assert_eq!(first.text_area_state.inner.get_text(), "x");
1747 assert_eq!(second, TextArea::default());
1748 assert_eq!(area, TextArea::default());
1749 }
1750
1751 #[test]
1752 fn swap_with_default_carries_the_hooks_out_with_it() {
1753 let payload = RefAny::new(99u32);
1754 let mut area = TextArea::create()
1755 .with_on_focus_lost(payload, record_focus_lost as TextAreaOnFocusLostCallbackType);
1756
1757 let old = area.swap_with_default();
1758
1759 assert!(old.text_area_state.on_focus_lost.is_some());
1760 assert!(area.text_area_state.on_focus_lost.is_none());
1761
1762 let mut stored = old
1763 .text_area_state
1764 .on_focus_lost
1765 .as_ref()
1766 .expect("hook")
1767 .refany
1768 .clone();
1769 assert_eq!(*stored.downcast_ref::<u32>().expect("payload"), 99);
1770 }
1771
1772 #[test]
1777 fn dom_has_the_shape_the_handlers_navigate() {
1778 let dom = TextArea::create().dom();
1779 let children = dom.children.as_ref();
1780
1781 assert_eq!(children.len(), 2, "a text area is exactly [placeholder, label]");
1782 assert!(dom.root.has_class("__azul-native-text-area-container"));
1783 assert!(children[0].root.has_class("__azul-native-text-area-placeholder"));
1784 assert!(children[1].root.has_class("__azul-native-text-area-label"));
1785
1786 let label_children = children[1].children.as_ref();
1787 assert_eq!(label_children.len(), 1, "the label owns exactly the cursor");
1788 assert!(label_children[0].root.has_class("__azul-native-text-area-cursor"));
1789 assert!(
1790 label_children[0].children.as_ref().is_empty(),
1791 "the cursor is a leaf"
1792 );
1793 }
1794
1795 #[test]
1796 fn dom_renders_the_text_into_the_label_and_the_placeholder_into_its_own_node() {
1797 let dom = TextArea::create()
1798 .with_text(AzString::from("body\nlines"))
1799 .with_placeholder(AzString::from("hint"))
1800 .dom();
1801 let children = dom.children.as_ref();
1802
1803 assert_eq!(text_of(&children[0]), Some("hint"));
1804 assert_eq!(text_of(&children[1]), Some("body\nlines"));
1805 }
1806
1807 #[test]
1808 fn dom_renders_an_empty_placeholder_node_when_none_was_set() {
1809 let dom = TextArea::create().with_text(AzString::from("x")).dom();
1812 assert_eq!(text_of(&dom.children.as_ref()[0]), Some(""));
1813 }
1814
1815 #[test]
1816 fn dom_round_trips_every_sample_string_into_the_label() {
1817 for s in ROUND_TRIP {
1818 let dom = TextArea::create().with_text(AzString::from(s)).dom();
1819 assert_eq!(
1820 text_of(&dom.children.as_ref()[1]),
1821 Some(s),
1822 "the label must render {s:?} verbatim"
1823 );
1824 }
1825 }
1826
1827 #[test]
1828 fn dom_drops_non_scalar_units_from_the_label() {
1829 let mut area = TextArea::create().with_text(AzString::from("ab"));
1830 let mut units = area.text_area_state.inner.text.clone().into_library_owned_vec();
1831 units.extend(NON_SCALAR);
1832 area.text_area_state.inner.text = units.into();
1833
1834 let dom = area.dom();
1835 assert_eq!(
1836 text_of(&dom.children.as_ref()[1]),
1837 Some("ab"),
1838 "the rendered label may only contain real scalars"
1839 );
1840 }
1841
1842 #[test]
1843 fn dom_snaps_the_cursor_to_the_end_of_the_buffer() {
1844 for (text, expected) in [("", 0), ("abc", 3), ("😀😀", 2), ("a\nb", 3)] {
1845 let mut area = TextArea::create().with_text(AzString::from(text));
1846 area.text_area_state.inner.cursor_pos = usize::MAX;
1847
1848 let dom = area.dom();
1849 let mut dataset = dom.root.get_dataset().cloned().expect("dataset");
1850 let w = dataset
1851 .downcast_ref::<TextAreaStateWrapper>()
1852 .expect("the dataset must be a TextAreaStateWrapper");
1853 assert_eq!(
1854 w.inner.cursor_pos, expected,
1855 "dom() must clamp the cursor to the buffer for {text:?}"
1856 );
1857 }
1858 }
1859
1860 #[test]
1861 fn dom_wires_up_all_four_focus_callbacks() {
1862 let dom = TextArea::create().dom();
1863 let callbacks = dom.root.get_callbacks();
1864 assert_eq!(callbacks.len(), 4);
1865
1866 let expected = [
1867 (
1868 EventFilter::Focus(FocusEventFilter::FocusReceived),
1869 default_on_focus_received as usize,
1870 ),
1871 (
1872 EventFilter::Focus(FocusEventFilter::FocusLost),
1873 default_on_focus_lost as usize,
1874 ),
1875 (
1876 EventFilter::Focus(FocusEventFilter::TextInput),
1877 default_on_text_input as usize,
1878 ),
1879 (
1880 EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
1881 default_on_virtual_key_down as usize,
1882 ),
1883 ];
1884
1885 for (cd, (event, cb)) in callbacks.as_ref().iter().zip(expected) {
1886 assert_eq!(cd.event, event);
1887 assert_eq!(cd.callback.cb, cb, "wrong handler wired to {event:?}");
1888 }
1889 }
1890
1891 #[test]
1892 fn dom_shares_one_state_handle_between_the_dataset_and_every_callback() {
1893 let dom = TextArea::create().with_text(AzString::from("seed")).dom();
1894 let dataset = dom.root.get_dataset().cloned().expect("dataset");
1895 poke(&dataset, |w| w.inner.max_len = 7);
1896
1897 for cd in dom.root.get_callbacks().as_ref() {
1898 let mut handle = cd.refany.clone();
1899 let w = handle
1900 .downcast_ref::<TextAreaStateWrapper>()
1901 .expect("every callback must carry the state wrapper");
1902 assert_eq!(
1903 w.inner.max_len, 7,
1904 "every callback must see the *same* state object as the dataset"
1905 );
1906 }
1907 }
1908
1909 #[test]
1910 fn dom_survives_a_very_large_buffer() {
1911 let big: String = "wide 😀 line\n".repeat(20_000);
1912 let dom = TextArea::create().with_text(AzString::from(big.as_str())).dom();
1913 assert_eq!(text_of(&dom.children.as_ref()[1]), Some(big.as_str()));
1914 }
1915
1916 #[test]
1917 fn styled_dom_navigation_matches_what_the_handlers_assume() {
1918 let (styled, nodes) = skeleton();
1922 let hierarchy = styled.node_hierarchy.as_container();
1923
1924 let placeholder = hierarchy[NodeId::new(nodes.container)]
1925 .first_child_id(NodeId::new(nodes.container))
1926 .expect("the container must have a first child");
1927 assert_eq!(placeholder.index(), nodes.placeholder);
1928
1929 let label = hierarchy[placeholder]
1930 .next_sibling_id()
1931 .expect("the placeholder must have a next sibling");
1932 assert_eq!(label.index(), nodes.label);
1933
1934 let cursor = hierarchy[label]
1935 .first_child_id(label)
1936 .expect("the label must have a first child");
1937 assert_eq!(cursor.index(), nodes.cursor);
1938 }
1939
1940 #[test]
1945 fn focus_received_ignores_a_foreign_payload() {
1946 let data = RefAny::new(0u8);
1947 let (update, changes, _) = run(Env::default(), &data, |r, ci| default_on_focus_received(r, ci));
1948
1949 assert_eq!(update, Update::DoNothing);
1950 assert!(changes.is_empty(), "a foreign payload must not touch the DOM");
1951 }
1952
1953 #[test]
1954 fn focus_received_bails_out_without_a_hit_node() {
1955 let data = RefAny::new(wrapper(""));
1956 poke(&data, |w| w.inner.cursor_pos = 42);
1957
1958 let (update, changes, _) = run(
1959 Env::default().hitting(Hit::Nothing),
1960 &data,
1961 |r, ci| default_on_focus_received(r, ci),
1962 );
1963
1964 assert_eq!(update, Update::DoNothing);
1965 assert!(changes.is_empty());
1966 assert_eq!(
1967 read(&data).cursor_pos,
1968 42,
1969 "the early return happens *before* the cursor is repaired"
1970 );
1971 }
1972
1973 #[test]
1974 fn focus_received_bails_out_on_a_childless_hit_node() {
1975 let data = RefAny::new(wrapper("text"));
1976 let (update, changes, _) = run(
1977 Env::default().hitting(Hit::Cursor),
1978 &data,
1979 |r, ci| default_on_focus_received(r, ci),
1980 );
1981
1982 assert_eq!(update, Update::DoNothing);
1983 assert!(changes.is_empty());
1984 }
1985
1986 #[test]
1987 fn focus_received_hides_the_placeholder_only_while_the_buffer_is_empty() {
1988 let empty = RefAny::new(wrapper(""));
1989 let (update, changes, nodes) = run(Env::default(), &empty, |r, ci| default_on_focus_received(r, ci));
1990 assert_eq!(update, Update::DoNothing);
1991 assert_eq!(
1992 opacity_writes(&changes),
1993 vec![(nodes.placeholder, 0.0)],
1994 "an empty area hides its placeholder on focus"
1995 );
1996
1997 let filled = RefAny::new(wrapper("typed"));
1998 let (update, changes, _) = run(Env::default(), &filled, |r, ci| default_on_focus_received(r, ci));
1999 assert_eq!(update, Update::DoNothing);
2000 assert!(
2001 changes.is_empty(),
2002 "a non-empty area has nothing to hide — the placeholder is already gone"
2003 );
2004 }
2005
2006 #[test]
2007 fn focus_received_repairs_a_stale_cursor() {
2008 for (text, expected) in [("", 0usize), ("abc", 3), ("😀 x", 3)] {
2009 let data = RefAny::new(wrapper(text));
2010 poke(&data, |w| w.inner.cursor_pos = usize::MAX);
2011
2012 let (_, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_received(r, ci));
2013 assert_eq!(
2014 read(&data).cursor_pos,
2015 expected,
2016 "focus must snap the cursor to the end for {text:?}"
2017 );
2018 }
2019 }
2020
2021 #[test]
2022 fn focus_received_does_not_edit_the_buffer() {
2023 let data = RefAny::new(wrapper("untouched\ntext"));
2024 let (_, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_received(r, ci));
2025 assert_eq!(read(&data).get_text(), "untouched\ntext");
2026 }
2027
2028 #[test]
2033 fn focus_lost_ignores_a_foreign_payload() {
2034 let data = RefAny::new("not a text area".to_string());
2035 let (update, changes, _) = run(Env::default(), &data, |r, ci| default_on_focus_lost(r, ci));
2036
2037 assert_eq!(update, Update::DoNothing);
2038 assert!(changes.is_empty());
2039 }
2040
2041 #[test]
2042 fn focus_lost_shows_the_placeholder_only_while_the_buffer_is_empty() {
2043 let empty = RefAny::new(wrapper(""));
2044 let (update, changes, nodes) = run(Env::default(), &empty, |r, ci| default_on_focus_lost(r, ci));
2045 assert_eq!(update, Update::DoNothing);
2046 assert_eq!(opacity_writes(&changes), vec![(nodes.placeholder, 1.0)]);
2047
2048 let filled = RefAny::new(wrapper("typed"));
2049 let (update, changes, _) = run(Env::default(), &filled, |r, ci| default_on_focus_lost(r, ci));
2050 assert_eq!(update, Update::DoNothing);
2051 assert!(changes.is_empty());
2052 }
2053
2054 #[test]
2055 fn focus_lost_forwards_the_state_to_the_user_hook() {
2056 let log = focus_log(Update::RefreshDomAllWindows);
2057 let mut state = wrapper("saved\ntext");
2058 state.on_focus_lost = Some(TextAreaOnFocusLost {
2059 callback: (record_focus_lost as TextAreaOnFocusLostCallbackType).into(),
2060 refany: log.clone(),
2061 })
2062 .into();
2063 let data = RefAny::new(state);
2064
2065 let (update, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_lost(r, ci));
2066
2067 assert_eq!(
2068 update,
2069 Update::RefreshDomAllWindows,
2070 "the hook's Update must be propagated verbatim"
2071 );
2072 let seen = focus_seen(&log);
2073 assert_eq!(seen.len(), 1);
2074 assert_eq!(seen[0].get_text(), "saved\ntext");
2075 }
2076
2077 #[test]
2078 fn focus_lost_skips_the_user_hook_when_the_dom_has_no_children() {
2079 let log = focus_log(Update::RefreshDom);
2083 let mut state = wrapper("x");
2084 state.on_focus_lost = Some(TextAreaOnFocusLost {
2085 callback: (record_focus_lost as TextAreaOnFocusLostCallbackType).into(),
2086 refany: log.clone(),
2087 })
2088 .into();
2089 let data = RefAny::new(state);
2090
2091 let (update, changes, _) = run(
2092 Env::default().hitting(Hit::Cursor),
2093 &data,
2094 |r, ci| default_on_focus_lost(r, ci),
2095 );
2096
2097 assert_eq!(update, Update::DoNothing);
2098 assert!(changes.is_empty());
2099 assert!(focus_seen(&log).is_empty(), "the hook must not have run");
2100 }
2101
2102 #[test]
2103 fn focus_lost_hands_the_hook_a_snapshot_it_cannot_write_back_through() {
2104 let log = focus_log(Update::DoNothing);
2107 let mut state = wrapper("original");
2108 state.on_focus_lost = Some(TextAreaOnFocusLost {
2109 callback: (record_focus_lost as TextAreaOnFocusLostCallbackType).into(),
2110 refany: log.clone(),
2111 })
2112 .into();
2113 let data = RefAny::new(state);
2114
2115 let (_, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_lost(r, ci));
2116
2117 let mut seen = focus_seen(&log);
2118 assert_eq!(seen.len(), 1);
2119 seen[0].text = buffer("clobbered");
2120 assert_eq!(read(&data).get_text(), "original");
2121 }
2122
2123 #[test]
2124 fn focus_lost_without_a_hook_reports_do_nothing() {
2125 let data = RefAny::new(wrapper("text"));
2126 let (update, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_lost(r, ci));
2127 assert_eq!(update, Update::DoNothing);
2128 }
2129
2130 #[test]
2131 fn focus_lost_does_not_move_the_cursor() {
2132 let data = RefAny::new(wrapper("abcdef"));
2133 poke(&data, |w| w.inner.cursor_pos = 2);
2134
2135 let (_, _, _) = run(Env::default(), &data, |r, ci| default_on_focus_lost(r, ci));
2136
2137 assert_eq!(
2138 read(&data).cursor_pos,
2139 2,
2140 "only focus-received re-snaps the cursor"
2141 );
2142 }
2143
2144 #[test]
2149 fn text_input_without_a_changeset_does_nothing() {
2150 let data = RefAny::new(wrapper("abc"));
2151 let (out, changes, _) = run(Env::default(), &data, default_on_text_input_inner);
2152
2153 assert_eq!(out, None);
2154 assert!(changes.is_empty());
2155 assert_eq!(read(&data).get_text(), "abc");
2156 }
2157
2158 #[test]
2159 fn text_input_with_an_empty_insertion_does_nothing() {
2160 let data = RefAny::new(wrapper("abc"));
2161 let (out, changes, _) = run(Env::typed(""), &data, default_on_text_input_inner);
2162
2163 assert_eq!(out, None, "an empty insertion is not an edit");
2164 assert!(changes.is_empty());
2165 assert_eq!(read(&data).get_text(), "abc");
2166 }
2167
2168 #[test]
2169 fn text_input_ignores_a_foreign_payload() {
2170 let data = RefAny::new(1234u64);
2171 let (out, changes, _) = run(Env::typed("x"), &data, default_on_text_input_inner);
2172
2173 assert_eq!(out, None);
2174 assert!(changes.is_empty());
2175 }
2176
2177 #[test]
2178 fn text_input_bails_out_on_a_childless_hit_node() {
2179 let data = RefAny::new(wrapper("abc"));
2180 let (out, changes, _) = run(
2181 Env::typed("x").hitting(Hit::Cursor),
2182 &data,
2183 default_on_text_input_inner,
2184 );
2185
2186 assert_eq!(out, None);
2187 assert!(changes.is_empty());
2188 assert_eq!(read(&data).get_text(), "abc", "no DOM, no edit");
2189 }
2190
2191 #[test]
2192 fn text_input_bails_out_when_the_hit_node_has_no_sibling_chain() {
2193 let data = RefAny::new(wrapper("abc"));
2196 let (out, changes, _) = run(
2197 Env::typed("x").hitting(Hit::Placeholder),
2198 &data,
2199 default_on_text_input_inner,
2200 );
2201
2202 assert_eq!(out, None);
2203 assert!(changes.is_empty());
2204 assert_eq!(read(&data).get_text(), "abc");
2205 }
2206
2207 #[test]
2208 fn text_input_appends_and_repaints() {
2209 let data = RefAny::new(wrapper("ab"));
2210 let (out, changes, nodes) = run(Env::typed("cd"), &data, default_on_text_input_inner);
2211
2212 assert_eq!(out, Some(Update::DoNothing), "no hook means no refresh");
2213 assert_eq!(read(&data).get_text(), "abcd");
2214 assert_eq!(
2215 opacity_writes(&changes),
2216 vec![(nodes.placeholder, 0.0)],
2217 "typing hides the placeholder"
2218 );
2219 assert_eq!(
2220 text_writes(&changes),
2221 vec![(nodes.label, "abcd".to_string())],
2222 "the label is rewritten with the *whole* buffer, not the delta"
2223 );
2224 }
2225
2226 #[test]
2227 fn text_input_preserves_embedded_newlines() {
2228 let data = RefAny::new(wrapper("first"));
2229 let (out, changes, nodes) = run(Env::typed("\nsecond\n"), &data, default_on_text_input_inner);
2230
2231 assert_eq!(out, Some(Update::DoNothing));
2232 assert_eq!(read(&data).get_text(), "first\nsecond\n");
2233 assert_eq!(
2234 text_writes(&changes),
2235 vec![(nodes.label, "first\nsecond\n".to_string())]
2236 );
2237 }
2238
2239 #[test]
2240 fn text_input_stores_pasted_unicode_by_char() {
2241 for s in ROUND_TRIP {
2242 if s.is_empty() {
2243 continue; }
2245 let data = RefAny::new(wrapper(""));
2246 let (out, _, _) = run(Env::typed(s), &data, default_on_text_input_inner);
2247
2248 assert_eq!(out, Some(Update::DoNothing), "insertion of {s:?}");
2249 let state = read(&data);
2250 assert_eq!(state.get_text(), s, "insertion of {s:?} must be lossless");
2251 assert_eq!(state.text.len(), s.chars().count());
2252 }
2253 }
2254
2255 #[test]
2256 fn text_input_advances_the_cursor_by_bytes_not_chars() {
2257 let data = RefAny::new(wrapper(""));
2263 let (_, _, _) = run(Env::typed("😀"), &data, default_on_text_input_inner);
2264
2265 let state = read(&data);
2266 assert_eq!(state.text.len(), 1, "one char went into the buffer");
2267 assert_eq!(state.cursor_pos, 4, "but the cursor moved by four bytes");
2268 assert!(
2269 state.cursor_pos > state.text.len(),
2270 "the cursor is left past the end of the buffer"
2271 );
2272
2273 let ascii = RefAny::new(wrapper(""));
2275 let (_, _, _) = run(Env::typed("abcd"), &ascii, default_on_text_input_inner);
2276 let ascii_state = read(&ascii);
2277 assert_eq!(ascii_state.cursor_pos, ascii_state.text.len());
2278 }
2279
2280 #[test]
2281 fn text_input_does_not_enforce_max_len() {
2282 let data = RefAny::new(wrapper("ab"));
2285 poke(&data, |w| w.inner.max_len = 2);
2286
2287 let (out, _, _) = run(Env::typed("cdefgh"), &data, default_on_text_input_inner);
2288
2289 assert_eq!(out, Some(Update::DoNothing));
2290 assert_eq!(read(&data).get_text(), "abcdefgh");
2291 assert_eq!(read(&data).max_len, 2, "the limit is stored, just not applied");
2292 }
2293
2294 #[test]
2295 fn text_input_shows_the_hook_the_would_be_text_before_committing() {
2296 let log = edit_log(ACCEPT);
2297 let mut state = wrapper("old");
2298 state.on_text_input = Some(TextAreaOnTextInput {
2299 callback: (record_text_input as TextAreaOnTextInputCallbackType).into(),
2300 refany: log.clone(),
2301 })
2302 .into();
2303 let data = RefAny::new(state);
2304
2305 let (out, _, _) = run(Env::typed("+new"), &data, default_on_text_input_inner);
2306
2307 assert_eq!(out, Some(Update::RefreshDom), "the hook's Update wins");
2308 let seen = edits_seen(&log);
2309 assert_eq!(seen.len(), 1);
2310 assert_eq!(
2311 seen[0].get_text(),
2312 "old+new",
2313 "the hook is shown the text as it *would* be after the edit"
2314 );
2315 assert_eq!(read(&data).get_text(), "old+new");
2316 }
2317
2318 #[test]
2319 fn text_input_rejected_by_the_hook_changes_nothing() {
2320 let log = edit_log(REJECT);
2321 let mut state = wrapper("locked");
2322 state.on_text_input = Some(TextAreaOnTextInput {
2323 callback: (record_text_input as TextAreaOnTextInputCallbackType).into(),
2324 refany: log.clone(),
2325 })
2326 .into();
2327 let data = RefAny::new(state);
2328
2329 let (out, changes, _) = run(Env::typed("nope"), &data, default_on_text_input_inner);
2330
2331 assert_eq!(
2332 out,
2333 Some(Update::RefreshDomAllWindows),
2334 "a rejected edit still returns the hook's Update"
2335 );
2336 assert!(changes.is_empty(), "a rejected edit must not repaint");
2337 let state = read(&data);
2338 assert_eq!(state.get_text(), "locked");
2339 assert_eq!(state.cursor_pos, 0, "and must not move the cursor");
2340 assert_eq!(edits_seen(&log).len(), 1, "the hook still ran exactly once");
2341 }
2342
2343 #[test]
2344 fn text_input_accumulates_across_edits() {
2345 let data = RefAny::new(wrapper(""));
2346 for chunk in ["a", "b\n", "c"] {
2347 let (out, _, _) = run(Env::typed(chunk), &data, default_on_text_input_inner);
2348 assert_eq!(out, Some(Update::DoNothing));
2349 }
2350
2351 let state = read(&data);
2352 assert_eq!(state.get_text(), "ab\nc");
2353 assert_eq!(state.cursor_pos, 4);
2354 }
2355
2356 #[test]
2357 fn text_input_writes_the_filtered_text_not_the_raw_buffer() {
2358 let data = RefAny::new(wrapper("a"));
2361 poke(&data, |w| {
2362 let mut units = w.inner.text.clone().into_library_owned_vec();
2363 units.extend(NON_SCALAR);
2364 w.inner.text = units.into();
2365 });
2366
2367 let (out, changes, nodes) = run(Env::typed("b"), &data, default_on_text_input_inner);
2368
2369 assert_eq!(out, Some(Update::DoNothing));
2370 assert_eq!(
2371 text_writes(&changes),
2372 vec![(nodes.label, "ab".to_string())],
2373 "only the scalars are rendered"
2374 );
2375 assert_eq!(
2376 read(&data).text.len(),
2377 NON_SCALAR.len() + 2,
2378 "the raw buffer keeps everything"
2379 );
2380 }
2381
2382 #[test]
2383 fn text_input_extern_wrapper_maps_none_onto_do_nothing() {
2384 let data = RefAny::new(wrapper("abc"));
2385 let (update, changes, _) = run(Env::default(), &data, |r, ci| default_on_text_input(r, ci));
2386
2387 assert_eq!(update, Update::DoNothing);
2388 assert!(changes.is_empty());
2389 }
2390
2391 #[test]
2392 fn text_input_survives_a_very_large_insertion() {
2393 let big: String = "chunk 😀\n".repeat(10_000);
2394 let data = RefAny::new(wrapper(""));
2395
2396 let (out, changes, nodes) = run(Env::typed(&big), &data, default_on_text_input_inner);
2397
2398 assert_eq!(out, Some(Update::DoNothing));
2399 assert_eq!(read(&data).text.len(), big.chars().count());
2400 assert_eq!(text_writes(&changes), vec![(nodes.label, big)]);
2401 }
2402
2403 #[test]
2408 fn virtual_key_down_without_a_keycode_does_nothing() {
2409 let data = RefAny::new(wrapper("abc"));
2410 let (out, changes, _) = run(Env::default(), &data, default_on_virtual_key_down_inner);
2411
2412 assert_eq!(out, None);
2413 assert!(changes.is_empty());
2414 assert_eq!(read(&data).get_text(), "abc");
2415 }
2416
2417 #[test]
2418 fn virtual_key_down_ignores_a_foreign_payload() {
2419 let data = RefAny::new(0i64);
2420 let (out, changes, _) = run(
2421 Env::key(VirtualKeyCode::Back),
2422 &data,
2423 default_on_virtual_key_down_inner,
2424 );
2425
2426 assert_eq!(out, None);
2427 assert!(changes.is_empty());
2428 }
2429
2430 #[test]
2431 fn virtual_key_down_bails_out_on_a_childless_hit_node() {
2432 let log = edit_log(ACCEPT);
2433 let mut state = wrapper("abc");
2434 state.on_virtual_key_down = Some(TextAreaOnVirtualKeyDown {
2435 callback: (record_virtual_key as TextAreaOnVirtualKeyDownCallbackType).into(),
2436 refany: log.clone(),
2437 })
2438 .into();
2439 let data = RefAny::new(state);
2440
2441 let (out, changes, _) = run(
2442 Env::key(VirtualKeyCode::Back).hitting(Hit::Cursor),
2443 &data,
2444 default_on_virtual_key_down_inner,
2445 );
2446
2447 assert_eq!(out, None);
2448 assert!(changes.is_empty());
2449 assert_eq!(read(&data).get_text(), "abc");
2450 assert!(
2451 edits_seen(&log).is_empty(),
2452 "the DOM walk precedes the hook, so it never ran"
2453 );
2454 }
2455
2456 #[test]
2457 fn backspace_on_an_empty_buffer_does_not_underflow() {
2458 let data = RefAny::new(wrapper(""));
2459 let (out, changes, nodes) = run(
2460 Env::key(VirtualKeyCode::Back),
2461 &data,
2462 default_on_virtual_key_down_inner,
2463 );
2464
2465 assert_eq!(out, Some(Update::DoNothing));
2466 let state = read(&data);
2467 assert!(state.text.is_empty());
2468 assert_eq!(state.cursor_pos, 0, "saturating_sub must hold the floor");
2469 assert_eq!(text_writes(&changes), vec![(nodes.label, String::new())]);
2471 assert_eq!(opacity_writes(&changes), vec![(nodes.placeholder, 1.0)]);
2472 }
2473
2474 #[test]
2475 fn backspace_removes_exactly_one_char() {
2476 for (before, after) in [
2477 ("abc", "ab"),
2478 ("a", ""),
2479 ("a\n", "a"),
2480 ("😀😀", "😀"), ("e\u{301}", "e"), ("日本語", "日本"),
2483 ] {
2484 let data = RefAny::new(wrapper(before));
2485 let (out, changes, nodes) = run(
2486 Env::key(VirtualKeyCode::Back),
2487 &data,
2488 default_on_virtual_key_down_inner,
2489 );
2490
2491 assert_eq!(out, Some(Update::DoNothing));
2492 let state = read(&data);
2493 assert_eq!(state.get_text(), after, "backspace on {before:?}");
2494 assert_eq!(state.text.len(), after.chars().count());
2495 assert_eq!(text_writes(&changes), vec![(nodes.label, after.to_string())]);
2496 }
2497 }
2498
2499 #[test]
2500 fn backspace_re_shows_the_placeholder_only_once_the_buffer_empties() {
2501 let data = RefAny::new(wrapper("ab"));
2502
2503 let (_, changes, _) = run(
2504 Env::key(VirtualKeyCode::Back),
2505 &data,
2506 default_on_virtual_key_down_inner,
2507 );
2508 assert!(
2509 opacity_writes(&changes).is_empty(),
2510 "still one char left — the placeholder stays hidden"
2511 );
2512
2513 let (_, changes, nodes) = run(
2514 Env::key(VirtualKeyCode::Back),
2515 &data,
2516 default_on_virtual_key_down_inner,
2517 );
2518 assert_eq!(opacity_writes(&changes), vec![(nodes.placeholder, 1.0)]);
2519 }
2520
2521 #[test]
2522 fn backspace_clamps_the_cursor_instead_of_wrapping() {
2523 let data = RefAny::new(wrapper("abc"));
2524 poke(&data, |w| w.inner.cursor_pos = 0);
2525
2526 let (_, _, _) = run(
2527 Env::key(VirtualKeyCode::Back),
2528 &data,
2529 default_on_virtual_key_down_inner,
2530 );
2531
2532 assert_eq!(
2533 read(&data).cursor_pos,
2534 0,
2535 "0 - 1 must saturate, never wrap to usize::MAX"
2536 );
2537 }
2538
2539 #[test]
2540 fn backspace_pops_a_non_scalar_unit_too() {
2541 let data = RefAny::new(wrapper("ab"));
2542 poke(&data, |w| {
2543 let mut units = w.inner.text.clone().into_library_owned_vec();
2544 units.push(0xD800); w.inner.text = units.into();
2546 });
2547
2548 let (_, changes, nodes) = run(
2549 Env::key(VirtualKeyCode::Back),
2550 &data,
2551 default_on_virtual_key_down_inner,
2552 );
2553
2554 let state = read(&data);
2555 assert_eq!(state.text.len(), 2, "the surrogate was the unit popped");
2556 assert_eq!(state.get_text(), "ab");
2557 assert_eq!(text_writes(&changes), vec![(nodes.label, "ab".to_string())]);
2558 }
2559
2560 #[test]
2561 fn return_inserts_a_newline() {
2562 let data = RefAny::new(wrapper("line"));
2563 let (out, changes, nodes) = run(
2564 Env::key(VirtualKeyCode::Return),
2565 &data,
2566 default_on_virtual_key_down_inner,
2567 );
2568
2569 assert_eq!(out, Some(Update::DoNothing));
2570 let state = read(&data);
2571 assert_eq!(state.get_text(), "line\n");
2572 assert_eq!(state.cursor_pos, 1, "cursor_pos started at 0 and moved by one");
2573 assert_eq!(text_writes(&changes), vec![(nodes.label, "line\n".to_string())]);
2574 assert_eq!(
2575 opacity_writes(&changes),
2576 vec![(nodes.placeholder, 0.0)],
2577 "the buffer is non-empty, so the placeholder must be hidden"
2578 );
2579 }
2580
2581 #[test]
2582 fn numpad_enter_behaves_exactly_like_return() {
2583 let via_return = RefAny::new(wrapper("x"));
2584 let (a_out, a_changes, _) = run(
2585 Env::key(VirtualKeyCode::Return),
2586 &via_return,
2587 default_on_virtual_key_down_inner,
2588 );
2589
2590 let via_numpad = RefAny::new(wrapper("x"));
2591 let (b_out, b_changes, _) = run(
2592 Env::key(VirtualKeyCode::NumpadEnter),
2593 &via_numpad,
2594 default_on_virtual_key_down_inner,
2595 );
2596
2597 assert_eq!(a_out, b_out);
2598 assert_eq!(read(&via_return), read(&via_numpad));
2599 assert_eq!(text_writes(&a_changes), text_writes(&b_changes));
2600 assert_eq!(opacity_writes(&a_changes), opacity_writes(&b_changes));
2601 }
2602
2603 #[test]
2604 fn repeated_returns_accumulate_blank_lines() {
2605 let data = RefAny::new(wrapper(""));
2606 for _ in 0..5 {
2607 let (out, _, _) = run(
2608 Env::key(VirtualKeyCode::Return),
2609 &data,
2610 default_on_virtual_key_down_inner,
2611 );
2612 assert_eq!(out, Some(Update::DoNothing));
2613 }
2614
2615 let state = read(&data);
2616 assert_eq!(state.get_text(), "\n\n\n\n\n");
2617 assert_eq!(state.cursor_pos, 5);
2618 }
2619
2620 #[test]
2621 fn a_plain_character_key_is_left_to_the_text_input_path() {
2622 for key in [
2625 VirtualKeyCode::A,
2626 VirtualKeyCode::Space,
2627 VirtualKeyCode::Tab,
2628 VirtualKeyCode::Escape,
2629 VirtualKeyCode::Left,
2630 ] {
2631 let data = RefAny::new(wrapper("abc"));
2632 let (out, changes, _) = run(Env::key(key), &data, default_on_virtual_key_down_inner);
2633
2634 assert_eq!(out, Some(Update::DoNothing), "{key:?}");
2635 assert!(changes.is_empty(), "{key:?} must not repaint");
2636 assert_eq!(read(&data).get_text(), "abc", "{key:?} must not edit");
2637 }
2638 }
2639
2640 #[test]
2641 fn the_hook_runs_even_for_keys_that_do_not_edit() {
2642 let log = edit_log(ACCEPT);
2643 let mut state = wrapper("abc");
2644 state.on_virtual_key_down = Some(TextAreaOnVirtualKeyDown {
2645 callback: (record_virtual_key as TextAreaOnVirtualKeyDownCallbackType).into(),
2646 refany: log.clone(),
2647 })
2648 .into();
2649 let data = RefAny::new(state);
2650
2651 let (out, changes, _) = run(
2652 Env::key(VirtualKeyCode::F1),
2653 &data,
2654 default_on_virtual_key_down_inner,
2655 );
2656
2657 assert_eq!(out, Some(Update::RefreshDom), "the hook's Update is returned");
2658 assert!(changes.is_empty());
2659 let seen = edits_seen(&log);
2660 assert_eq!(seen.len(), 1);
2661 assert_eq!(seen[0].get_text(), "abc", "the hook sees the pre-edit state");
2662 }
2663
2664 #[test]
2665 fn a_rejecting_hook_suppresses_the_built_in_editing() {
2666 for key in [VirtualKeyCode::Back, VirtualKeyCode::Return] {
2667 let log = edit_log(REJECT);
2668 let mut state = wrapper("frozen");
2669 state.on_virtual_key_down = Some(TextAreaOnVirtualKeyDown {
2670 callback: (record_virtual_key as TextAreaOnVirtualKeyDownCallbackType).into(),
2671 refany: log.clone(),
2672 })
2673 .into();
2674 let data = RefAny::new(state);
2675
2676 let (out, changes, _) = run(Env::key(key), &data, default_on_virtual_key_down_inner);
2677
2678 assert_eq!(out, Some(Update::RefreshDomAllWindows), "{key:?}");
2679 assert!(changes.is_empty(), "{key:?} must not repaint");
2680 assert_eq!(read(&data).get_text(), "frozen", "{key:?} must not edit");
2681 assert_eq!(edits_seen(&log).len(), 1);
2682 }
2683 }
2684
2685 #[test]
2686 fn an_accepting_hook_lets_the_edit_through_and_still_sets_the_update() {
2687 let log = edit_log(ACCEPT);
2688 let mut state = wrapper("ab");
2689 state.on_virtual_key_down = Some(TextAreaOnVirtualKeyDown {
2690 callback: (record_virtual_key as TextAreaOnVirtualKeyDownCallbackType).into(),
2691 refany: log.clone(),
2692 })
2693 .into();
2694 let data = RefAny::new(state);
2695
2696 let (out, changes, nodes) = run(
2697 Env::key(VirtualKeyCode::Back),
2698 &data,
2699 default_on_virtual_key_down_inner,
2700 );
2701
2702 assert_eq!(out, Some(Update::RefreshDom));
2703 assert_eq!(read(&data).get_text(), "a");
2704 assert_eq!(text_writes(&changes), vec![(nodes.label, "a".to_string())]);
2705 }
2706
2707 #[test]
2708 fn virtual_key_down_extern_wrapper_maps_none_onto_do_nothing() {
2709 let data = RefAny::new(wrapper("abc"));
2710 let (update, changes, _) = run(Env::default(), &data, |r, ci| default_on_virtual_key_down(r, ci));
2711
2712 assert_eq!(update, Update::DoNothing);
2713 assert!(changes.is_empty());
2714 }
2715
2716 #[test]
2717 fn typing_then_editing_keeps_buffer_and_label_in_agreement() {
2718 let data = RefAny::new(wrapper(""));
2721
2722 let (_, changes, nodes) = run(Env::typed("hello"), &data, default_on_text_input_inner);
2723 assert_eq!(text_writes(&changes), vec![(nodes.label, "hello".to_string())]);
2724
2725 let (_, changes, nodes) = run(
2726 Env::key(VirtualKeyCode::Return),
2727 &data,
2728 default_on_virtual_key_down_inner,
2729 );
2730 assert_eq!(text_writes(&changes), vec![(nodes.label, "hello\n".to_string())]);
2731
2732 let (_, changes, nodes) = run(Env::typed("world"), &data, default_on_text_input_inner);
2733 assert_eq!(
2734 text_writes(&changes),
2735 vec![(nodes.label, "hello\nworld".to_string())]
2736 );
2737
2738 let (_, changes, nodes) = run(
2739 Env::key(VirtualKeyCode::Back),
2740 &data,
2741 default_on_virtual_key_down_inner,
2742 );
2743 assert_eq!(
2744 text_writes(&changes),
2745 vec![(nodes.label, "hello\nworl".to_string())]
2746 );
2747
2748 assert_eq!(read(&data).get_text(), "hello\nworl");
2749 }
2750
2751 fn before_first_layout() -> Env {
2758 Env {
2759 with_dom: false,
2760 ..Env::default()
2761 }
2762 }
2763
2764 #[test]
2765 fn focus_received_is_inert_before_the_first_layout() {
2766 let data = RefAny::new(wrapper(""));
2767 poke(&data, |w| w.inner.cursor_pos = 5);
2768
2769 let (update, changes, _) = run(before_first_layout(), &data, |r, ci| default_on_focus_received(r, ci));
2770
2771 assert_eq!(update, Update::DoNothing);
2772 assert!(changes.is_empty());
2773 assert_eq!(read(&data).cursor_pos, 5, "the cursor is not repaired either");
2774 }
2775
2776 #[test]
2777 fn focus_lost_is_inert_before_the_first_layout() {
2778 let log = focus_log(Update::RefreshDom);
2779 let mut state = wrapper("");
2780 state.on_focus_lost = Some(TextAreaOnFocusLost {
2781 callback: (record_focus_lost as TextAreaOnFocusLostCallbackType).into(),
2782 refany: log.clone(),
2783 })
2784 .into();
2785 let data = RefAny::new(state);
2786
2787 let (update, changes, _) = run(before_first_layout(), &data, |r, ci| default_on_focus_lost(r, ci));
2788
2789 assert_eq!(update, Update::DoNothing);
2790 assert!(changes.is_empty());
2791 assert!(focus_seen(&log).is_empty());
2792 }
2793
2794 #[test]
2795 fn text_input_is_inert_before_the_first_layout() {
2796 let data = RefAny::new(wrapper("abc"));
2797 let env = Env {
2798 with_dom: false,
2799 ..Env::typed("xyz")
2800 };
2801
2802 let (out, changes, _) = run(env, &data, default_on_text_input_inner);
2803
2804 assert_eq!(out, None);
2805 assert!(changes.is_empty());
2806 assert_eq!(read(&data).get_text(), "abc", "no DOM to walk, no edit");
2807 }
2808
2809 #[test]
2810 fn virtual_key_down_is_inert_before_the_first_layout() {
2811 for key in [VirtualKeyCode::Back, VirtualKeyCode::Return] {
2812 let data = RefAny::new(wrapper("abc"));
2813 let env = Env {
2814 with_dom: false,
2815 ..Env::key(key)
2816 };
2817
2818 let (out, changes, _) = run(env, &data, default_on_virtual_key_down_inner);
2819
2820 assert_eq!(out, None, "{key:?}");
2821 assert!(changes.is_empty(), "{key:?}");
2822 assert_eq!(read(&data).get_text(), "abc", "{key:?}");
2823 }
2824 }
2825}