1use alloc::{string::String, vec::Vec};
11
12use azul_core::{
13 callbacks::{CoreCallback, CoreCallbackData, Update},
14 dom::Dom,
15 refany::RefAny,
16 task::OptionTimerId,
17 window::VirtualKeyCode,
18};
19#[allow(clippy::wildcard_imports)] use azul_css::{
21 dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
22 props::{
23 basic::*,
24 layout::*,
25 property::{CssProperty, *},
26 style::*,
27 },
28 *,
29};
30use azul_css::css::BoxOrStatic;
31
32use crate::callbacks::{Callback, CallbackInfo};
33
34const BACKGROUND_COLOR: ColorU = ColorU {
35 r: 255,
36 g: 255,
37 b: 255,
38 a: 255,
39}; const BLACK: ColorU = ColorU {
41 r: 0,
42 g: 0,
43 b: 0,
44 a: 255,
45};
46const TEXT_COLOR: StyleTextColor = StyleTextColor { inner: BLACK }; const COLOR_9B9B9B: ColorU = ColorU {
48 r: 155,
49 g: 155,
50 b: 155,
51 a: 255,
52}; const COLOR_4286F4: ColorU = ColorU {
54 r: 66,
55 g: 134,
56 b: 244,
57 a: 255,
58}; const COLOR_4C4C4C: ColorU = ColorU {
60 r: 76,
61 g: 76,
62 b: 76,
63 a: 255,
64}; const CURSOR_COLOR_BLACK: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(BLACK)];
67const CURSOR_COLOR: StyleBackgroundContentVec =
68 StyleBackgroundContentVec::from_const_slice(CURSOR_COLOR_BLACK);
69
70const BACKGROUND_THEME_LIGHT: &[StyleBackgroundContent] =
71 &[StyleBackgroundContent::Color(BACKGROUND_COLOR)];
72const BACKGROUND_COLOR_LIGHT: StyleBackgroundContentVec =
73 StyleBackgroundContentVec::from_const_slice(BACKGROUND_THEME_LIGHT);
74
75const SANS_SERIF_STR: &str = "system:ui";
76const SANS_SERIF: AzString = AzString::from_const_str(SANS_SERIF_STR);
77const SANS_SERIF_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SANS_SERIF)];
78const SANS_SERIF_FAMILY: StyleFontFamilyVec =
79 StyleFontFamilyVec::from_const_slice(SANS_SERIF_FAMILIES);
80
81const TEXT_CURSOR_TRANSFORM: &[StyleTransform] =
84 &[StyleTransform::Translate(StyleTransformTranslate2D {
85 x: PixelValue::const_px(0),
86 y: PixelValue::const_px(2),
87 })];
88
89static TEXT_CURSOR_PROPS: &[CssPropertyWithConditions] = &[
90 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
91 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(1))),
92 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(11))),
93 CssPropertyWithConditions::simple(CssProperty::const_background_content(CURSOR_COLOR)),
94 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
95 CssPropertyWithConditions::simple(CssProperty::const_transform(
96 StyleTransformVec::from_const_slice(TEXT_CURSOR_TRANSFORM),
97 )),
98];
99
100#[cfg(target_os = "windows")]
103static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
104 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
105 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
106 CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
107 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
108 CssPropertyWithConditions::simple(CssProperty::const_background_content(
109 BACKGROUND_COLOR_LIGHT,
110 )),
111 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
112 inner: COLOR_4C4C4C,
113 })),
114 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
115 LayoutPaddingLeft::const_px(2),
116 )),
117 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
118 LayoutPaddingRight::const_px(2),
119 )),
120 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
121 1,
122 ))),
123 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
124 LayoutPaddingBottom::const_px(1),
125 )),
126 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
128 LayoutBorderTopWidth::const_px(1),
129 )),
130 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
131 LayoutBorderBottomWidth::const_px(1),
132 )),
133 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
134 LayoutBorderLeftWidth::const_px(1),
135 )),
136 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
137 LayoutBorderRightWidth::const_px(1),
138 )),
139 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
140 inner: BorderStyle::Inset,
141 })),
142 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
143 StyleBorderBottomStyle {
144 inner: BorderStyle::Inset,
145 },
146 )),
147 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
148 inner: BorderStyle::Inset,
149 })),
150 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
151 StyleBorderRightStyle {
152 inner: BorderStyle::Inset,
153 },
154 )),
155 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
156 inner: COLOR_9B9B9B,
157 })),
158 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
159 StyleBorderBottomColor {
160 inner: COLOR_9B9B9B,
161 },
162 )),
163 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
164 inner: COLOR_9B9B9B,
165 })),
166 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
167 StyleBorderRightColor {
168 inner: COLOR_9B9B9B,
169 },
170 )),
171 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
172 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
173 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
174 LayoutJustifyContent::Center,
175 )),
176 CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
178 inner: COLOR_4C4C4C,
179 })),
180 CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
181 StyleBorderBottomColor {
182 inner: COLOR_4C4C4C,
183 },
184 )),
185 CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
186 StyleBorderLeftColor {
187 inner: COLOR_4C4C4C,
188 },
189 )),
190 CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
191 StyleBorderRightColor {
192 inner: COLOR_4C4C4C,
193 },
194 )),
195 CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
197 inner: COLOR_4286F4,
198 })),
199 CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
200 StyleBorderBottomColor {
201 inner: COLOR_4286F4,
202 },
203 )),
204 CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
205 StyleBorderLeftColor {
206 inner: COLOR_4286F4,
207 },
208 )),
209 CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
210 StyleBorderRightColor {
211 inner: COLOR_4286F4,
212 },
213 )),
214];
215
216#[cfg(target_os = "linux")]
217static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
218 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
219 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
220 CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
221 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
222 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
223 CssPropertyWithConditions::simple(CssProperty::const_background_content(
224 BACKGROUND_COLOR_LIGHT,
225 )),
226 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
227 inner: COLOR_4C4C4C,
228 })),
229 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
230 LayoutPaddingLeft::const_px(2),
231 )),
232 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
233 LayoutPaddingRight::const_px(2),
234 )),
235 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
236 1,
237 ))),
238 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
239 LayoutPaddingBottom::const_px(1),
240 )),
241 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
243 LayoutBorderTopWidth::const_px(1),
244 )),
245 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
246 LayoutBorderBottomWidth::const_px(1),
247 )),
248 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
249 LayoutBorderLeftWidth::const_px(1),
250 )),
251 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
252 LayoutBorderRightWidth::const_px(1),
253 )),
254 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
255 inner: BorderStyle::Inset,
256 })),
257 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
258 StyleBorderBottomStyle {
259 inner: BorderStyle::Inset,
260 },
261 )),
262 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
263 inner: BorderStyle::Inset,
264 })),
265 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
266 StyleBorderRightStyle {
267 inner: BorderStyle::Inset,
268 },
269 )),
270 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
271 inner: COLOR_9B9B9B,
272 })),
273 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
274 StyleBorderBottomColor {
275 inner: COLOR_9B9B9B,
276 },
277 )),
278 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
279 inner: COLOR_9B9B9B,
280 })),
281 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
282 StyleBorderRightColor {
283 inner: COLOR_9B9B9B,
284 },
285 )),
286 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
287 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
288 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
289 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
290 LayoutJustifyContent::Center,
291 )),
292 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
293 CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
295 inner: COLOR_4286F4,
296 })),
297 CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
298 StyleBorderBottomColor {
299 inner: COLOR_4286F4,
300 },
301 )),
302 CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
303 StyleBorderLeftColor {
304 inner: COLOR_4286F4,
305 },
306 )),
307 CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
308 StyleBorderRightColor {
309 inner: COLOR_4286F4,
310 },
311 )),
312 CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
314 inner: COLOR_4286F4,
315 })),
316 CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
317 StyleBorderBottomColor {
318 inner: COLOR_4286F4,
319 },
320 )),
321 CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
322 StyleBorderLeftColor {
323 inner: COLOR_4286F4,
324 },
325 )),
326 CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
327 StyleBorderRightColor {
328 inner: COLOR_4286F4,
329 },
330 )),
331];
332
333#[cfg(not(any(target_os = "windows", target_os = "linux")))]
336static TEXT_INPUT_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
337 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
338 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Text)),
339 CssPropertyWithConditions::simple(CssProperty::const_box_sizing(LayoutBoxSizing::BorderBox)),
340 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
341 CssPropertyWithConditions::simple(CssProperty::const_background_content(
342 BACKGROUND_COLOR_LIGHT,
343 )),
344 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
345 inner: COLOR_4C4C4C,
346 })),
347 CssPropertyWithConditions::simple(CssProperty::const_padding_left(
348 LayoutPaddingLeft::const_px(2),
349 )),
350 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
351 LayoutPaddingRight::const_px(2),
352 )),
353 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
354 1,
355 ))),
356 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
357 LayoutPaddingBottom::const_px(1),
358 )),
359 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
361 LayoutBorderTopWidth::const_px(1),
362 )),
363 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
364 LayoutBorderBottomWidth::const_px(1),
365 )),
366 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
367 LayoutBorderLeftWidth::const_px(1),
368 )),
369 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
370 LayoutBorderRightWidth::const_px(1),
371 )),
372 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
373 inner: BorderStyle::Inset,
374 })),
375 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
376 StyleBorderBottomStyle {
377 inner: BorderStyle::Inset,
378 },
379 )),
380 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
381 inner: BorderStyle::Inset,
382 })),
383 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
384 StyleBorderRightStyle {
385 inner: BorderStyle::Inset,
386 },
387 )),
388 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
389 inner: COLOR_9B9B9B,
390 })),
391 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
392 StyleBorderBottomColor {
393 inner: COLOR_9B9B9B,
394 },
395 )),
396 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
397 inner: COLOR_9B9B9B,
398 })),
399 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
400 StyleBorderRightColor {
401 inner: COLOR_9B9B9B,
402 },
403 )),
404 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
405 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
406 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
407 CssPropertyWithConditions::simple(CssProperty::const_justify_content(
408 LayoutJustifyContent::Center,
409 )),
410 CssPropertyWithConditions::on_hover(CssProperty::const_border_top_color(StyleBorderTopColor {
412 inner: COLOR_4286F4,
413 })),
414 CssPropertyWithConditions::on_hover(CssProperty::const_border_bottom_color(
415 StyleBorderBottomColor {
416 inner: COLOR_4286F4,
417 },
418 )),
419 CssPropertyWithConditions::on_hover(CssProperty::const_border_left_color(
420 StyleBorderLeftColor {
421 inner: COLOR_4286F4,
422 },
423 )),
424 CssPropertyWithConditions::on_hover(CssProperty::const_border_right_color(
425 StyleBorderRightColor {
426 inner: COLOR_4286F4,
427 },
428 )),
429 CssPropertyWithConditions::on_focus(CssProperty::const_border_top_color(StyleBorderTopColor {
431 inner: COLOR_4286F4,
432 })),
433 CssPropertyWithConditions::on_focus(CssProperty::const_border_bottom_color(
434 StyleBorderBottomColor {
435 inner: COLOR_4286F4,
436 },
437 )),
438 CssPropertyWithConditions::on_focus(CssProperty::const_border_left_color(
439 StyleBorderLeftColor {
440 inner: COLOR_4286F4,
441 },
442 )),
443 CssPropertyWithConditions::on_focus(CssProperty::const_border_right_color(
444 StyleBorderRightColor {
445 inner: COLOR_4286F4,
446 },
447 )),
448];
449
450#[cfg(target_os = "windows")]
453static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
454 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
455 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
456 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
457 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
458 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
459 inner: COLOR_4C4C4C,
460 })),
461 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
462];
463
464#[cfg(target_os = "linux")]
465static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
466 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
467 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
468 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
469 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
470 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
471 inner: COLOR_4C4C4C,
472 })),
473 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
474];
475
476#[cfg(not(any(target_os = "windows", target_os = "linux")))]
477static TEXT_INPUT_LABEL_PROPS: &[CssPropertyWithConditions] = &[
478 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
479 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
480 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
481 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
482 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
483 inner: COLOR_4C4C4C,
484 })),
485 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
486];
487
488#[cfg(target_os = "windows")]
491static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
492 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
493 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
494 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
495 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
496 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
497 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
498 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
499 inner: COLOR_4C4C4C,
500 })),
501 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
502 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
503];
504
505#[cfg(target_os = "linux")]
506static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
507 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
508 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
509 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
510 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
511 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
512 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
513 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
514 inner: COLOR_4C4C4C,
515 })),
516 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
517 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
518];
519
520#[cfg(not(any(target_os = "windows", target_os = "linux")))]
521static TEXT_INPUT_PLACEHOLDER_PROPS: &[CssPropertyWithConditions] = &[
522 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
523 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
524 CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
525 CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(2))),
526 CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(2))),
527 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
528 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
529 inner: COLOR_4C4C4C,
530 })),
531 CssPropertyWithConditions::simple(CssProperty::const_font_family(SANS_SERIF_FAMILY)),
532 CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(100))),
533];
534
535#[derive(Debug, Clone, PartialEq, Eq)]
541#[repr(C)]
542pub struct TextInput {
543 pub text_input_state: TextInputStateWrapper,
544 pub placeholder_style: CssPropertyWithConditionsVec,
545 pub container_style: CssPropertyWithConditionsVec,
546 pub label_style: CssPropertyWithConditionsVec,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq)]
551#[repr(C)]
552pub struct TextInputState {
553 pub text: U32Vec, pub placeholder: OptionString,
555 pub max_len: usize,
556 pub selection: OptionTextInputSelection,
557 pub cursor_pos: usize,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
562#[repr(C)]
563pub struct TextInputStateWrapper {
564 pub inner: TextInputState,
565 pub on_text_input: OptionTextInputOnTextInput,
566 pub on_virtual_key_down: OptionTextInputOnVirtualKeyDown,
567 pub on_focus_lost: OptionTextInputOnFocusLost,
568 pub update_text_input_before_calling_focus_lost_fn: bool,
569 pub update_text_input_before_calling_vk_down_fn: bool,
570 pub cursor_animation: OptionTimerId,
571}
572
573#[derive(Debug, Copy, Clone, PartialEq, Eq)]
576#[repr(C)]
577pub struct OnTextInputReturn {
578 pub update: Update,
579 pub valid: TextInputValid,
580}
581
582#[derive(Debug, Copy, Clone, PartialEq, Eq)]
584#[repr(C)]
585pub enum TextInputValid {
586 Yes,
587 No,
588}
589
590pub type TextInputOnTextInputCallbackType =
593 extern "C" fn(RefAny, CallbackInfo, TextInputState) -> OnTextInputReturn;
594impl_widget_callback!(
595 TextInputOnTextInput,
596 OptionTextInputOnTextInput,
597 TextInputOnTextInputCallback,
598 TextInputOnTextInputCallbackType
599);
600
601azul_core::impl_managed_callback! {
602 wrapper: TextInputOnTextInputCallback,
603 info_ty: CallbackInfo,
604 return_ty: OnTextInputReturn,
605 default_ret: OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
606 invoker_static: TEXT_INPUT_ON_TEXT_INPUT_INVOKER,
607 invoker_ty: AzTextInputOnTextInputCallbackInvoker,
608 thunk_fn: az_text_input_on_text_input_callback_thunk,
609 setter_fn: AzApp_setTextInputOnTextInputCallbackInvoker,
610 from_handle_fn: AzTextInputOnTextInputCallback_createFromHostHandle,
611 extra_args: [ state: TextInputState ],
612}
613
614pub type TextInputOnVirtualKeyDownCallbackType =
615 extern "C" fn(RefAny, CallbackInfo, TextInputState) -> OnTextInputReturn;
616impl_widget_callback!(
617 TextInputOnVirtualKeyDown,
618 OptionTextInputOnVirtualKeyDown,
619 TextInputOnVirtualKeyDownCallback,
620 TextInputOnVirtualKeyDownCallbackType
621);
622
623azul_core::impl_managed_callback! {
624 wrapper: TextInputOnVirtualKeyDownCallback,
625 info_ty: CallbackInfo,
626 return_ty: OnTextInputReturn,
627 default_ret: OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes },
628 invoker_static: TEXT_INPUT_ON_VIRTUAL_KEY_DOWN_INVOKER,
629 invoker_ty: AzTextInputOnVirtualKeyDownCallbackInvoker,
630 thunk_fn: az_text_input_on_virtual_key_down_callback_thunk,
631 setter_fn: AzApp_setTextInputOnVirtualKeyDownCallbackInvoker,
632 from_handle_fn: AzTextInputOnVirtualKeyDownCallback_createFromHostHandle,
633 extra_args: [ state: TextInputState ],
634}
635
636pub type TextInputOnFocusLostCallbackType =
637 extern "C" fn(RefAny, CallbackInfo, TextInputState) -> Update;
638impl_widget_callback!(
639 TextInputOnFocusLost,
640 OptionTextInputOnFocusLost,
641 TextInputOnFocusLostCallback,
642 TextInputOnFocusLostCallbackType
643);
644
645azul_core::impl_managed_callback! {
646 wrapper: TextInputOnFocusLostCallback,
647 info_ty: CallbackInfo,
648 return_ty: Update,
649 default_ret: Update::DoNothing,
650 invoker_static: TEXT_INPUT_ON_FOCUS_LOST_INVOKER,
651 invoker_ty: AzTextInputOnFocusLostCallbackInvoker,
652 thunk_fn: az_text_input_on_focus_lost_callback_thunk,
653 setter_fn: AzApp_setTextInputOnFocusLostCallbackInvoker,
654 from_handle_fn: AzTextInputOnFocusLostCallback_createFromHostHandle,
655 extra_args: [ state: TextInputState ],
656}
657#[allow(variant_size_differences)] #[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
659#[repr(C, u8)]
660pub enum TextInputSelection {
661 All,
662 FromTo(TextInputSelectionRange),
663}
664
665azul_css::impl_option!(
666 TextInputSelection,
667 OptionTextInputSelection,
668 copy = false,
669 [Debug, Clone, Hash, PartialEq, Eq]
670);
671
672#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
673#[repr(C)]
674pub struct TextInputSelectionRange {
675 pub dir_from: usize,
676 pub dir_to: usize,
677}
678
679impl Default for TextInput {
680 fn default() -> Self {
681 Self {
682 text_input_state: TextInputStateWrapper::default(),
683 placeholder_style: CssPropertyWithConditionsVec::from_const_slice(
684 TEXT_INPUT_PLACEHOLDER_PROPS,
685 ),
686 container_style: CssPropertyWithConditionsVec::from_const_slice(
687 TEXT_INPUT_CONTAINER_PROPS,
688 ),
689 label_style: CssPropertyWithConditionsVec::from_const_slice(TEXT_INPUT_LABEL_PROPS),
690 }
691 }
692}
693
694impl Default for TextInputState {
695 fn default() -> Self {
696 Self {
697 text: Vec::new().into(),
698 placeholder: None.into(),
699 max_len: 50,
700 selection: None.into(),
701 cursor_pos: 0,
702 }
703 }
704}
705
706impl TextInputState {
707 #[must_use] pub fn get_text(&self) -> String {
708 self.text
709 .iter()
710 .filter_map(|c| core::char::from_u32(*c))
711 .collect()
712 }
713}
714
715impl Default for TextInputStateWrapper {
716 fn default() -> Self {
717 Self {
718 inner: TextInputState::default(),
719 on_text_input: None.into(),
720 on_virtual_key_down: None.into(),
721 on_focus_lost: None.into(),
722 update_text_input_before_calling_focus_lost_fn: true,
723 update_text_input_before_calling_vk_down_fn: true,
724 cursor_animation: None.into(),
725 }
726 }
727}
728
729impl TextInput {
730 #[must_use] pub fn create() -> Self {
731 Self::default()
732 }
733
734 #[must_use] pub fn with_text(mut self, text: AzString) -> Self {
735 self.set_text(text);
736 self
737 }
738
739 #[allow(clippy::needless_pass_by_value)]
741 pub fn set_text(&mut self, text: AzString) {
742 self.text_input_state.inner.text = text
743 .as_str()
744 .chars()
745 .map(|c| c as u32)
746 .collect::<Vec<_>>()
747 .into();
748 }
749
750 pub fn set_placeholder(&mut self, placeholder: AzString) {
751 self.text_input_state.inner.placeholder = Some(placeholder).into();
752 }
753
754 #[must_use] pub fn with_placeholder(mut self, placeholder: AzString) -> Self {
755 self.set_placeholder(placeholder);
756 self
757 }
758
759 pub fn set_on_text_input<C: Into<TextInputOnTextInputCallback>>(
760 &mut self,
761 refany: RefAny,
762 callback: C,
763 ) {
764 self.text_input_state.on_text_input = Some(TextInputOnTextInput {
765 callback: callback.into(),
766 refany,
767 })
768 .into();
769 }
770
771 #[must_use]
772 pub fn with_on_text_input<C: Into<TextInputOnTextInputCallback>>(
773 mut self,
774 refany: RefAny,
775 callback: C,
776 ) -> Self {
777 self.set_on_text_input(refany, callback);
778 self
779 }
780
781 pub fn set_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
782 &mut self,
783 refany: RefAny,
784 callback: C,
785 ) {
786 self.text_input_state.on_virtual_key_down = Some(TextInputOnVirtualKeyDown {
787 callback: callback.into(),
788 refany,
789 })
790 .into();
791 }
792
793 #[must_use]
794 pub fn with_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
795 mut self,
796 refany: RefAny,
797 callback: C,
798 ) -> Self {
799 self.set_on_virtual_key_down(refany, callback);
800 self
801 }
802
803 pub fn set_on_focus_lost<C: Into<TextInputOnFocusLostCallback>>(
804 &mut self,
805 refany: RefAny,
806 callback: C,
807 ) {
808 self.text_input_state.on_focus_lost = Some(TextInputOnFocusLost {
809 callback: callback.into(),
810 refany,
811 })
812 .into();
813 }
814
815 #[must_use]
816 pub fn with_on_focus_lost<C: Into<TextInputOnFocusLostCallback>>(
817 mut self,
818 refany: RefAny,
819 callback: C,
820 ) -> Self {
821 self.set_on_focus_lost(refany, callback);
822 self
823 }
824
825 pub fn set_placeholder_style(&mut self, style: CssPropertyWithConditionsVec) {
826 self.placeholder_style = style;
827 }
828
829 #[must_use] pub fn with_placeholder_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
830 self.set_placeholder_style(style);
831 self
832 }
833
834 pub fn set_container_style(&mut self, style: CssPropertyWithConditionsVec) {
835 self.container_style = style;
836 }
837
838 #[must_use] pub fn with_container_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
839 self.set_container_style(style);
840 self
841 }
842
843 pub fn set_label_style(&mut self, style: CssPropertyWithConditionsVec) {
844 self.label_style = style;
845 }
846
847 #[must_use] pub fn with_label_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
848 self.set_label_style(style);
849 self
850 }
851
852 #[must_use]
853 pub fn swap_with_default(&mut self) -> Self {
854 let mut s = Self::default();
855 core::mem::swap(&mut s, self);
856 s
857 }
858
859 #[must_use] pub fn dom(mut self) -> Dom {
860 use azul_core::{
861 callbacks::CoreCallbackData,
862 dom::{EventFilter, FocusEventFilter, HoverEventFilter, IdOrClass::Class, TabIndex},
863 };
864
865 self.text_input_state.inner.cursor_pos = self.text_input_state.inner.text.len();
866
867 let label_text: String = self
868 .text_input_state
869 .inner
870 .text
871 .iter()
872 .filter_map(|s| core::char::from_u32(*s))
873 .collect();
874
875 let placeholder = self
876 .text_input_state
877 .inner
878 .placeholder
879 .as_ref()
880 .map(|s| s.as_str().to_string())
881 .unwrap_or_default();
882
883 let state_ref = RefAny::new(self.text_input_state);
884
885 Dom::create_div()
886 .with_ids_and_classes(vec![Class("__azul-native-text-input-container".into())].into())
887 .with_css_props(self.container_style)
888 .with_tab_index(TabIndex::Auto)
889 .with_dataset(Some(state_ref.clone()).into())
890 .with_callbacks(
891 vec![
892 CoreCallbackData {
893 event: EventFilter::Focus(FocusEventFilter::FocusReceived),
894 refany: state_ref.clone(),
895 callback: CoreCallback {
896 cb: default_on_focus_received as usize,
897 ctx: azul_core::refany::OptionRefAny::None,
898 },
899 },
900 CoreCallbackData {
901 event: EventFilter::Focus(FocusEventFilter::FocusLost),
902 refany: state_ref.clone(),
903 callback: CoreCallback {
904 cb: default_on_focus_lost as usize,
905 ctx: azul_core::refany::OptionRefAny::None,
906 },
907 },
908 CoreCallbackData {
909 event: EventFilter::Focus(FocusEventFilter::TextInput),
910 refany: state_ref.clone(),
911 callback: CoreCallback {
912 cb: default_on_text_input as usize,
913 ctx: azul_core::refany::OptionRefAny::None,
914 },
915 },
916 CoreCallbackData {
917 event: EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
918 refany: state_ref.clone(),
919 callback: CoreCallback {
920 cb: default_on_virtual_key_down as usize,
921 ctx: azul_core::refany::OptionRefAny::None,
922 },
923 },
924 CoreCallbackData {
925 event: EventFilter::Hover(HoverEventFilter::MouseOver),
926 refany: state_ref,
927 callback: CoreCallback {
928 cb: default_on_mouse_hover as usize,
929 ctx: azul_core::refany::OptionRefAny::None,
930 },
931 },
932 ]
933 .into(),
934 )
935 .with_children(
936 vec![
937 Dom::create_text(placeholder)
938 .with_ids_and_classes(
939 vec![Class("__azul-native-text-input-placeholder".into())].into(),
940 )
941 .with_css_props(self.placeholder_style),
942 Dom::create_text(label_text)
943 .with_ids_and_classes(
944 vec![Class("__azul-native-text-input-label".into())].into(),
945 )
946 .with_css_props(self.label_style)
947 .with_children(
948 vec![Dom::create_div()
949 .with_ids_and_classes(
950 vec![Class("__azul-native-text-input-cursor".into())].into(),
951 )
952 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
953 TEXT_CURSOR_PROPS,
954 ))]
955 .into(),
956 ),
957 ]
958 .into(),
959 )
960 }
961}
962
963extern "C" fn default_on_focus_received(mut text_input: RefAny, mut info: CallbackInfo) -> Update {
964 let Some(mut text_input) = text_input.downcast_mut::<TextInputStateWrapper>() else {
965 return Update::DoNothing;
966 };
967
968 let text_input = &mut *text_input;
969
970 let Some(placeholder_text_node_id) = info.get_first_child(info.get_hit_node()) else {
971 return Update::DoNothing;
972 };
973
974 if text_input.inner.text.is_empty() {
976 info.set_css_property(
977 placeholder_text_node_id,
978 CssProperty::const_opacity(StyleOpacity::const_new(0)),
979 );
980 }
981
982 text_input.inner.cursor_pos = text_input.inner.text.len();
983
984 Update::DoNothing
985}
986
987extern "C" fn default_on_focus_lost(mut text_input: RefAny, mut info: CallbackInfo) -> Update {
988 let Some(mut text_input) = text_input.downcast_mut::<TextInputStateWrapper>() else {
989 return Update::DoNothing;
990 };
991
992 let text_input = &mut *text_input;
993
994 let Some(placeholder_text_node_id) = info.get_first_child(info.get_hit_node()) else {
995 return Update::DoNothing;
996 };
997
998 if text_input.inner.text.is_empty() {
1000 info.set_css_property(
1001 placeholder_text_node_id,
1002 CssProperty::const_opacity(StyleOpacity::const_new(100)),
1003 );
1004 }
1005
1006 let text_input = &mut *text_input;
1008 let onfocuslost = &mut text_input.on_focus_lost;
1009 let inner = text_input.inner.clone();
1010
1011 match onfocuslost.as_mut() {
1012 Some(TextInputOnFocusLost { callback, refany }) => {
1013 (callback.cb)(refany.clone(), info, inner)
1014 }
1015 None => Update::DoNothing,
1016 }
1017}
1018
1019extern "C" fn default_on_text_input(text_input: RefAny, info: CallbackInfo) -> Update {
1020 default_on_text_input_inner(text_input, info).unwrap_or(Update::DoNothing)
1021}
1022
1023fn default_on_text_input_inner(mut text_input: RefAny, mut info: CallbackInfo) -> Option<Update> {
1024 let mut text_input = text_input.downcast_mut::<TextInputStateWrapper>()?;
1025
1026 let changeset = info.get_text_changeset()?;
1028 let inserted_text = changeset.inserted_text.as_str().to_string();
1029
1030 if inserted_text.is_empty() {
1032 return None;
1033 }
1034
1035 let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
1036 let label_node_id = info.get_next_sibling(placeholder_node_id)?;
1037 let _cursor_node_id = info.get_first_child(label_node_id)?;
1038
1039 let result = {
1040 let text_input = &mut *text_input;
1042 let ontextinput = &mut text_input.on_text_input;
1043
1044 let mut inner_clone = text_input.inner.clone();
1046 inner_clone.cursor_pos = inner_clone.cursor_pos.saturating_add(inserted_text.len());
1047 inner_clone.text = {
1048 let mut internal = inner_clone.text.clone().into_library_owned_vec();
1049 internal.extend(inserted_text.chars().map(|c| c as u32));
1050 internal.into()
1051 };
1052
1053 match ontextinput.as_mut() {
1054 Some(TextInputOnTextInput { callback, refany }) => {
1055 (callback.cb)(refany.clone(), info, inner_clone)
1056 }
1057 None => OnTextInputReturn {
1058 update: Update::DoNothing,
1059 valid: TextInputValid::Yes,
1060 },
1061 }
1062 };
1063
1064 if result.valid == TextInputValid::Yes {
1065 info.set_css_property(
1067 placeholder_node_id,
1068 CssProperty::const_opacity(StyleOpacity::const_new(0)),
1069 );
1070
1071 text_input.inner.text = {
1073 let mut internal = text_input.inner.text.clone().into_library_owned_vec();
1074 internal.extend(inserted_text.chars().map(|c| c as u32));
1075 internal.into()
1076 };
1077 text_input.inner.cursor_pos = text_input
1078 .inner
1079 .cursor_pos
1080 .saturating_add(inserted_text.len());
1081
1082 info.change_node_text(label_node_id, text_input.inner.get_text().into());
1083 }
1084
1085 Some(result.update)
1086}
1087
1088extern "C" fn default_on_virtual_key_down(text_input: RefAny, info: CallbackInfo) -> Update {
1089 default_on_virtual_key_down_inner(text_input, info).unwrap_or(Update::DoNothing)
1090}
1091
1092fn default_on_virtual_key_down_inner(
1093 mut text_input: RefAny,
1094 mut info: CallbackInfo,
1095) -> Option<Update> {
1096 let mut text_input = text_input.downcast_mut::<TextInputStateWrapper>()?;
1097 let keyboard_state = info.get_current_keyboard_state();
1098
1099 let c = keyboard_state.current_virtual_keycode.into_option()?;
1100 let placeholder_node_id = info.get_first_child(info.get_hit_node())?;
1101 let label_node_id = info.get_next_sibling(placeholder_node_id)?;
1102 let _cursor_node_id = info.get_first_child(label_node_id)?;
1103
1104 let result = {
1107 let text_input = &mut *text_input;
1109 let inner_clone = text_input.inner.clone();
1110 match text_input.on_virtual_key_down.as_mut() {
1111 Some(TextInputOnVirtualKeyDown { callback, refany }) => {
1112 (callback.cb)(refany.clone(), info, inner_clone)
1113 }
1114 None => OnTextInputReturn {
1115 update: Update::DoNothing,
1116 valid: TextInputValid::Yes,
1117 },
1118 }
1119 };
1120
1121 if result.valid == TextInputValid::Yes && c == VirtualKeyCode::Back {
1122 text_input.inner.text = {
1123 let mut internal = text_input.inner.text.clone().into_library_owned_vec();
1124 internal.pop();
1125 internal.into()
1126 };
1127 text_input.inner.cursor_pos = text_input.inner.cursor_pos.saturating_sub(1);
1128
1129 info.change_node_text(label_node_id, text_input.inner.get_text().into());
1130 }
1131
1132 Some(result.update)
1133}
1134
1135extern "C" fn default_on_mouse_hover(mut text_input: RefAny, _info: CallbackInfo) -> Update {
1136 let Some(_text_input) = text_input.downcast_mut::<TextInputStateWrapper>() else {
1137 return Update::DoNothing;
1138 };
1139
1140 Update::DoNothing
1141}
1142
1143#[cfg(all(test, feature = "std"))]
1144#[allow(clippy::too_many_lines, clippy::float_cmp)]
1145mod autotest_generated {
1146 use std::{
1147 collections::{BTreeMap, HashMap},
1148 sync::{Arc, Mutex},
1149 };
1150
1151 use azul_core::{
1152 dom::{
1153 DomId, DomNodeId, EventFilter, FocusEventFilter, HoverEventFilter, IdOrClass, NodeId,
1154 TabIndex,
1155 },
1156 geom::{LogicalRect, OptionLogicalPosition},
1157 gl::OptionGlContextPtr,
1158 hit_test::ScrollPosition,
1159 refany::OptionRefAny,
1160 resources::RendererResources,
1161 styled_dom::{NodeHierarchyItemId, StyledDom},
1162 window::{MonitorVec, RawWindowHandle},
1163 };
1164 use rust_fontconfig::FcFontCache;
1165
1166 use super::*;
1167 #[cfg(feature = "icu")]
1168 use crate::icu::IcuLocalizerHandle;
1169 use crate::{
1170 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
1171 managers::text_input::PendingTextEdit,
1172 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
1173 window::{DomLayoutResult, LayoutWindow},
1174 window_state::FullWindowState,
1175 };
1176
1177 const HOSTILE: [&str; 20] = [
1186 "",
1187 " ",
1188 "a",
1189 "hello world",
1190 "\0", "a\0b", "\u{7f}", "\u{80}", "\u{7ff}", "\u{800}", "\u{ffff}", "\u{10000}", "\u{10ffff}", "é",
1200 "e\u{301}", "👨👩👧👦", "日本語",
1203 "مرحبا", "\u{200b}", "\r\n\t",
1206 ];
1207
1208 const NON_SCALAR_UNITS: [u32; 6] = [
1212 0xD800, 0xDC00, 0xDFFF, 0x0011_0000, 0xFFFF_FFFE,
1217 u32::MAX,
1218 ];
1219
1220 fn input_with_units(units: &[u32]) -> TextInput {
1227 let mut input = TextInput::create();
1228 input.text_input_state.inner.text = units.to_vec().into();
1229 input
1230 }
1231
1232 fn rendered(input: TextInput) -> (StyledDom, RefAny) {
1237 let dom = input.dom();
1238 let state = dom.root.callbacks.as_ref()[0].refany.clone();
1239 (StyledDom::create_from_dom(dom), state)
1240 }
1241
1242 fn state_of(state: &RefAny) -> TextInputState {
1244 let mut state = state.clone();
1245 let wrapper = state
1246 .downcast_ref::<TextInputStateWrapper>()
1247 .expect("the widget state must still be a TextInputStateWrapper");
1248 wrapper.inner.clone()
1249 }
1250
1251 fn poke(state: &RefAny, f: impl FnOnce(&mut TextInputStateWrapper)) {
1254 let mut state = state.clone();
1255 let mut wrapper = state
1256 .downcast_mut::<TextInputStateWrapper>()
1257 .expect("the widget state must still be a TextInputStateWrapper");
1258 f(&mut wrapper);
1259 }
1260
1261 struct Recorder {
1270 seen: Vec<TextInputState>,
1271 update: Update,
1272 valid: TextInputValid,
1273 }
1274
1275 fn recorder(update: Update, valid: TextInputValid) -> RefAny {
1276 RefAny::new(Recorder {
1277 seen: Vec::new(),
1278 update,
1279 valid,
1280 })
1281 }
1282
1283 fn recorded(probe: &RefAny) -> Vec<TextInputState> {
1284 let mut probe = probe.clone();
1285 let log = probe
1286 .downcast_ref::<Recorder>()
1287 .expect("the user payload must still be a Recorder");
1288 log.seen.clone()
1289 }
1290
1291 extern "C" fn record_text_input(
1292 mut data: RefAny,
1293 _: CallbackInfo,
1294 state: TextInputState,
1295 ) -> OnTextInputReturn {
1296 let Some(mut log) = data.downcast_mut::<Recorder>() else {
1297 return OnTextInputReturn {
1298 update: Update::DoNothing,
1299 valid: TextInputValid::Yes,
1300 };
1301 };
1302 log.seen.push(state);
1303 OnTextInputReturn {
1304 update: log.update,
1305 valid: log.valid,
1306 }
1307 }
1308
1309 extern "C" fn record_virtual_key(
1313 mut data: RefAny,
1314 _: CallbackInfo,
1315 state: TextInputState,
1316 ) -> OnTextInputReturn {
1317 match data.downcast_mut::<Recorder>() {
1318 Some(mut log) => {
1319 let answer = OnTextInputReturn {
1320 update: log.update,
1321 valid: log.valid,
1322 };
1323 log.seen.push(state);
1324 answer
1325 }
1326 None => OnTextInputReturn {
1327 update: Update::RefreshDom,
1328 valid: TextInputValid::No,
1329 },
1330 }
1331 }
1332
1333 extern "C" fn record_focus_lost(
1334 mut data: RefAny,
1335 _: CallbackInfo,
1336 state: TextInputState,
1337 ) -> Update {
1338 data.downcast_mut::<Recorder>().map_or(Update::RefreshDom, |mut log| {
1339 log.seen.push(state);
1340 log.update
1341 })
1342 }
1343
1344 extern "C" fn generic_shaped(_: RefAny, _: CallbackInfo) -> Update {
1348 Update::DoNothing
1349 }
1350
1351 const CONTAINER: usize = 0;
1357
1358 fn dom_node(idx: usize) -> DomNodeId {
1359 DomNodeId {
1360 dom: DomId::ROOT_ID,
1361 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
1362 }
1363 }
1364
1365 fn node_none() -> DomNodeId {
1369 DomNodeId {
1370 dom: DomId::ROOT_ID,
1371 node: NodeHierarchyItemId::NONE,
1372 }
1373 }
1374
1375 fn inner_id(node: DomNodeId) -> NodeId {
1376 node.node
1377 .into_crate_internal()
1378 .expect("expected a concrete node id")
1379 }
1380
1381 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
1385 DomLayoutResult {
1386 styled_dom,
1387 layout_tree: LayoutTree {
1388 nodes: Vec::new(),
1389 warm: Vec::new(),
1390 cold: Vec::new(),
1391 root: 0,
1392 dom_to_layout: BTreeMap::new(),
1393 children_arena: Vec::new(),
1394 children_offsets: Vec::new(),
1395 subtree_needs_intrinsic: Vec::new(),
1396 },
1397 calculated_positions: Vec::new(),
1398 viewport: LogicalRect::zero(),
1399 display_list: DisplayList::default(),
1400 scroll_ids: HashMap::new(),
1401 scroll_id_to_node_id: HashMap::new(),
1402 }
1403 }
1404
1405 struct Env {
1407 styled_dom: StyledDom,
1408 hit: DomNodeId,
1409 keycode: Option<VirtualKeyCode>,
1410 changeset: Option<PendingTextEdit>,
1411 }
1412
1413 impl Env {
1414 fn new(styled_dom: StyledDom) -> Self {
1415 Self {
1416 styled_dom,
1417 hit: dom_node(CONTAINER),
1418 keycode: None,
1419 changeset: None,
1420 }
1421 }
1422
1423 fn hit(mut self, hit: DomNodeId) -> Self {
1424 self.hit = hit;
1425 self
1426 }
1427
1428 fn key(mut self, keycode: VirtualKeyCode) -> Self {
1429 self.keycode = Some(keycode);
1430 self
1431 }
1432
1433 fn insert(mut self, text: &str) -> Self {
1434 self.changeset = Some(PendingTextEdit {
1435 node: dom_node(CONTAINER),
1436 inserted_text: text.into(),
1437 old_text: AzString::from(""),
1438 });
1439 self
1440 }
1441 }
1442
1443 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
1446 struct Nodes {
1447 placeholder: Option<DomNodeId>,
1448 label: Option<DomNodeId>,
1449 cursor: Option<DomNodeId>,
1450 }
1451
1452 fn run<R>(env: Env, f: impl FnOnce(CallbackInfo) -> R) -> (R, Vec<CallbackChange>, Nodes) {
1456 let mut layout_window =
1457 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
1458 layout_window
1459 .layout_results
1460 .insert(DomId::ROOT_ID, layout_result(env.styled_dom));
1461 layout_window.text_input_manager.pending_changeset = env.changeset;
1462 let layout_window = layout_window;
1463
1464 let renderer_resources = RendererResources::default();
1465 let previous_window_state: Option<FullWindowState> = None;
1466 let mut current_window_state = FullWindowState::default();
1467 current_window_state.keyboard_state.current_virtual_keycode = env.keycode.into();
1468 let gl_context = OptionGlContextPtr::None;
1469 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
1470 BTreeMap::new();
1471 let window_handle = RawWindowHandle::Unsupported;
1472 let system_callbacks = ExternalSystemCallbacks::rust_internal();
1473
1474 let ref_data = CallbackInfoRefData {
1475 layout_window: &layout_window,
1476 renderer_resources: &renderer_resources,
1477 previous_window_state: &previous_window_state,
1478 current_window_state: ¤t_window_state,
1479 gl_context: &gl_context,
1480 current_scroll_manager: &scroll_states,
1481 current_window_handle: &window_handle,
1482 system_callbacks: &system_callbacks,
1483 system_style: Arc::new(azul_css::system::SystemStyle::default()),
1484 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
1485 #[cfg(feature = "icu")]
1486 icu_localizer: IcuLocalizerHandle::default(),
1487 ctx: OptionRefAny::None,
1488 };
1489
1490 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
1491
1492 let probe = CallbackInfo::new(
1493 &ref_data,
1494 &changes,
1495 dom_node(CONTAINER),
1496 OptionLogicalPosition::None,
1497 OptionLogicalPosition::None,
1498 );
1499 let placeholder = probe.get_first_child(dom_node(CONTAINER));
1500 let label = placeholder.and_then(|p| probe.get_next_sibling(p));
1501 let cursor = label.and_then(|l| probe.get_first_child(l));
1502 let nodes = Nodes {
1503 placeholder,
1504 label,
1505 cursor,
1506 };
1507
1508 let info = CallbackInfo::new(
1509 &ref_data,
1510 &changes,
1511 env.hit,
1512 OptionLogicalPosition::None,
1513 OptionLogicalPosition::None,
1514 );
1515
1516 let r = f(info);
1517 let pushed = info.take_changes();
1518 (r, pushed, nodes)
1519 }
1520
1521 fn pushed_opacities(changes: &[CallbackChange]) -> Vec<(NodeId, f32)> {
1523 changes
1524 .iter()
1525 .filter_map(|c| match c {
1526 CallbackChange::ChangeNodeCssProperties {
1527 node_id, properties, ..
1528 } => {
1529 let o = properties.as_ref().iter().find_map(|p| match p {
1530 CssProperty::Opacity(o) => o.get_property().map(|o| o.inner.normalized()),
1531 _ => None,
1532 })?;
1533 Some((*node_id, o))
1534 }
1535 _ => None,
1536 })
1537 .collect()
1538 }
1539
1540 fn pushed_texts(changes: &[CallbackChange]) -> Vec<(DomNodeId, String)> {
1542 changes
1543 .iter()
1544 .filter_map(|c| match c {
1545 CallbackChange::ChangeNodeText { node_id, text } => {
1546 Some((*node_id, text.as_str().to_string()))
1547 }
1548 _ => None,
1549 })
1550 .collect()
1551 }
1552
1553 const PLACEHOLDER_CHILD: usize = 0;
1559 const LABEL_CHILD: usize = 1;
1560
1561 fn classes(node: &Dom) -> Vec<String> {
1562 node.root
1563 .get_ids_and_classes()
1564 .as_ref()
1565 .iter()
1566 .filter_map(|c| match c {
1567 IdOrClass::Class(s) => Some(s.as_str().to_string()),
1568 IdOrClass::Id(_) => None,
1569 })
1570 .collect()
1571 }
1572
1573 fn text_of(node: &Dom) -> String {
1574 node.root
1575 .get_node_type()
1576 .format()
1577 .expect("expected a text node")
1578 }
1579
1580 fn dataset_state(dom: &Dom) -> TextInputState {
1581 let mut dataset = dom
1582 .root
1583 .get_dataset()
1584 .cloned()
1585 .expect("TextInput::dom must attach its state as the container's dataset");
1586 let wrapper = dataset
1587 .downcast_ref::<TextInputStateWrapper>()
1588 .expect("the dataset must be a TextInputStateWrapper");
1589 wrapper.inner.clone()
1590 }
1591
1592 fn style(n: usize) -> CssPropertyWithConditionsVec {
1595 let all: Vec<CssPropertyWithConditions> =
1596 TextInput::default().container_style.as_ref().to_vec();
1597 assert!(n <= all.len(), "not enough default properties to slice");
1598 CssPropertyWithConditionsVec::from_vec(all.into_iter().take(n).collect())
1599 }
1600
1601 #[test]
1606 fn get_text_on_a_default_state_is_the_empty_string() {
1607 let state = TextInputState::default();
1608 assert_eq!(state.get_text(), "");
1609 assert!(state.text.is_empty());
1610 assert_eq!(state.cursor_pos, 0);
1611 assert!(state.placeholder.is_none());
1612 assert!(state.selection.is_none());
1613 }
1614
1615 #[test]
1616 fn get_text_round_trips_every_hostile_string() {
1617 for s in HOSTILE {
1621 let input = TextInput::create().with_text(s.into());
1622 assert_eq!(
1623 input.text_input_state.inner.get_text(),
1624 s,
1625 "the buffer did not round-trip {s:?}",
1626 );
1627 }
1628 }
1629
1630 #[test]
1631 fn get_text_silently_drops_code_units_that_are_not_unicode_scalars() {
1632 for unit in NON_SCALAR_UNITS {
1636 let state = TextInputState {
1637 text: vec![u32::from('A'), unit, u32::from('B')].into(),
1638 ..TextInputState::default()
1639 };
1640 assert_eq!(
1641 state.get_text(),
1642 "AB",
1643 "code unit {unit:#x} was not dropped from the rendered text",
1644 );
1645 assert_eq!(
1646 state.text.len(),
1647 3,
1648 "get_text must not mutate the buffer it reads",
1649 );
1650 }
1651 }
1652
1653 #[test]
1654 fn get_text_on_a_buffer_of_nothing_but_junk_is_empty_and_does_not_panic() {
1655 let state = TextInputState {
1656 text: NON_SCALAR_UNITS.to_vec().into(),
1657 ..TextInputState::default()
1658 };
1659 assert_eq!(state.get_text(), "");
1660 assert_eq!(state.text.len(), NON_SCALAR_UNITS.len());
1661 }
1662
1663 #[test]
1664 fn get_text_never_yields_more_chars_than_the_buffer_holds() {
1665 let mut units: Vec<u32> = Vec::new();
1669 for (i, unit) in NON_SCALAR_UNITS.iter().enumerate() {
1670 units.push(u32::from('x'));
1671 units.push(*unit);
1672 units.push(0x1F600 + i as u32);
1673 }
1674 let state = TextInputState {
1675 text: units.clone().into(),
1676 ..TextInputState::default()
1677 };
1678 let rendered = state.get_text();
1679 assert!(
1680 rendered.chars().count() <= state.text.len(),
1681 "get_text produced {} chars from a {}-unit buffer",
1682 rendered.chars().count(),
1683 state.text.len(),
1684 );
1685 assert_eq!(rendered.chars().count(), units.len() - NON_SCALAR_UNITS.len());
1686 }
1687
1688 #[test]
1689 fn get_text_is_pure() {
1690 let state = TextInputState {
1691 text: vec![u32::from('a'), 0xD800, u32::from('b')].into(),
1692 ..TextInputState::default()
1693 };
1694 let before = state.clone();
1695 assert_eq!(state.get_text(), state.get_text());
1696 assert_eq!(state, before, "get_text mutated the state it was given");
1697 }
1698
1699 #[test]
1700 fn get_text_on_a_very_large_buffer_does_not_panic() {
1701 let n = 50_000;
1702 let state = TextInputState {
1703 text: core::iter::repeat_n(u32::from('ß'), n).collect::<Vec<_>>().into(),
1704 ..TextInputState::default()
1705 };
1706 let text = state.get_text();
1707 assert_eq!(text.chars().count(), n);
1708 assert_eq!(text.len(), n * 2);
1710 }
1711
1712 #[test]
1717 fn with_text_is_exactly_set_text() {
1718 for s in HOSTILE {
1719 let mut a = TextInput::create();
1720 a.set_text(s.into());
1721 let b = TextInput::create().with_text(s.into());
1722 assert_eq!(a, b, "with_text and set_text disagree on {s:?}");
1723 }
1724 }
1725
1726 #[test]
1727 fn set_text_stores_one_code_unit_per_scalar_not_per_byte() {
1728 for s in HOSTILE {
1732 let input = TextInput::create().with_text(s.into());
1733 assert_eq!(
1734 input.text_input_state.inner.text.len(),
1735 s.chars().count(),
1736 "the buffer length for {s:?} is not the scalar count",
1737 );
1738 }
1739
1740 let family = "👨👩👧👦";
1741 let input = TextInput::create().with_text(family.into());
1742 assert_eq!(input.text_input_state.inner.text.len(), 7);
1743 assert_eq!(family.len(), 25, "the ZWJ family is 25 bytes, not 7");
1744 }
1745
1746 #[test]
1747 fn set_text_stores_the_scalar_values_verbatim() {
1748 let input = TextInput::create().with_text("aé\u{10ffff}".into());
1749 assert_eq!(
1750 input.text_input_state.inner.text.as_slice(),
1751 &[0x61, 0xE9, 0x0010_FFFF],
1752 );
1753 }
1754
1755 #[test]
1756 fn set_text_replaces_rather_than_appends() {
1757 let mut input = TextInput::create();
1758 input.set_text("first".into());
1759 input.set_text("second".into());
1760 assert_eq!(input.text_input_state.inner.get_text(), "second");
1761 assert_eq!(input.text_input_state.inner.text.len(), 6);
1762 }
1763
1764 #[test]
1765 fn set_text_with_an_empty_string_clears_the_buffer() {
1766 let mut input = TextInput::create().with_text("something".into());
1767 input.set_text("".into());
1768 assert!(input.text_input_state.inner.text.is_empty());
1769 assert_eq!(input.text_input_state.inner.get_text(), "");
1770 assert_eq!(input, TextInput::create(), "clearing did not restore a fresh widget");
1771 }
1772
1773 #[test]
1774 fn set_text_does_not_enforce_max_len() {
1775 let long: String = "x".repeat(200);
1780 let input = TextInput::create().with_text(long.clone().into());
1781 assert_eq!(input.text_input_state.inner.max_len, 50);
1782 assert_eq!(input.text_input_state.inner.text.len(), 200);
1783 assert_eq!(input.text_input_state.inner.get_text(), long);
1784 }
1785
1786 #[test]
1787 fn set_text_leaves_the_cursor_where_it_was() {
1788 let input = TextInput::create().with_text("hello".into());
1792 assert_eq!(input.text_input_state.inner.cursor_pos, 0);
1793 assert_eq!(input.text_input_state.inner.text.len(), 5);
1794 }
1795
1796 #[test]
1797 fn set_text_touches_nothing_but_the_buffer() {
1798 let mut input = TextInput::create()
1799 .with_placeholder("type here".into())
1800 .with_placeholder_style(style(3));
1801 let before = input.clone();
1802 input.set_text("abc".into());
1803
1804 assert_eq!(
1805 input.text_input_state.inner.placeholder.as_ref().map(|s| s.as_str().to_string()),
1806 Some("type here".to_string()),
1807 );
1808 assert_eq!(input.placeholder_style, before.placeholder_style);
1809 assert_eq!(input.container_style, before.container_style);
1810 assert_eq!(input.label_style, before.label_style);
1811 assert_eq!(input.text_input_state.inner.max_len, before.text_input_state.inner.max_len);
1812 assert!(input.text_input_state.inner.selection.is_none());
1813 }
1814
1815 #[test]
1816 fn with_text_on_a_very_large_string_does_not_panic() {
1817 let n = 50_000;
1818 let long: String = "a".repeat(n);
1819 let input = TextInput::create().with_text(long.into());
1820 assert_eq!(input.text_input_state.inner.text.len(), n);
1821 }
1822
1823 #[test]
1824 fn set_text_is_idempotent() {
1825 for s in HOSTILE {
1826 let mut input = TextInput::create();
1827 input.set_text(s.into());
1828 let once = input.clone();
1829 input.set_text(s.into());
1830 assert_eq!(input, once, "re-assigning {s:?} changed the widget");
1831 }
1832 }
1833
1834 #[test]
1839 fn placeholder_is_absent_on_a_fresh_widget() {
1840 assert!(TextInput::create().text_input_state.inner.placeholder.is_none());
1841 }
1842
1843 #[test]
1844 fn with_placeholder_is_exactly_set_placeholder_and_stores_the_string_verbatim() {
1845 for s in HOSTILE {
1846 let mut a = TextInput::create();
1847 a.set_placeholder(s.into());
1848 let b = TextInput::create().with_placeholder(s.into());
1849 assert_eq!(a, b, "with_placeholder and set_placeholder disagree on {s:?}");
1850
1851 assert_eq!(
1852 a.text_input_state.inner.placeholder.as_ref().map(|p| p.as_str()),
1853 Some(s),
1854 "the placeholder {s:?} was not stored byte-for-byte",
1855 );
1856 }
1857 }
1858
1859 #[test]
1860 fn set_placeholder_overwrites_a_previous_placeholder_and_never_clears_it() {
1861 let mut input = TextInput::create();
1862 input.set_placeholder("first".into());
1863 input.set_placeholder("".into());
1864 assert_eq!(
1866 input.text_input_state.inner.placeholder.as_ref().map(|p| p.as_str()),
1867 Some(""),
1868 );
1869 }
1870
1871 #[test]
1872 fn set_placeholder_does_not_touch_the_text_buffer() {
1873 let mut input = TextInput::create().with_text("abc".into());
1874 input.set_placeholder("hint".into());
1875 assert_eq!(input.text_input_state.inner.get_text(), "abc");
1876 }
1877
1878 #[test]
1883 fn each_style_setter_writes_exactly_one_slot() {
1884 let marker = style(1);
1885
1886 let mut a = TextInput::create();
1887 a.set_placeholder_style(marker.clone());
1888 assert_eq!(a.placeholder_style, marker);
1889 assert_eq!(a.container_style, TextInput::create().container_style);
1890 assert_eq!(a.label_style, TextInput::create().label_style);
1891
1892 let mut b = TextInput::create();
1893 b.set_container_style(marker.clone());
1894 assert_eq!(b.container_style, marker);
1895 assert_eq!(b.placeholder_style, TextInput::create().placeholder_style);
1896 assert_eq!(b.label_style, TextInput::create().label_style);
1897
1898 let mut c = TextInput::create();
1899 c.set_label_style(marker.clone());
1900 assert_eq!(c.label_style, marker);
1901 assert_eq!(c.placeholder_style, TextInput::create().placeholder_style);
1902 assert_eq!(c.container_style, TextInput::create().container_style);
1903 }
1904
1905 #[test]
1906 fn the_with_style_builders_are_exactly_their_setters() {
1907 let s = style(2);
1908
1909 let mut a = TextInput::create();
1910 a.set_placeholder_style(s.clone());
1911 assert_eq!(a, TextInput::create().with_placeholder_style(s.clone()));
1912
1913 let mut b = TextInput::create();
1914 b.set_container_style(s.clone());
1915 assert_eq!(b, TextInput::create().with_container_style(s.clone()));
1916
1917 let mut c = TextInput::create();
1918 c.set_label_style(s.clone());
1919 assert_eq!(c, TextInput::create().with_label_style(s));
1920 }
1921
1922 #[test]
1923 fn style_setters_accept_an_empty_vector_and_survive_rendering() {
1924 let empty = CssPropertyWithConditionsVec::from_vec(Vec::new());
1925 let input = TextInput::create()
1926 .with_placeholder_style(empty.clone())
1927 .with_container_style(empty.clone())
1928 .with_label_style(empty.clone());
1929 assert!(input.container_style.is_empty());
1930
1931 let dom = input.dom();
1933 assert_eq!(dom.children.as_ref().len(), 2);
1934 }
1935
1936 #[test]
1937 fn style_setters_overwrite_rather_than_merge() {
1938 let mut input = TextInput::create();
1939 input.set_container_style(style(4));
1940 input.set_container_style(style(1));
1941 assert_eq!(input.container_style.len(), 1);
1942 }
1943
1944 #[test]
1949 fn set_on_text_input_stores_the_fn_pointer_and_the_payload_verbatim() {
1950 let mut input = TextInput::create();
1951 input.set_on_text_input(
1952 RefAny::new(0xDEAD_BEEF_u32),
1953 record_text_input as TextInputOnTextInputCallbackType,
1954 );
1955
1956 let slot = input
1957 .text_input_state
1958 .on_text_input
1959 .as_ref()
1960 .expect("set_on_text_input stored nothing");
1961 assert_eq!(
1962 slot.callback.cb as *const () as usize,
1963 record_text_input as TextInputOnTextInputCallbackType as *const () as usize,
1964 "the fn pointer was mangled on the way in",
1965 );
1966
1967 let mut payload = slot.refany.clone();
1968 assert_eq!(
1969 *payload.downcast_ref::<u32>().expect("the payload changed type"),
1970 0xDEAD_BEEF,
1971 );
1972 assert!(
1973 payload.downcast_ref::<u64>().is_none(),
1974 "the payload must not be readable as a differently-typed value",
1975 );
1976 }
1977
1978 #[test]
1979 fn set_on_virtual_key_down_and_set_on_focus_lost_store_their_own_slots() {
1980 let mut input = TextInput::create();
1981 input.set_on_virtual_key_down(
1982 RefAny::new(1_u8),
1983 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
1984 );
1985 input.set_on_focus_lost(
1986 RefAny::new(2_u8),
1987 record_focus_lost as TextInputOnFocusLostCallbackType,
1988 );
1989
1990 assert!(
1991 input.text_input_state.on_text_input.as_ref().is_none(),
1992 "the text-input slot was filled in by an unrelated setter",
1993 );
1994 assert_eq!(
1995 input
1996 .text_input_state
1997 .on_virtual_key_down
1998 .as_ref()
1999 .expect("the virtual-key slot is empty")
2000 .callback
2001 .cb as *const () as usize,
2002 record_virtual_key as TextInputOnVirtualKeyDownCallbackType as *const () as usize,
2003 );
2004 assert_eq!(
2005 input
2006 .text_input_state
2007 .on_focus_lost
2008 .as_ref()
2009 .expect("the focus-lost slot is empty")
2010 .callback
2011 .cb as *const () as usize,
2012 record_focus_lost as TextInputOnFocusLostCallbackType as *const () as usize,
2013 );
2014 }
2015
2016 #[test]
2017 fn the_with_callback_builders_are_exactly_their_setters() {
2018 let payload = RefAny::new(7_u16);
2019
2020 let mut a = TextInput::create();
2021 a.set_on_text_input(payload.clone(), record_text_input as TextInputOnTextInputCallbackType);
2022 assert_eq!(
2023 a,
2024 TextInput::create().with_on_text_input(
2025 payload.clone(),
2026 record_text_input as TextInputOnTextInputCallbackType,
2027 ),
2028 );
2029
2030 let mut b = TextInput::create();
2031 b.set_on_virtual_key_down(
2032 payload.clone(),
2033 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2034 );
2035 assert_eq!(
2036 b,
2037 TextInput::create().with_on_virtual_key_down(
2038 payload.clone(),
2039 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2040 ),
2041 );
2042
2043 let mut c = TextInput::create();
2044 c.set_on_focus_lost(payload.clone(), record_focus_lost as TextInputOnFocusLostCallbackType);
2045 assert_eq!(
2046 c,
2047 TextInput::create()
2048 .with_on_focus_lost(payload, record_focus_lost as TextInputOnFocusLostCallbackType),
2049 );
2050 }
2051
2052 #[test]
2053 fn setting_a_callback_twice_replaces_it_rather_than_stacking() {
2054 let mut input = TextInput::create();
2055 input.set_on_text_input(
2056 RefAny::new(1_u8),
2057 record_text_input as TextInputOnTextInputCallbackType,
2058 );
2059 input.set_on_text_input(
2060 RefAny::new(2_u8),
2061 record_virtual_key as TextInputOnTextInputCallbackType,
2062 );
2063
2064 let slot = input.text_input_state.on_text_input.as_ref().expect("slot is empty");
2065 assert_eq!(
2066 slot.callback.cb as *const () as usize,
2067 record_virtual_key as TextInputOnTextInputCallbackType as *const () as usize,
2068 "the second assignment did not win",
2069 );
2070 let mut payload = slot.refany.clone();
2071 assert_eq!(*payload.downcast_ref::<u8>().expect("wrong payload type"), 2);
2072 }
2073
2074 #[test]
2075 fn a_generic_two_argument_callback_is_accepted_through_the_ffi_conversion() {
2076 let generic = Callback {
2080 cb: generic_shaped,
2081 ctx: OptionRefAny::None,
2082 };
2083 let input = TextInput::create().with_on_text_input(RefAny::new(0_u8), generic);
2084 assert_eq!(
2085 input
2086 .text_input_state
2087 .on_text_input
2088 .as_ref()
2089 .expect("the transmuted callback was dropped")
2090 .callback
2091 .cb as *const () as usize,
2092 generic_shaped as *const () as usize,
2093 );
2094 }
2095
2096 #[test]
2101 fn create_is_default_and_is_pure() {
2102 assert_eq!(TextInput::create(), TextInput::default());
2103 assert_eq!(TextInput::create(), TextInput::create());
2104 }
2105
2106 #[test]
2107 fn create_starts_empty_with_no_hooks_and_no_running_animation() {
2108 let input = TextInput::create();
2109 assert!(input.text_input_state.inner.text.is_empty());
2110 assert!(input.text_input_state.inner.placeholder.is_none());
2111 assert!(input.text_input_state.inner.selection.is_none());
2112 assert_eq!(input.text_input_state.inner.cursor_pos, 0);
2113 assert_eq!(input.text_input_state.inner.max_len, 50);
2114 assert!(input.text_input_state.on_text_input.as_ref().is_none());
2115 assert!(input.text_input_state.on_virtual_key_down.as_ref().is_none());
2116 assert!(input.text_input_state.on_focus_lost.as_ref().is_none());
2117 assert!(input.text_input_state.cursor_animation.is_none());
2118 assert!(input.text_input_state.update_text_input_before_calling_focus_lost_fn);
2119 assert!(input.text_input_state.update_text_input_before_calling_vk_down_fn);
2120 assert!(!input.container_style.is_empty());
2121 }
2122
2123 #[test]
2124 fn swap_with_default_returns_the_old_widget_and_leaves_a_fresh_one_behind() {
2125 let mut input = TextInput::create()
2126 .with_text("typed".into())
2127 .with_placeholder("hint".into());
2128 let old = input.swap_with_default();
2129
2130 assert_eq!(old.text_input_state.inner.get_text(), "typed");
2131 assert_eq!(input, TextInput::create(), "what was left behind is not a fresh widget");
2132 }
2133
2134 #[test]
2135 fn swapping_twice_round_trips_the_original_widget() {
2136 let mut a = TextInput::create().with_text("abc".into());
2137 let mut b = a.swap_with_default(); let c = b.swap_with_default(); assert_eq!(c, TextInput::create().with_text("abc".into()));
2141 assert_eq!(a, TextInput::create());
2142 assert_eq!(b, TextInput::create());
2143 }
2144
2145 #[test]
2146 fn swap_with_default_moves_the_hooks_out_rather_than_copying_them() {
2147 let probe = recorder(Update::DoNothing, TextInputValid::Yes);
2148 let mut input = TextInput::create().with_on_text_input(
2149 probe.clone(),
2150 record_text_input as TextInputOnTextInputCallbackType,
2151 );
2152
2153 let old = input.swap_with_default();
2154
2155 assert!(
2156 old.text_input_state.on_text_input.as_ref().is_some(),
2157 "the hook vanished during the swap",
2158 );
2159 assert!(
2162 input.text_input_state.on_text_input.as_ref().is_none(),
2163 "the hook was copied instead of moved",
2164 );
2165 assert!(recorded(&probe).is_empty(), "the hook fired during a swap");
2168 }
2169
2170 #[test]
2175 fn dom_builds_a_container_with_a_placeholder_and_a_label_carrying_a_cursor() {
2176 let dom = TextInput::create().dom();
2177
2178 assert_eq!(classes(&dom), vec!["__azul-native-text-input-container"]);
2179 assert_eq!(dom.children.as_ref().len(), 2);
2180
2181 let placeholder = &dom.children.as_ref()[PLACEHOLDER_CHILD];
2182 let label = &dom.children.as_ref()[LABEL_CHILD];
2183 assert_eq!(classes(placeholder), vec!["__azul-native-text-input-placeholder"]);
2184 assert_eq!(classes(label), vec!["__azul-native-text-input-label"]);
2185
2186 assert!(
2187 placeholder.children.as_ref().is_empty(),
2188 "the placeholder must be a leaf: the handlers reach the label through its sibling",
2189 );
2190 assert_eq!(label.children.as_ref().len(), 1);
2191 assert_eq!(
2192 classes(&label.children.as_ref()[0]),
2193 vec!["__azul-native-text-input-cursor"],
2194 );
2195 }
2196
2197 #[test]
2198 fn dom_marks_the_container_as_keyboard_focusable() {
2199 assert_eq!(TextInput::create().dom().root.get_tab_index(), Some(TabIndex::Auto));
2202 }
2203
2204 #[test]
2205 fn dom_registers_exactly_the_five_default_handlers_over_one_shared_state() {
2206 let dom = TextInput::create().dom();
2207 let callbacks = dom.root.callbacks.as_ref();
2208
2209 let events: Vec<EventFilter> = callbacks.iter().map(|c| c.event).collect();
2210 assert_eq!(
2211 events,
2212 vec![
2213 EventFilter::Focus(FocusEventFilter::FocusReceived),
2214 EventFilter::Focus(FocusEventFilter::FocusLost),
2215 EventFilter::Focus(FocusEventFilter::TextInput),
2216 EventFilter::Focus(FocusEventFilter::VirtualKeyDown),
2217 EventFilter::Hover(HoverEventFilter::MouseOver),
2218 ],
2219 );
2220
2221 let targets: Vec<usize> = callbacks.iter().map(|c| c.callback.cb).collect();
2222 assert_eq!(
2223 targets,
2224 vec![
2225 default_on_focus_received as usize,
2226 default_on_focus_lost as usize,
2227 default_on_text_input as usize,
2228 default_on_virtual_key_down as usize,
2229 default_on_mouse_hover as usize,
2230 ],
2231 "the handlers are wired to the wrong events",
2232 );
2233
2234 for c in callbacks {
2237 assert_eq!(c.refany, callbacks[0].refany, "a handler got its own state copy");
2238 }
2239 assert_eq!(
2240 dom.root.get_dataset().expect("no dataset attached"),
2241 &callbacks[0].refany,
2242 );
2243 }
2244
2245 #[test]
2246 fn dom_renders_the_buffer_into_the_label_and_the_placeholder_into_its_own_node() {
2247 let dom = TextInput::create()
2248 .with_text("typed".into())
2249 .with_placeholder("hint".into())
2250 .dom();
2251
2252 assert_eq!(text_of(&dom.children.as_ref()[PLACEHOLDER_CHILD]), "hint");
2253 assert_eq!(text_of(&dom.children.as_ref()[LABEL_CHILD]), "typed");
2254 }
2255
2256 #[test]
2257 fn dom_without_a_placeholder_still_renders_an_empty_placeholder_node() {
2258 let dom = TextInput::create().with_text("typed".into()).dom();
2261 assert_eq!(dom.children.as_ref().len(), 2);
2262 assert_eq!(text_of(&dom.children.as_ref()[PLACEHOLDER_CHILD]), "");
2263 }
2264
2265 #[test]
2266 fn dom_passes_hostile_text_and_placeholder_through_unchanged() {
2267 for s in HOSTILE {
2268 let dom = TextInput::create()
2269 .with_text(s.into())
2270 .with_placeholder(s.into())
2271 .dom();
2272 assert_eq!(text_of(&dom.children.as_ref()[LABEL_CHILD]), s, "label mangled {s:?}");
2273 assert_eq!(
2274 text_of(&dom.children.as_ref()[PLACEHOLDER_CHILD]),
2275 s,
2276 "placeholder mangled {s:?}",
2277 );
2278 }
2279 }
2280
2281 #[test]
2282 fn dom_syncs_the_cursor_to_the_end_of_the_buffer() {
2283 for s in HOSTILE {
2284 let dom = TextInput::create().with_text(s.into()).dom();
2285 assert_eq!(
2286 dataset_state(&dom).cursor_pos,
2287 s.chars().count(),
2288 "the cursor was not parked at the end of {s:?}",
2289 );
2290 }
2291 }
2292
2293 #[test]
2294 fn dom_measures_the_cursor_in_code_units_which_can_outrun_the_rendered_text() {
2295 let dom = input_with_units(&[u32::from('a'), 0xD800, u32::from('b')]).dom();
2299 assert_eq!(dataset_state(&dom).cursor_pos, 3);
2300 assert_eq!(text_of(&dom.children.as_ref()[LABEL_CHILD]), "ab");
2301 }
2302
2303 #[test]
2304 fn dom_on_a_very_large_buffer_does_not_panic() {
2305 let n = 50_000;
2306 let long: String = "x".repeat(n);
2307 let dom = TextInput::create().with_text(long.into()).dom();
2308 assert_eq!(text_of(&dom.children.as_ref()[LABEL_CHILD]).len(), n);
2309 assert_eq!(dataset_state(&dom).cursor_pos, n);
2310 }
2311
2312 #[test]
2313 fn dom_keeps_the_configured_styles_on_the_nodes_they_were_set_for() {
2314 let placeholder_style = style(1);
2315 let label_style = style(2);
2316 let container_style = style(3);
2317 let dom = TextInput::create()
2318 .with_placeholder_style(placeholder_style.clone())
2319 .with_label_style(label_style.clone())
2320 .with_container_style(container_style.clone())
2321 .dom();
2322
2323 let inline = |node: &Dom| -> Vec<CssProperty> {
2324 node.root.style.iter_inline_properties().map(|(p, _)| p.clone()).collect()
2325 };
2326 let declared = |v: &CssPropertyWithConditionsVec| -> Vec<CssProperty> {
2327 v.as_ref().iter().map(|p| p.property.clone()).collect()
2328 };
2329
2330 assert_eq!(inline(&dom), declared(&container_style));
2331 assert_eq!(
2332 inline(&dom.children.as_ref()[PLACEHOLDER_CHILD]),
2333 declared(&placeholder_style),
2334 );
2335 assert_eq!(inline(&dom.children.as_ref()[LABEL_CHILD]), declared(&label_style));
2336 }
2337
2338 #[test]
2339 fn the_rendered_tree_flattens_to_three_distinct_reachable_children() {
2340 let (styled_dom, _) = rendered(TextInput::create());
2341 let ((), _, nodes) = run(Env::new(styled_dom), |_| ());
2342
2343 let placeholder = nodes.placeholder.expect("the container has no first child");
2344 let label = nodes.label.expect("the placeholder has no next sibling");
2345 let cursor = nodes.cursor.expect("the label has no first child");
2346
2347 assert_ne!(placeholder, label);
2348 assert_ne!(label, cursor);
2349 assert_ne!(placeholder, cursor);
2350 assert_ne!(placeholder, dom_node(CONTAINER));
2351 }
2352
2353 #[test]
2358 fn focus_received_with_a_foreign_payload_is_an_inert_no_op() {
2359 let (styled_dom, _) = rendered(TextInput::create());
2360 let foreign = RefAny::new(0xDEAD_BEEF_u32);
2361 let (update, changes, _) = run(Env::new(styled_dom), |info| {
2362 default_on_focus_received(foreign.clone(), info)
2363 });
2364 assert_eq!(update, Update::DoNothing);
2365 assert!(changes.is_empty(), "a foreign payload still produced {changes:?}");
2366 }
2367
2368 #[test]
2369 fn focus_received_on_a_node_with_no_children_bails_out_before_touching_css() {
2370 let (styled_dom, state) = rendered(TextInput::create());
2374 let (update, changes, nodes) = run(Env::new(styled_dom).hit(node_none()), |info| {
2375 default_on_focus_received(state.clone(), info)
2376 });
2377 assert_eq!(update, Update::DoNothing);
2378 assert!(changes.is_empty());
2379 assert!(nodes.placeholder.is_some(), "the fixture itself is malformed");
2380 }
2381
2382 #[test]
2383 fn focus_received_on_the_cursor_leaf_is_a_no_op() {
2384 let (probe_dom, _) = rendered(TextInput::create());
2386 let (_, _, nodes) = run(Env::new(probe_dom), |_| ());
2387 let cursor = nodes.cursor.expect("no cursor node");
2388
2389 let (styled_dom, state) = rendered(TextInput::create());
2390 let (update, changes, _) = run(Env::new(styled_dom).hit(cursor), |info| {
2391 default_on_focus_received(state.clone(), info)
2392 });
2393 assert_eq!(update, Update::DoNothing);
2394 assert!(changes.is_empty(), "a childless hit node still pushed {changes:?}");
2395 }
2396
2397 #[test]
2398 fn focus_received_hides_the_placeholder_only_while_the_buffer_is_empty() {
2399 let (styled_dom, state) = rendered(TextInput::create());
2400 let (update, changes, nodes) = run(Env::new(styled_dom), |info| {
2401 default_on_focus_received(state.clone(), info)
2402 });
2403 assert_eq!(update, Update::DoNothing);
2404 assert_eq!(
2405 pushed_opacities(&changes),
2406 vec![(inner_id(nodes.placeholder.expect("no placeholder")), 0.0)],
2407 "focusing an empty input did not hide its placeholder",
2408 );
2409
2410 let (styled_dom, state) = rendered(TextInput::create().with_text("typed".into()));
2411 let (update, changes, _) = run(Env::new(styled_dom), |info| {
2412 default_on_focus_received(state.clone(), info)
2413 });
2414 assert_eq!(update, Update::DoNothing);
2415 assert!(
2416 changes.is_empty(),
2417 "focusing a non-empty input touched the placeholder anyway: {changes:?}",
2418 );
2419 }
2420
2421 #[test]
2422 fn focus_received_reparks_the_cursor_at_the_end_of_the_buffer() {
2423 let (styled_dom, state) = rendered(TextInput::create().with_text("hello".into()));
2424 poke(&state, |w| w.inner.cursor_pos = usize::MAX);
2426
2427 let (_, _, _) = run(Env::new(styled_dom), |info| {
2428 default_on_focus_received(state.clone(), info)
2429 });
2430 assert_eq!(state_of(&state).cursor_pos, 5);
2431 }
2432
2433 #[test]
2438 fn focus_lost_shows_the_placeholder_only_while_the_buffer_is_empty() {
2439 let (styled_dom, state) = rendered(TextInput::create());
2440 let (update, changes, nodes) =
2441 run(Env::new(styled_dom), |info| default_on_focus_lost(state.clone(), info));
2442 assert_eq!(update, Update::DoNothing);
2443 assert_eq!(
2444 pushed_opacities(&changes),
2445 vec![(inner_id(nodes.placeholder.expect("no placeholder")), 1.0)],
2446 "blurring an empty input did not bring its placeholder back",
2447 );
2448
2449 let (styled_dom, state) = rendered(TextInput::create().with_text("typed".into()));
2450 let (_, changes, _) =
2451 run(Env::new(styled_dom), |info| default_on_focus_lost(state.clone(), info));
2452 assert!(
2453 changes.is_empty(),
2454 "blurring a non-empty input revealed the placeholder over the text: {changes:?}",
2455 );
2456 }
2457
2458 #[test]
2459 fn focus_lost_hands_the_hook_the_live_state_and_returns_its_verdict() {
2460 let probe = recorder(Update::RefreshDomAllWindows, TextInputValid::Yes);
2461 let (styled_dom, state) = rendered(
2462 TextInput::create()
2463 .with_text("typed".into())
2464 .with_on_focus_lost(probe.clone(), record_focus_lost as TextInputOnFocusLostCallbackType),
2465 );
2466
2467 let (update, _, _) =
2468 run(Env::new(styled_dom), |info| default_on_focus_lost(state.clone(), info));
2469
2470 assert_eq!(update, Update::RefreshDomAllWindows, "the hook's Update was swallowed");
2471 let seen = recorded(&probe);
2472 assert_eq!(seen.len(), 1, "the hook fired {} times, expected once", seen.len());
2473 assert_eq!(seen[0].get_text(), "typed");
2474 assert_eq!(seen[0].cursor_pos, 5, "the hook saw a cursor that dom() should have synced");
2475 }
2476
2477 #[test]
2478 fn focus_lost_without_a_hook_reports_no_work_to_do() {
2479 let (styled_dom, state) = rendered(TextInput::create().with_text("typed".into()));
2480 let (update, _, _) =
2481 run(Env::new(styled_dom), |info| default_on_focus_lost(state.clone(), info));
2482 assert_eq!(update, Update::DoNothing);
2483 }
2484
2485 #[test]
2486 fn focus_lost_on_a_none_hit_node_skips_the_hook_entirely() {
2487 let probe = recorder(Update::RefreshDom, TextInputValid::Yes);
2491 let (styled_dom, state) = rendered(TextInput::create().with_on_focus_lost(
2492 probe.clone(),
2493 record_focus_lost as TextInputOnFocusLostCallbackType,
2494 ));
2495
2496 let (update, changes, _) = run(Env::new(styled_dom).hit(node_none()), |info| {
2497 default_on_focus_lost(state.clone(), info)
2498 });
2499
2500 assert_eq!(update, Update::DoNothing);
2501 assert!(changes.is_empty());
2502 assert!(recorded(&probe).is_empty(), "the hook fired on a hit node that does not exist");
2503 }
2504
2505 #[test]
2506 fn focus_lost_with_a_foreign_payload_is_an_inert_no_op() {
2507 let (styled_dom, _) = rendered(TextInput::create());
2508 let foreign = RefAny::new("not a text input".to_string());
2509 let (update, changes, _) = run(Env::new(styled_dom), |info| {
2510 default_on_focus_lost(foreign.clone(), info)
2511 });
2512 assert_eq!(update, Update::DoNothing);
2513 assert!(changes.is_empty());
2514 }
2515
2516 #[test]
2521 fn text_input_without_a_pending_changeset_does_nothing() {
2522 let (styled_dom, state) = rendered(TextInput::create());
2523 let (update, changes, _) =
2524 run(Env::new(styled_dom), |info| default_on_text_input(state.clone(), info));
2525 assert_eq!(update, Update::DoNothing);
2526 assert!(changes.is_empty());
2527 assert_eq!(state_of(&state).get_text(), "");
2528 }
2529
2530 #[test]
2531 fn text_input_with_an_empty_insertion_does_nothing() {
2532 let (styled_dom, state) = rendered(TextInput::create().with_text("abc".into()));
2533 let (update, changes, _) = run(Env::new(styled_dom).insert(""), |info| {
2534 default_on_text_input(state.clone(), info)
2535 });
2536 assert_eq!(update, Update::DoNothing);
2537 assert!(changes.is_empty(), "an empty insertion still repainted: {changes:?}");
2538 assert_eq!(state_of(&state).get_text(), "abc");
2539 assert_eq!(state_of(&state).cursor_pos, 3);
2540 }
2541
2542 #[test]
2543 fn text_input_appends_the_insertion_hides_the_placeholder_and_repaints_the_label() {
2544 let (styled_dom, state) = rendered(TextInput::create().with_placeholder("hint".into()));
2545 let (update, changes, nodes) = run(Env::new(styled_dom).insert("hi"), |info| {
2546 default_on_text_input(state.clone(), info)
2547 });
2548
2549 assert_eq!(update, Update::DoNothing, "no hook is installed, so nothing needs redrawing");
2550 assert_eq!(state_of(&state).get_text(), "hi");
2551 assert_eq!(state_of(&state).cursor_pos, 2);
2552
2553 assert_eq!(
2554 pushed_opacities(&changes),
2555 vec![(inner_id(nodes.placeholder.expect("no placeholder")), 0.0)],
2556 );
2557 assert_eq!(
2558 pushed_texts(&changes),
2559 vec![(nodes.label.expect("no label"), "hi".to_string())],
2560 "the label was not repainted with the new buffer",
2561 );
2562 }
2563
2564 #[test]
2565 fn text_input_appends_to_an_existing_buffer_rather_than_replacing_it() {
2566 let (styled_dom, state) = rendered(TextInput::create().with_text("ab".into()));
2567 let (_, changes, nodes) = run(Env::new(styled_dom).insert("cd"), |info| {
2568 default_on_text_input(state.clone(), info)
2569 });
2570 assert_eq!(state_of(&state).get_text(), "abcd");
2571 assert_eq!(
2572 pushed_texts(&changes),
2573 vec![(nodes.label.expect("no label"), "abcd".to_string())],
2574 );
2575 }
2576
2577 #[test]
2578 fn text_input_hands_the_hook_a_preview_that_already_contains_the_insertion() {
2579 let probe = recorder(Update::RefreshDom, TextInputValid::Yes);
2582 let (styled_dom, state) = rendered(
2583 TextInput::create()
2584 .with_text("ab".into())
2585 .with_on_text_input(probe.clone(), record_text_input as TextInputOnTextInputCallbackType),
2586 );
2587
2588 let (update, _, _) = run(Env::new(styled_dom).insert("c"), |info| {
2589 default_on_text_input(state.clone(), info)
2590 });
2591
2592 assert_eq!(update, Update::RefreshDom, "the hook's Update was swallowed");
2593 let seen = recorded(&probe);
2594 assert_eq!(seen.len(), 1);
2595 assert_eq!(seen[0].get_text(), "abc", "the hook was shown the pre-edit buffer");
2596 assert_eq!(seen[0].cursor_pos, 3);
2597 }
2598
2599 #[test]
2600 fn text_input_rejected_by_the_hook_leaves_the_buffer_and_the_screen_untouched() {
2601 let probe = recorder(Update::RefreshDomAllWindows, TextInputValid::No);
2602 let (styled_dom, state) = rendered(
2603 TextInput::create()
2604 .with_text("ab".into())
2605 .with_placeholder("hint".into())
2606 .with_on_text_input(probe.clone(), record_text_input as TextInputOnTextInputCallbackType),
2607 );
2608
2609 let (update, changes, _) = run(Env::new(styled_dom).insert("c"), |info| {
2610 default_on_text_input(state.clone(), info)
2611 });
2612
2613 assert_eq!(update, Update::RefreshDomAllWindows, "a rejected edit still reports its Update");
2614 assert_eq!(state_of(&state).get_text(), "ab", "a rejected edit was applied anyway");
2615 assert_eq!(state_of(&state).cursor_pos, 2, "a rejected edit still moved the cursor");
2616 assert!(
2617 changes.is_empty(),
2618 "a rejected edit still repainted the widget: {changes:?}",
2619 );
2620 }
2621
2622 #[test]
2623 fn text_input_advances_the_cursor_by_utf8_byte_length_not_by_scalar_count() {
2624 let (styled_dom, state) = rendered(TextInput::create());
2629 let (_, _, _) = run(Env::new(styled_dom).insert("é"), |info| {
2630 default_on_text_input(state.clone(), info)
2631 });
2632
2633 let after = state_of(&state);
2634 assert_eq!(after.get_text(), "é");
2635 assert_eq!(after.text.len(), 1, "the buffer holds one scalar");
2636 assert_eq!(after.cursor_pos, 2, "but the cursor moved by the two UTF-8 bytes");
2637 assert!(
2638 after.cursor_pos > after.text.len(),
2639 "the cursor is expected to overshoot here; see the KNOWN GAP above",
2640 );
2641 }
2642
2643 #[test]
2644 fn text_input_saturates_the_cursor_instead_of_overflowing_it() {
2645 let (styled_dom, state) = rendered(TextInput::create());
2646 poke(&state, |w| w.inner.cursor_pos = usize::MAX);
2647
2648 let (update, _, _) = run(Env::new(styled_dom).insert("abc"), |info| {
2649 default_on_text_input(state.clone(), info)
2650 });
2651
2652 assert_eq!(update, Update::DoNothing);
2653 assert_eq!(
2654 state_of(&state).cursor_pos,
2655 usize::MAX,
2656 "the cursor wrapped around instead of saturating",
2657 );
2658 assert_eq!(state_of(&state).get_text(), "abc");
2659 }
2660
2661 #[test]
2662 fn text_input_accepts_astral_combining_and_multi_scalar_insertions() {
2663 for s in ["\u{10ffff}", "e\u{301}", "👨👩👧👦", "日本語", "\0"] {
2664 let (styled_dom, state) = rendered(TextInput::create());
2665 let (_, changes, nodes) = run(Env::new(styled_dom).insert(s), |info| {
2666 default_on_text_input(state.clone(), info)
2667 });
2668 assert_eq!(state_of(&state).get_text(), s, "the buffer mangled {s:?}");
2669 assert_eq!(state_of(&state).text.len(), s.chars().count());
2670 assert_eq!(
2671 pushed_texts(&changes),
2672 vec![(nodes.label.expect("no label"), s.to_string())],
2673 );
2674 }
2675 }
2676
2677 #[test]
2678 fn text_input_on_a_wrong_shaped_subtree_changes_nothing() {
2679 let (probe_dom, _) = rendered(TextInput::create());
2683 let (_, _, nodes) = run(Env::new(probe_dom), |_| ());
2684 let label = nodes.label.expect("no label");
2685
2686 let (styled_dom, state) = rendered(TextInput::create().with_text("ab".into()));
2687 let (result, changes, _) = run(Env::new(styled_dom).hit(label).insert("c"), |info| {
2688 default_on_text_input_inner(state.clone(), info)
2689 });
2690
2691 assert_eq!(result, None, "the handler claimed to have handled a malformed tree");
2692 assert!(changes.is_empty());
2693 assert_eq!(state_of(&state).get_text(), "ab", "the buffer changed anyway");
2694 }
2695
2696 #[test]
2697 fn text_input_on_a_none_hit_node_changes_nothing() {
2698 let (styled_dom, state) = rendered(TextInput::create().with_text("ab".into()));
2699 let (update, changes, _) = run(Env::new(styled_dom).hit(node_none()).insert("c"), |info| {
2700 default_on_text_input(state.clone(), info)
2701 });
2702 assert_eq!(update, Update::DoNothing);
2703 assert!(changes.is_empty());
2704 assert_eq!(state_of(&state).get_text(), "ab");
2705 }
2706
2707 #[test]
2708 fn text_input_with_a_foreign_payload_is_an_inert_no_op() {
2709 let (styled_dom, _) = rendered(TextInput::create());
2710 let foreign = RefAny::new(0_u8);
2711 let (result, changes, _) = run(Env::new(styled_dom).insert("a"), |info| {
2712 default_on_text_input_inner(foreign.clone(), info)
2713 });
2714 assert_eq!(result, None);
2715 assert!(changes.is_empty());
2716 }
2717
2718 #[test]
2719 fn text_input_ignores_max_len() {
2720 let (styled_dom, state) = rendered(TextInput::create());
2723 let filler: String = "x".repeat(80);
2724 let (_, _, _) = run(Env::new(styled_dom).insert(&filler), |info| {
2725 default_on_text_input(state.clone(), info)
2726 });
2727 let after = state_of(&state);
2728 assert_eq!(after.max_len, 50);
2729 assert_eq!(after.text.len(), 80);
2730 }
2731
2732 #[test]
2737 fn virtual_key_down_without_a_pressed_key_does_nothing() {
2738 let probe = recorder(Update::RefreshDom, TextInputValid::Yes);
2739 let (styled_dom, state) = rendered(
2740 TextInput::create().with_text("ab".into()).with_on_virtual_key_down(
2741 probe.clone(),
2742 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2743 ),
2744 );
2745
2746 let (update, changes, _) = run(Env::new(styled_dom), |info| {
2747 default_on_virtual_key_down(state.clone(), info)
2748 });
2749
2750 assert_eq!(update, Update::DoNothing);
2751 assert!(changes.is_empty());
2752 assert!(recorded(&probe).is_empty(), "the hook fired without a key being down");
2753 assert_eq!(state_of(&state).get_text(), "ab");
2754 }
2755
2756 #[test]
2757 fn backspace_pops_one_code_unit_and_walks_the_cursor_back() {
2758 let (styled_dom, state) = rendered(TextInput::create().with_text("abc".into()));
2759 let (update, changes, nodes) =
2760 run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2761 default_on_virtual_key_down(state.clone(), info)
2762 });
2763
2764 assert_eq!(update, Update::DoNothing);
2765 assert_eq!(state_of(&state).get_text(), "ab");
2766 assert_eq!(state_of(&state).cursor_pos, 2);
2767 assert_eq!(
2768 pushed_texts(&changes),
2769 vec![(nodes.label.expect("no label"), "ab".to_string())],
2770 );
2771 assert!(pushed_opacities(&changes).is_empty(), "backspace must not touch the placeholder");
2772 }
2773
2774 #[test]
2775 fn backspace_on_an_empty_buffer_saturates_at_zero_instead_of_underflowing() {
2776 let (styled_dom, state) = rendered(TextInput::create());
2777 let (update, changes, nodes) =
2778 run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2779 default_on_virtual_key_down(state.clone(), info)
2780 });
2781
2782 assert_eq!(update, Update::DoNothing);
2783 assert_eq!(state_of(&state).cursor_pos, 0, "the cursor underflowed past zero");
2784 assert!(state_of(&state).text.is_empty());
2785 assert_eq!(
2787 pushed_texts(&changes),
2788 vec![(nodes.label.expect("no label"), String::new())],
2789 );
2790 }
2791
2792 #[test]
2793 fn backspace_deletes_one_scalar_not_one_grapheme() {
2794 let (styled_dom, state) = rendered(TextInput::create().with_text("e\u{301}".into()));
2797 let (_, _, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2798 default_on_virtual_key_down(state.clone(), info)
2799 });
2800 assert_eq!(state_of(&state).get_text(), "e");
2801 assert_eq!(state_of(&state).cursor_pos, 1);
2802 }
2803
2804 #[test]
2805 fn backspace_deletes_a_whole_astral_scalar_at_once() {
2806 let (styled_dom, state) = rendered(TextInput::create().with_text("a\u{10ffff}".into()));
2809 let (_, _, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2810 default_on_virtual_key_down(state.clone(), info)
2811 });
2812 assert_eq!(state_of(&state).get_text(), "a");
2813 assert_eq!(state_of(&state).text.as_slice(), &[0x61]);
2814 }
2815
2816 #[test]
2817 fn a_non_backspace_key_still_reaches_the_hook_but_leaves_the_buffer_alone() {
2818 let probe = recorder(Update::RefreshDom, TextInputValid::Yes);
2819 let (styled_dom, state) = rendered(
2820 TextInput::create().with_text("ab".into()).with_on_virtual_key_down(
2821 probe.clone(),
2822 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2823 ),
2824 );
2825
2826 let (update, changes, _) = run(Env::new(styled_dom).key(VirtualKeyCode::A), |info| {
2827 default_on_virtual_key_down(state.clone(), info)
2828 });
2829
2830 assert_eq!(update, Update::RefreshDom);
2831 assert!(changes.is_empty(), "a plain key press repainted the label: {changes:?}");
2832 assert_eq!(state_of(&state).get_text(), "ab");
2833 assert_eq!(recorded(&probe).len(), 1, "the hook must see every key, not just backspace");
2834 }
2835
2836 #[test]
2837 fn a_rejecting_hook_suppresses_the_deletion_but_keeps_its_update() {
2838 let probe = recorder(Update::RefreshDomAllWindows, TextInputValid::No);
2839 let (styled_dom, state) = rendered(
2840 TextInput::create().with_text("abc".into()).with_on_virtual_key_down(
2841 probe.clone(),
2842 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2843 ),
2844 );
2845
2846 let (update, changes, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2847 default_on_virtual_key_down(state.clone(), info)
2848 });
2849
2850 assert_eq!(update, Update::RefreshDomAllWindows);
2851 assert!(changes.is_empty(), "a vetoed backspace still repainted: {changes:?}");
2852 assert_eq!(state_of(&state).get_text(), "abc", "a vetoed backspace deleted anyway");
2853 assert_eq!(state_of(&state).cursor_pos, 3);
2854 }
2855
2856 #[test]
2857 fn the_virtual_key_hook_is_shown_the_state_from_before_the_deletion() {
2858 let probe = recorder(Update::DoNothing, TextInputValid::Yes);
2862 let (styled_dom, state) = rendered(
2863 TextInput::create().with_text("abc".into()).with_on_virtual_key_down(
2864 probe.clone(),
2865 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2866 ),
2867 );
2868
2869 let (_, _, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2870 default_on_virtual_key_down(state.clone(), info)
2871 });
2872
2873 let seen = recorded(&probe);
2874 assert_eq!(seen.len(), 1);
2875 assert_eq!(seen[0].get_text(), "abc");
2876 assert_eq!(seen[0].cursor_pos, 3);
2877 assert_eq!(state_of(&state).get_text(), "ab", "... and the deletion still happened");
2878 }
2879
2880 #[test]
2881 fn virtual_key_down_on_a_none_hit_node_skips_the_hook_entirely() {
2882 let probe = recorder(Update::RefreshDom, TextInputValid::Yes);
2883 let (styled_dom, state) = rendered(
2884 TextInput::create().with_text("abc".into()).with_on_virtual_key_down(
2885 probe.clone(),
2886 record_virtual_key as TextInputOnVirtualKeyDownCallbackType,
2887 ),
2888 );
2889
2890 let (result, changes, _) = run(
2891 Env::new(styled_dom).hit(node_none()).key(VirtualKeyCode::Back),
2892 |info| default_on_virtual_key_down_inner(state.clone(), info),
2893 );
2894
2895 assert_eq!(result, None);
2896 assert!(changes.is_empty());
2897 assert!(recorded(&probe).is_empty(), "the hook fired on a hit node that does not exist");
2898 assert_eq!(state_of(&state).get_text(), "abc");
2899 }
2900
2901 #[test]
2902 fn virtual_key_down_with_a_foreign_payload_is_an_inert_no_op() {
2903 let (styled_dom, _) = rendered(TextInput::create());
2904 let foreign = RefAny::new(vec![1_u32, 2, 3]);
2905 let (update, changes, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2906 default_on_virtual_key_down(foreign.clone(), info)
2907 });
2908 assert_eq!(update, Update::DoNothing);
2909 assert!(changes.is_empty());
2910 }
2911
2912 #[test]
2913 fn repeated_backspaces_drain_the_buffer_and_then_stop() {
2914 let (_, state) = rendered(TextInput::create().with_text("abc".into()));
2916 for i in 0..6 {
2917 let (styled_dom, _) = rendered(TextInput::create());
2920 let (update, _, _) = run(Env::new(styled_dom).key(VirtualKeyCode::Back), |info| {
2921 default_on_virtual_key_down(state.clone(), info)
2922 });
2923 assert_eq!(update, Update::DoNothing, "backspace #{i} reported work to do");
2924 }
2925 let after = state_of(&state);
2926 assert!(after.text.is_empty());
2927 assert_eq!(after.cursor_pos, 0);
2928 }
2929
2930 #[test]
2935 fn mouse_hover_is_inert_for_every_payload_and_every_hit_node() {
2936 let (styled_dom, state) = rendered(TextInput::create().with_text("abc".into()));
2937 let (update, changes, _) =
2938 run(Env::new(styled_dom), |info| default_on_mouse_hover(state.clone(), info));
2939 assert_eq!(update, Update::DoNothing);
2940 assert!(changes.is_empty());
2941 assert_eq!(state_of(&state).get_text(), "abc", "hovering edited the buffer");
2942
2943 let (styled_dom, _) = rendered(TextInput::create());
2944 let foreign = RefAny::new(0_u8);
2945 let (update, changes, _) = run(Env::new(styled_dom).hit(node_none()), |info| {
2946 default_on_mouse_hover(foreign.clone(), info)
2947 });
2948 assert_eq!(update, Update::DoNothing);
2949 assert!(changes.is_empty());
2950 }
2951}