@effindomv2/fui-rs 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/COMMERCIAL.md +7 -0
  2. package/Cargo.toml +34 -0
  3. package/LICENSE.md +9 -0
  4. package/README.md +88 -0
  5. package/package.json +54 -0
  6. package/scripts/build.sh +227 -0
  7. package/scripts/check-runtime-dependency.sh +41 -0
  8. package/scripts/framework-host-services.ts +40 -0
  9. package/scripts/generate-host-events.ts +17 -0
  10. package/scripts/generate-host-services.ts +28 -0
  11. package/scripts/hostgen/common.ts +73 -0
  12. package/scripts/hostgen/registry.ts +159 -0
  13. package/scripts/hostgen/rust-host-events.ts +171 -0
  14. package/scripts/hostgen/rust-host-services.ts +257 -0
  15. package/src/animation.rs +625 -0
  16. package/src/app.rs +324 -0
  17. package/src/assets.rs +572 -0
  18. package/src/bindings/mod.rs +1 -0
  19. package/src/bindings/ui.rs +705 -0
  20. package/src/bitmap.rs +317 -0
  21. package/src/bridge_callbacks.rs +242 -0
  22. package/src/context_menu_manager.rs +332 -0
  23. package/src/controls/anti_selection_area.rs +54 -0
  24. package/src/controls/button.rs +542 -0
  25. package/src/controls/checkbox.rs +372 -0
  26. package/src/controls/combobox.rs +1574 -0
  27. package/src/controls/context_menu.rs +1367 -0
  28. package/src/controls/control_template_set.rs +46 -0
  29. package/src/controls/control_tokens.rs +596 -0
  30. package/src/controls/dialog.rs +590 -0
  31. package/src/controls/dropdown.rs +1010 -0
  32. package/src/controls/form.rs +229 -0
  33. package/src/controls/internal/button_presenter.rs +142 -0
  34. package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
  35. package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
  36. package/src/controls/internal/dropdown_field_presenter.rs +269 -0
  37. package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
  38. package/src/controls/internal/mod.rs +13 -0
  39. package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
  40. package/src/controls/internal/pressable_labeled_control.rs +492 -0
  41. package/src/controls/internal/radio_indicator_presenter.rs +208 -0
  42. package/src/controls/internal/selectable_popup_list.rs +597 -0
  43. package/src/controls/internal/slider_presenter.rs +282 -0
  44. package/src/controls/internal/switch_indicator_presenter.rs +235 -0
  45. package/src/controls/internal/text_input_core.rs +1074 -0
  46. package/src/controls/internal/text_input_presenter.rs +108 -0
  47. package/src/controls/mod.rs +235 -0
  48. package/src/controls/nav_link.rs +395 -0
  49. package/src/controls/popup.rs +191 -0
  50. package/src/controls/progress_bar.rs +287 -0
  51. package/src/controls/radio_button.rs +348 -0
  52. package/src/controls/radio_group.rs +283 -0
  53. package/src/controls/selection_area.rs +82 -0
  54. package/src/controls/shared.rs +68 -0
  55. package/src/controls/slider.rs +701 -0
  56. package/src/controls/switch.rs +293 -0
  57. package/src/controls/templating.rs +49 -0
  58. package/src/controls/tests.rs +3905 -0
  59. package/src/controls/text_area.rs +207 -0
  60. package/src/controls/text_input.rs +192 -0
  61. package/src/debug.rs +146 -0
  62. package/src/drag_drop.rs +424 -0
  63. package/src/drag_gesture.rs +258 -0
  64. package/src/drawing.rs +385 -0
  65. package/src/event.rs +1603 -0
  66. package/src/external_drop.rs +467 -0
  67. package/src/fetch.rs +500 -0
  68. package/src/ffi.rs +3542 -0
  69. package/src/file.rs +1677 -0
  70. package/src/focus_adorner.rs +307 -0
  71. package/src/focus_visibility.rs +121 -0
  72. package/src/frame_scheduler.rs +159 -0
  73. package/src/frame_signal.rs +64 -0
  74. package/src/generated/ffi.rs +798 -0
  75. package/src/generated/framework_host_services.rs +74 -0
  76. package/src/generated/mod.rs +2 -0
  77. package/src/host_services.rs +162 -0
  78. package/src/image_sampling.rs +78 -0
  79. package/src/keyboard_scroll.rs +137 -0
  80. package/src/keyboard_scroll_tracker.rs +385 -0
  81. package/src/lib.rs +547 -0
  82. package/src/logger.rs +56 -0
  83. package/src/mobile_text_selection_toolbar.rs +1034 -0
  84. package/src/navigation.rs +48 -0
  85. package/src/node/core.rs +2210 -0
  86. package/src/node/custom_drawable.rs +149 -0
  87. package/src/node/flex_box.rs +874 -0
  88. package/src/node/grid.rs +304 -0
  89. package/src/node/helpers.rs +442 -0
  90. package/src/node/image.rs +506 -0
  91. package/src/node/mod.rs +315 -0
  92. package/src/node/scroll_bar.rs +845 -0
  93. package/src/node/scroll_box.rs +514 -0
  94. package/src/node/scroll_state.rs +121 -0
  95. package/src/node/scroll_view.rs +557 -0
  96. package/src/node/svg_node.rs +474 -0
  97. package/src/node/text_node.rs +633 -0
  98. package/src/node/virtual_list.rs +678 -0
  99. package/src/panic_hook.rs +36 -0
  100. package/src/persisted.rs +274 -0
  101. package/src/platform.rs +556 -0
  102. package/src/popup_presenter.rs +344 -0
  103. package/src/selection_handle_adorner.rs +838 -0
  104. package/src/signal.rs +134 -0
  105. package/src/text.rs +1065 -0
  106. package/src/theme.rs +571 -0
  107. package/src/timers.rs +106 -0
  108. package/src/tool_tip.rs +167 -0
  109. package/src/tool_tip_manager.rs +691 -0
  110. package/src/transitions.rs +41 -0
  111. package/src/typography.rs +520 -0
  112. package/src/viewport.rs +108 -0
  113. package/src/worker.rs +308 -0
  114. package/src/worker_job.rs +82 -0
  115. package/src/worker_runtime.rs +172 -0
@@ -0,0 +1,1074 @@
1
+ use super::text_input_presenter::{
2
+ create_default_text_input_presenter, TextInputPresenter, TextInputTemplate,
3
+ TextInputVisualState,
4
+ };
5
+ use crate::bindings::ui;
6
+ use crate::controls::control_template_set::get_control_templates;
7
+ use crate::controls::TextInputColors;
8
+ use crate::event::{
9
+ FocusChangedEventArgs, KeyEventArgs, SelectionChangedEventArgs, TextChangedEventArgs,
10
+ };
11
+ use crate::ffi::{
12
+ CursorStyle, KeyEventType, KeyModifier, PositionType, SemanticRole, TextVerticalAlign, Unit,
13
+ };
14
+ use crate::node::{
15
+ flex_box, FlexBox, FlexBoxSurface, HasFlexBoxRoot, Node, NodeHandle, ScrollBarVisibility,
16
+ ScrollBox, TextCore,
17
+ };
18
+ use crate::persisted::{persisted_value_adapter, PersistedStringCodec};
19
+ use crate::signal::SubscriptionGuard;
20
+ use crate::theme::{current_theme, subscribe};
21
+ use crate::{focus_adorner, focus_visibility};
22
+ use std::cell::{Cell, RefCell};
23
+ use std::cmp::min;
24
+ use std::rc::{Rc, Weak};
25
+
26
+ const UNLIMITED_TEXT_LENGTH: i32 = i32::MAX;
27
+
28
+ #[derive(Clone, Copy)]
29
+ pub(crate) struct TextInputProfile {
30
+ max_lines: i32,
31
+ multiline: bool,
32
+ wraps_by_default: bool,
33
+ default_semantic_label: &'static str,
34
+ }
35
+
36
+ impl TextInputProfile {
37
+ pub(crate) const fn single_line() -> Self {
38
+ Self {
39
+ max_lines: 1,
40
+ multiline: false,
41
+ wraps_by_default: false,
42
+ default_semantic_label: "Text input",
43
+ }
44
+ }
45
+
46
+ pub(crate) const fn multiline() -> Self {
47
+ Self {
48
+ max_lines: 0,
49
+ multiline: true,
50
+ wraps_by_default: true,
51
+ default_semantic_label: "Text area",
52
+ }
53
+ }
54
+ }
55
+
56
+ fn create_presenter(
57
+ profile: TextInputProfile,
58
+ template: Option<Rc<dyn TextInputTemplate>>,
59
+ ) -> Rc<dyn TextInputPresenter> {
60
+ if let Some(template) = template {
61
+ return template.create();
62
+ }
63
+ let app_template = get_control_templates().and_then(|set| {
64
+ if profile.multiline {
65
+ set.text_area
66
+ } else {
67
+ set.text_input
68
+ }
69
+ });
70
+ if let Some(template) = app_template {
71
+ return template.create();
72
+ }
73
+ create_default_text_input_presenter()
74
+ }
75
+
76
+ fn char_count(text: &str) -> u32 {
77
+ text.chars().count() as u32
78
+ }
79
+
80
+ fn char_to_byte(text: &str, index: u32) -> u32 {
81
+ let target = min(index, char_count(text)) as usize;
82
+ if target == 0 {
83
+ return 0;
84
+ }
85
+ for (position, (byte, _)) in text.char_indices().enumerate() {
86
+ if position == target {
87
+ return byte as u32;
88
+ }
89
+ }
90
+ text.len() as u32
91
+ }
92
+
93
+ fn byte_to_char(text: &str, byte_index: u32) -> u32 {
94
+ let target = min(byte_index as usize, text.len());
95
+ let mut count = 0;
96
+ for (byte, _) in text.char_indices() {
97
+ if byte >= target {
98
+ break;
99
+ }
100
+ count += 1;
101
+ }
102
+ if target == text.len() {
103
+ text.chars().count() as u32
104
+ } else {
105
+ count
106
+ }
107
+ }
108
+
109
+ fn is_enabled(root: &FlexBox) -> bool {
110
+ root.retained_node_ref().is_enabled_for_routing()
111
+ }
112
+
113
+ pub struct TextInputCore {
114
+ self_weak: RefCell<Weak<TextInputCore>>,
115
+ profile: TextInputProfile,
116
+ root: FlexBox,
117
+ editor_text: TextCore,
118
+ editor_scroll_box: Option<ScrollBox>,
119
+ placeholder_text: TextCore,
120
+ placeholder_host: FlexBox,
121
+ placeholder_attached: Cell<bool>,
122
+ presenter: RefCell<Rc<dyn TextInputPresenter>>,
123
+ template: RefCell<Option<Rc<dyn TextInputTemplate>>>,
124
+ colors_value: Cell<Option<TextInputColors>>,
125
+ text_value: RefCell<String>,
126
+ placeholder_value: RefCell<String>,
127
+ max_chars_value: Cell<i32>,
128
+ read_only_value: Cell<bool>,
129
+ password_value: Cell<bool>,
130
+ accepts_tab_value: Cell<bool>,
131
+ host_autofill_hint_value: RefCell<Option<String>>,
132
+ wrapping_value: Cell<bool>,
133
+ vertical_scrollbar_visibility_value: Cell<ScrollBarVisibility>,
134
+ horizontal_scrollbar_visibility_value: Cell<ScrollBarVisibility>,
135
+ font_family_override: RefCell<Option<crate::FontFamily>>,
136
+ font_size_override: Cell<f32>,
137
+ has_font_size_override: Cell<bool>,
138
+ selection_start_chars: Cell<u32>,
139
+ selection_end_chars: Cell<u32>,
140
+ selection_start_bytes: Cell<u32>,
141
+ selection_end_bytes: Cell<u32>,
142
+ focused_state: Cell<bool>,
143
+ changed_callback: RefCell<Option<Rc<dyn Fn(TextChangedEventArgs)>>>,
144
+ selection_changed_callback: RefCell<Option<Rc<dyn Fn(SelectionChangedEventArgs)>>>,
145
+ focus_changed_callback: RefCell<Option<Rc<dyn Fn(FocusChangedEventArgs)>>>,
146
+ theme_guard: RefCell<Option<SubscriptionGuard>>,
147
+ focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
148
+ }
149
+
150
+ impl Default for TextInputCore {
151
+ fn default() -> Self {
152
+ Self::new()
153
+ }
154
+ }
155
+
156
+ impl TextInputCore {
157
+ pub fn new() -> Self {
158
+ Self::with_profile(TextInputProfile::single_line())
159
+ }
160
+
161
+ pub(crate) fn multiline() -> Self {
162
+ Self::with_profile(TextInputProfile::multiline())
163
+ }
164
+
165
+ fn with_profile(profile: TextInputProfile) -> Self {
166
+ let root = flex_box();
167
+ root.clip_to_bounds(true)
168
+ .interactive(true)
169
+ .reflect_semantic_disabled_from_enabled()
170
+ .selection_area_barrier(true);
171
+
172
+ let editor_text = TextCore::new("");
173
+ editor_text
174
+ .semantic_role(SemanticRole::Textbox)
175
+ .reflect_semantic_disabled_from_enabled()
176
+ .focusable(true, 0)
177
+ .selectable(true, 0)
178
+ .editable(true);
179
+ let placeholder_text = TextCore::new("");
180
+ let placeholder_host = flex_box();
181
+ placeholder_host
182
+ .position_type(PositionType::Absolute)
183
+ .clip_to_bounds(false)
184
+ .interactive(true)
185
+ .child(&placeholder_text);
186
+
187
+ let editor_scroll_box = if profile.multiline {
188
+ let scroll_box = ScrollBox::new();
189
+ scroll_box
190
+ .fill_size()
191
+ .persist_scroll(false)
192
+ .vertical_scrollbar_visibility(ScrollBarVisibility::Auto)
193
+ .horizontal_scrollbar_visibility(ScrollBarVisibility::Auto)
194
+ .child(&editor_text);
195
+ root.child(&scroll_box);
196
+ Some(scroll_box)
197
+ } else {
198
+ root.child(&editor_text);
199
+ None
200
+ };
201
+
202
+ let presenter = create_presenter(profile, None);
203
+ presenter.bind(root.clone(), editor_text.clone(), placeholder_host.clone());
204
+
205
+ let this = Self {
206
+ self_weak: RefCell::new(Weak::new()),
207
+ profile,
208
+ root,
209
+ editor_text,
210
+ editor_scroll_box,
211
+ placeholder_text,
212
+ placeholder_host,
213
+ placeholder_attached: Cell::new(false),
214
+ presenter: RefCell::new(presenter),
215
+ template: RefCell::new(None),
216
+ colors_value: Cell::new(None),
217
+ text_value: RefCell::new(String::new()),
218
+ placeholder_value: RefCell::new(String::new()),
219
+ max_chars_value: Cell::new(UNLIMITED_TEXT_LENGTH),
220
+ read_only_value: Cell::new(false),
221
+ password_value: Cell::new(false),
222
+ accepts_tab_value: Cell::new(false),
223
+ host_autofill_hint_value: RefCell::new(None),
224
+ wrapping_value: Cell::new(profile.wraps_by_default),
225
+ vertical_scrollbar_visibility_value: Cell::new(ScrollBarVisibility::Auto),
226
+ horizontal_scrollbar_visibility_value: Cell::new(ScrollBarVisibility::Auto),
227
+ font_family_override: RefCell::new(None),
228
+ font_size_override: Cell::new(0.0),
229
+ has_font_size_override: Cell::new(false),
230
+ selection_start_chars: Cell::new(0),
231
+ selection_end_chars: Cell::new(0),
232
+ selection_start_bytes: Cell::new(0),
233
+ selection_end_bytes: Cell::new(0),
234
+ focused_state: Cell::new(false),
235
+ changed_callback: RefCell::new(None),
236
+ selection_changed_callback: RefCell::new(None),
237
+ focus_changed_callback: RefCell::new(None),
238
+ theme_guard: RefCell::new(None),
239
+ focus_visibility_guard: RefCell::new(None),
240
+ };
241
+ this.sync_semantic_label();
242
+ this.sync_editor_limits();
243
+ this.sync_editor_editability();
244
+ this.sync_browser_input_metadata();
245
+ this.sync_theme_state();
246
+ this.sync_scroll_chrome_state();
247
+ this.sync_placeholder_visibility();
248
+ this
249
+ }
250
+
251
+ pub(crate) fn finish_init(&self, weak: Weak<TextInputCore>) {
252
+ *self.self_weak.borrow_mut() = weak;
253
+ self.install_events();
254
+ self.install_persisted_state();
255
+ self.install_theme_subscription();
256
+ self.install_focus_visibility_subscription();
257
+ self.install_effective_enabled_subscription();
258
+ }
259
+
260
+ pub fn text(&self, value: impl Into<String>) -> &Self {
261
+ let value = value.into();
262
+ *self.text_value.borrow_mut() = value.clone();
263
+ self.editor_text.text(value);
264
+ self.caret_to_end();
265
+ self.sync_semantic_label();
266
+ self.sync_placeholder_visibility();
267
+ self
268
+ }
269
+
270
+ pub fn value(&self) -> String {
271
+ self.text_value.borrow().clone()
272
+ }
273
+
274
+ pub fn selection_start(&self) -> u32 {
275
+ self.selection_start_chars.get()
276
+ }
277
+
278
+ pub fn selection_end(&self) -> u32 {
279
+ self.selection_end_chars.get()
280
+ }
281
+
282
+ pub fn selection_start_byte_offset(&self) -> u32 {
283
+ self.selection_start_bytes.get()
284
+ }
285
+
286
+ pub fn selection_end_byte_offset(&self) -> u32 {
287
+ self.selection_end_bytes.get()
288
+ }
289
+
290
+ pub fn placeholder(&self, value: impl Into<String>) -> &Self {
291
+ let value = value.into();
292
+ *self.placeholder_value.borrow_mut() = value.clone();
293
+ self.placeholder_text.text(value);
294
+ self.sync_semantic_label();
295
+ self.sync_placeholder_visibility();
296
+ self
297
+ }
298
+
299
+ pub fn max_chars(&self, limit: i32) -> &Self {
300
+ self.max_chars_value.set(if limit < 0 {
301
+ UNLIMITED_TEXT_LENGTH
302
+ } else {
303
+ limit
304
+ });
305
+ self.sync_editor_limits();
306
+ self
307
+ }
308
+
309
+ pub fn read_only(&self, flag: bool) -> &Self {
310
+ self.read_only_value.set(flag);
311
+ self.sync_editor_editability();
312
+ self
313
+ }
314
+
315
+ pub fn accepts_tab(&self, flag: bool) -> &Self {
316
+ self.accepts_tab_value.set(flag);
317
+ self.editor_text.editor_accepts_tab(flag);
318
+ self
319
+ }
320
+
321
+ pub fn password(&self, flag: bool) -> &Self {
322
+ self.password_value.set(flag);
323
+ self.editor_text.obscured(flag);
324
+ self.sync_browser_input_metadata();
325
+ self.sync_semantic_label();
326
+ self
327
+ }
328
+
329
+ pub fn host_autofill(&self, hint: Option<&str>) -> &Self {
330
+ *self.host_autofill_hint_value.borrow_mut() =
331
+ hint.filter(|value| !value.is_empty()).map(str::to_string);
332
+ self.sync_browser_input_metadata();
333
+ self
334
+ }
335
+
336
+ pub fn selection_range(&self, start: u32, end: u32) -> &Self {
337
+ let text = self.text_value.borrow();
338
+ let start = min(start, char_count(&text));
339
+ let end = min(end, char_count(&text));
340
+ let start_bytes = char_to_byte(&text, start);
341
+ let end_bytes = char_to_byte(&text, end);
342
+ drop(text);
343
+ self.selection_start_chars.set(start);
344
+ self.selection_end_chars.set(end);
345
+ self.selection_start_bytes.set(start_bytes);
346
+ self.selection_end_bytes.set(end_bytes);
347
+ self.editor_text.selection_range(start_bytes, end_bytes);
348
+ self
349
+ }
350
+
351
+ pub fn caret(&self, position: u32) -> &Self {
352
+ self.selection_range(position, position)
353
+ }
354
+
355
+ pub fn caret_to_end(&self) -> &Self {
356
+ let end = char_count(&self.text_value.borrow());
357
+ self.selection_range(end, end)
358
+ }
359
+
360
+ pub fn colors(&self, colors: Option<TextInputColors>) -> &Self {
361
+ self.colors_value.set(colors);
362
+ self.sync_theme_state();
363
+ self
364
+ }
365
+
366
+ pub fn enabled(&self, enabled: bool) -> &Self {
367
+ self.root.enabled(enabled);
368
+ self.sync_editor_editability();
369
+ self.sync_theme_state();
370
+ self.sync_placeholder_visibility();
371
+ self
372
+ }
373
+
374
+ pub fn focusable(&self, enabled: bool, tab_index: i32) -> &Self {
375
+ self.editor_text.focusable(enabled, tab_index);
376
+ self
377
+ }
378
+
379
+ pub fn node_id(&self, id: impl Into<String>) -> &Self {
380
+ self.editor_text.node_id(id);
381
+ self
382
+ }
383
+
384
+ pub fn line_height(&self, value: f32) -> &Self {
385
+ self.editor_text.line_height(value);
386
+ self.placeholder_text.line_height(value);
387
+ self
388
+ }
389
+
390
+ pub fn wrapping(&self, flag: bool) -> &Self {
391
+ if self.wrapping_value.get() == flag {
392
+ return self;
393
+ }
394
+ self.wrapping_value.set(flag);
395
+ self.sync_editor_wrapping();
396
+ self.sync_scroll_chrome_state();
397
+ self.sync_theme_state();
398
+ self
399
+ }
400
+
401
+ pub fn vertical_scrollbar_visibility(&self, mode: ScrollBarVisibility) -> &Self {
402
+ self.vertical_scrollbar_visibility_value.set(mode);
403
+ self.sync_scroll_chrome_state();
404
+ self
405
+ }
406
+
407
+ pub fn horizontal_scrollbar_visibility(&self, mode: ScrollBarVisibility) -> &Self {
408
+ self.horizontal_scrollbar_visibility_value.set(mode);
409
+ self.sync_scroll_chrome_state();
410
+ self
411
+ }
412
+
413
+ pub fn font_family(&self, family: crate::FontFamily) -> &Self {
414
+ *self.font_family_override.borrow_mut() = Some(family);
415
+ self.sync_theme_state();
416
+ self
417
+ }
418
+
419
+ pub fn font_size(&self, size: f32) -> &Self {
420
+ self.font_size_override.set(size);
421
+ self.has_font_size_override.set(size > 0.0);
422
+ self.sync_theme_state();
423
+ self
424
+ }
425
+
426
+ pub fn template(&self, template: Option<Rc<dyn TextInputTemplate>>) -> &Self {
427
+ *self.template.borrow_mut() = template.clone();
428
+ let presenter = create_presenter(self.profile, template);
429
+ presenter.bind(
430
+ self.root.clone(),
431
+ self.editor_text.clone(),
432
+ self.placeholder_host.clone(),
433
+ );
434
+ *self.presenter.borrow_mut() = presenter;
435
+ self.sync_theme_state();
436
+ self
437
+ }
438
+
439
+ pub fn on_changed(&self, handler: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
440
+ *self.changed_callback.borrow_mut() = Some(Rc::new(handler));
441
+ self
442
+ }
443
+
444
+ pub fn on_text_changed(&self, handler: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
445
+ self.on_changed(handler)
446
+ }
447
+
448
+ pub fn on_selection_changed(
449
+ &self,
450
+ handler: impl Fn(SelectionChangedEventArgs) + 'static,
451
+ ) -> &Self {
452
+ *self.selection_changed_callback.borrow_mut() = Some(Rc::new(handler));
453
+ self
454
+ }
455
+
456
+ pub fn on_focus_changed(&self, handler: impl Fn(FocusChangedEventArgs) + 'static) -> &Self {
457
+ *self.focus_changed_callback.borrow_mut() = Some(Rc::new(handler));
458
+ self
459
+ }
460
+
461
+ pub fn focus_now(&self) -> &Self {
462
+ self.focus_editor();
463
+ self.editor_text.selection_range(
464
+ self.selection_start_bytes.get(),
465
+ self.selection_end_bytes.get(),
466
+ );
467
+ self
468
+ }
469
+
470
+ pub fn scroll_offset_x(&self) -> f32 {
471
+ self.editor_scroll_box
472
+ .as_ref()
473
+ .map(|scroll_box| scroll_box.scroll_state().offset_x())
474
+ .unwrap_or(0.0)
475
+ }
476
+
477
+ pub fn scroll_offset_y(&self) -> f32 {
478
+ self.editor_scroll_box
479
+ .as_ref()
480
+ .map(|scroll_box| scroll_box.scroll_state().offset_y())
481
+ .unwrap_or(0.0)
482
+ }
483
+
484
+ pub fn scroll_to(&self, x: f32, y: f32) -> &Self {
485
+ if let Some(scroll_box) = &self.editor_scroll_box {
486
+ scroll_box.scroll_to(x, y);
487
+ }
488
+ self
489
+ }
490
+
491
+ fn install_events(&self) {
492
+ let weak = self.self_weak.borrow().clone();
493
+ self.root.on_pointer_down(move |event| {
494
+ if let Some(this) = weak.upgrade() {
495
+ if event.target_handle() == this.root.handle() {
496
+ this.handle_shell_pointer_down();
497
+ event.handled = true;
498
+ }
499
+ }
500
+ });
501
+
502
+ let weak = self.self_weak.borrow().clone();
503
+ self.placeholder_host.on_pointer_down(move |event| {
504
+ if let Some(this) = weak.upgrade() {
505
+ this.handle_shell_pointer_down();
506
+ event.handled = true;
507
+ }
508
+ });
509
+
510
+ if let Some(scroll_box) = &self.editor_scroll_box {
511
+ let weak = self.self_weak.borrow().clone();
512
+ scroll_box.viewport().on_pointer_down(move |_event| {
513
+ if let Some(this) = weak.upgrade() {
514
+ this.handle_viewport_pointer_down();
515
+ }
516
+ });
517
+ }
518
+
519
+ let weak = self.self_weak.borrow().clone();
520
+ self.editor_text
521
+ .on_text_replaced(move |_start, _end, _text| {
522
+ if let Some(this) = weak.upgrade() {
523
+ this.handle_editor_text_changed();
524
+ }
525
+ });
526
+
527
+ let weak = self.self_weak.borrow().clone();
528
+ self.editor_text.on_text_changed(move |_event| {
529
+ if let Some(this) = weak.upgrade() {
530
+ this.handle_editor_text_changed();
531
+ }
532
+ });
533
+
534
+ let weak = self.self_weak.borrow().clone();
535
+ self.editor_text.on_selection_changed(move |event| {
536
+ if let Some(this) = weak.upgrade() {
537
+ this.handle_editor_selection_changed(event.start, event.end);
538
+ }
539
+ });
540
+
541
+ let weak = self.self_weak.borrow().clone();
542
+ self.editor_text.on_key_down(move |event| {
543
+ if let Some(this) = weak.upgrade() {
544
+ this.handle_editor_key_down(event);
545
+ }
546
+ });
547
+
548
+ let weak = self.self_weak.borrow().clone();
549
+ self.editor_text.on_focus_changed(move |event| {
550
+ if let Some(this) = weak.upgrade() {
551
+ this.handle_editor_focus_changed(event.focused);
552
+ }
553
+ });
554
+ }
555
+
556
+ fn install_theme_subscription(&self) {
557
+ let weak = self.self_weak.borrow().clone();
558
+ let guard = subscribe(move |_theme| {
559
+ if let Some(this) = weak.upgrade() {
560
+ this.sync_theme_state();
561
+ }
562
+ });
563
+ *self.theme_guard.borrow_mut() = Some(guard);
564
+ }
565
+
566
+ fn install_focus_visibility_subscription(&self) {
567
+ let weak = self.self_weak.borrow().clone();
568
+ let guard = focus_visibility::subscribe(move |_visible| {
569
+ if let Some(this) = weak.upgrade() {
570
+ this.sync_focus_chrome();
571
+ }
572
+ });
573
+ *self.focus_visibility_guard.borrow_mut() = Some(guard);
574
+ }
575
+
576
+ fn install_effective_enabled_subscription(&self) {
577
+ let weak = self.self_weak.borrow().clone();
578
+ self.root
579
+ .retained_node_ref()
580
+ .on_effective_enabled_changed(Rc::new(move |_enabled| {
581
+ let Some(this) = weak.upgrade() else {
582
+ return;
583
+ };
584
+ this.sync_editor_editability();
585
+ this.sync_theme_state();
586
+ this.sync_placeholder_visibility();
587
+ }));
588
+ }
589
+
590
+ fn install_persisted_state(&self) {
591
+ let weak = self.self_weak.borrow().clone();
592
+ self.editor_text.persist_state(persisted_value_adapter(
593
+ "text-input-value",
594
+ PersistedStringCodec,
595
+ 1,
596
+ {
597
+ let weak = weak.clone();
598
+ move || {
599
+ let this = weak.upgrade()?;
600
+ if this.password_value.get() {
601
+ None
602
+ } else {
603
+ Some(this.value())
604
+ }
605
+ }
606
+ },
607
+ move |value| {
608
+ let Some(this) = weak.upgrade() else {
609
+ return;
610
+ };
611
+ if this.password_value.get() {
612
+ return;
613
+ }
614
+ this.apply_persisted_text(value);
615
+ },
616
+ ));
617
+ }
618
+
619
+ fn sync_theme_state(&self) {
620
+ let theme = current_theme();
621
+ let resolved_font_family = self
622
+ .font_family_override
623
+ .borrow()
624
+ .clone()
625
+ .unwrap_or_else(|| theme.fonts.body_family.clone());
626
+ let resolved_font_size = if self.has_font_size_override.get() {
627
+ self.font_size_override.get()
628
+ } else {
629
+ theme.fonts.size_body
630
+ };
631
+ let line_height = resolved_font_size + theme.spacing.sm;
632
+ self.presenter.borrow().apply(
633
+ theme.clone(),
634
+ &TextInputVisualState {
635
+ multiline: self.profile.multiline,
636
+ enabled: is_enabled(&self.root),
637
+ wrapping: self.wrapping_value.get(),
638
+ },
639
+ self.colors_value.get(),
640
+ );
641
+
642
+ let colors = self.colors_value.get();
643
+ let text_color = if is_enabled(&self.root) {
644
+ colors
645
+ .filter(|value| value.has_text_primary())
646
+ .map(|value| value.text_primary_color())
647
+ .unwrap_or(theme.colors.text_primary)
648
+ } else {
649
+ colors
650
+ .filter(|value| value.has_text_muted())
651
+ .map(|value| value.text_muted_color())
652
+ .unwrap_or(theme.colors.text_muted)
653
+ };
654
+ let caret_color = colors
655
+ .filter(|value| value.has_caret())
656
+ .map(|value| value.caret_color())
657
+ .unwrap_or(theme.colors.accent);
658
+ let placeholder_color = colors
659
+ .filter(|value| value.has_placeholder())
660
+ .map(|value| value.placeholder_color())
661
+ .unwrap_or(theme.colors.text_muted);
662
+
663
+ self.editor_text
664
+ .width(
665
+ if self.should_editor_track_viewport_width() {
666
+ 100.0
667
+ } else {
668
+ 0.0
669
+ },
670
+ if self.should_editor_track_viewport_width() {
671
+ Unit::Percent
672
+ } else {
673
+ Unit::Auto
674
+ },
675
+ )
676
+ .height(
677
+ if self.profile.multiline {
678
+ 0.0
679
+ } else {
680
+ line_height
681
+ },
682
+ if self.profile.multiline {
683
+ Unit::Auto
684
+ } else {
685
+ Unit::Pixel
686
+ },
687
+ )
688
+ .font_family(resolved_font_family.clone())
689
+ .font_size(resolved_font_size)
690
+ .text_vertical_align(if self.profile.multiline {
691
+ TextVerticalAlign::Top
692
+ } else {
693
+ TextVerticalAlign::Center
694
+ })
695
+ .text_color(text_color)
696
+ .caret_color(caret_color)
697
+ .wrapping(self.wrapping_value.get());
698
+ self.placeholder_host.width(100.0, Unit::Percent).height(
699
+ if self.profile.multiline {
700
+ 0.0
701
+ } else {
702
+ line_height
703
+ },
704
+ if self.profile.multiline {
705
+ Unit::Auto
706
+ } else {
707
+ Unit::Pixel
708
+ },
709
+ );
710
+ self.placeholder_text
711
+ .width(100.0, Unit::Percent)
712
+ .height(
713
+ if self.profile.multiline {
714
+ 0.0
715
+ } else {
716
+ line_height
717
+ },
718
+ if self.profile.multiline {
719
+ Unit::Auto
720
+ } else {
721
+ Unit::Pixel
722
+ },
723
+ )
724
+ .font_family(resolved_font_family)
725
+ .font_size(resolved_font_size)
726
+ .text_vertical_align(if self.profile.multiline {
727
+ TextVerticalAlign::Top
728
+ } else {
729
+ TextVerticalAlign::Center
730
+ })
731
+ .text_color(placeholder_color)
732
+ .wrapping(self.wrapping_value.get());
733
+ if let Some(scroll_box) = &self.editor_scroll_box {
734
+ scroll_box.flex_box_root().cursor(CursorStyle::Default);
735
+ scroll_box.fill_size();
736
+ self.sync_scroll_chrome_state();
737
+ }
738
+ self.sync_focus_chrome();
739
+ }
740
+
741
+ fn handle_editor_focus_changed(&self, focused: bool) {
742
+ if self.focused_state.get() == focused {
743
+ return;
744
+ }
745
+ self.focused_state.set(focused);
746
+ self.sync_focus_chrome();
747
+ self.sync_placeholder_visibility();
748
+ if let Some(callback) = self.focus_changed_callback.borrow().clone() {
749
+ callback(FocusChangedEventArgs { focused });
750
+ }
751
+ }
752
+
753
+ fn handle_editor_text_changed(&self) {
754
+ let value = self.editor_text.text_value();
755
+ if *self.text_value.borrow() == value {
756
+ return;
757
+ }
758
+ *self.text_value.borrow_mut() = value.clone();
759
+ if self.password_value.get() {
760
+ self.editor_text.obscured(true);
761
+ }
762
+ self.clamp_selection_to_text();
763
+ self.sync_semantic_label();
764
+ self.sync_placeholder_visibility();
765
+ if let Some(callback) = self.changed_callback.borrow().clone() {
766
+ callback(TextChangedEventArgs { text: value });
767
+ }
768
+ }
769
+
770
+ fn handle_editor_selection_changed(&self, start: u32, end: u32) {
771
+ self.selection_start_bytes.set(start);
772
+ self.selection_end_bytes.set(end);
773
+ let text = self.text_value.borrow();
774
+ let start_chars = byte_to_char(&text, start);
775
+ let end_chars = byte_to_char(&text, end);
776
+ let start_bytes = char_to_byte(&text, start_chars);
777
+ let end_bytes = char_to_byte(&text, end_chars);
778
+ drop(text);
779
+ self.selection_start_bytes.set(start_bytes);
780
+ self.selection_end_bytes.set(end_bytes);
781
+ self.selection_start_chars.set(start_chars);
782
+ self.selection_end_chars.set(end_chars);
783
+ if let Some(callback) = self.selection_changed_callback.borrow().clone() {
784
+ callback(SelectionChangedEventArgs {
785
+ start: start_chars,
786
+ end: end_chars,
787
+ });
788
+ }
789
+ }
790
+
791
+ fn handle_editor_key_down(&self, event: &mut KeyEventArgs) {
792
+ if !self.accepts_tab_value.get() && !Self::is_text_navigation_key(event) {
793
+ return;
794
+ }
795
+ if event.event_type != KeyEventType::Down || !is_enabled(&self.root) {
796
+ return;
797
+ }
798
+ if Self::is_text_navigation_key(event) {
799
+ event.handled = true;
800
+ return;
801
+ }
802
+ if event.key != "Tab" || event.modifiers != 0 || self.read_only_value.get() {
803
+ return;
804
+ }
805
+ self.replace_selection_with_text("\t");
806
+ event.handled = true;
807
+ }
808
+
809
+ fn is_text_navigation_key(event: &KeyEventArgs) -> bool {
810
+ let non_shift_modifiers = event.modifiers
811
+ & ((KeyModifier::Ctrl as u32) | (KeyModifier::Alt as u32) | (KeyModifier::Meta as u32));
812
+ event.event_type == KeyEventType::Down
813
+ && non_shift_modifiers == 0
814
+ && matches!(
815
+ event.key.as_str(),
816
+ "ArrowLeft"
817
+ | "ArrowRight"
818
+ | "ArrowUp"
819
+ | "ArrowDown"
820
+ | "Home"
821
+ | "End"
822
+ | "PageUp"
823
+ | "PageDown"
824
+ )
825
+ }
826
+
827
+ fn replace_selection_with_text(&self, inserted: &str) {
828
+ let text = self.text_value.borrow().clone();
829
+ let start = min(
830
+ self.selection_start_chars.get(),
831
+ self.selection_end_chars.get(),
832
+ );
833
+ let end = self
834
+ .selection_start_chars
835
+ .get()
836
+ .max(self.selection_end_chars.get());
837
+ let current_len = char_count(&text);
838
+ let inserted_len = char_count(inserted);
839
+ let replaced_len = end - start;
840
+ if current_len - replaced_len + inserted_len > self.max_chars_value.get() as u32 {
841
+ return;
842
+ }
843
+
844
+ let start_byte = char_to_byte(&text, start) as usize;
845
+ let end_byte = char_to_byte(&text, end) as usize;
846
+ let mut value =
847
+ String::with_capacity(text.len() - (end_byte - start_byte) + inserted.len());
848
+ value.push_str(&text[..start_byte]);
849
+ value.push_str(inserted);
850
+ value.push_str(&text[end_byte..]);
851
+ let caret = start + inserted_len;
852
+
853
+ *self.text_value.borrow_mut() = value.clone();
854
+ let handle = self.editor_text.handle();
855
+ if handle != NodeHandle::INVALID {
856
+ let caret_byte = char_to_byte(&value, caret);
857
+ ui::replace_text_range(
858
+ handle.raw(),
859
+ start_byte as u32,
860
+ end_byte as u32,
861
+ inserted,
862
+ caret_byte,
863
+ );
864
+ self.selection_start_chars.set(caret);
865
+ self.selection_end_chars.set(caret);
866
+ self.selection_start_bytes.set(caret_byte);
867
+ self.selection_end_bytes.set(caret_byte);
868
+ } else {
869
+ self.editor_text.text(value.clone());
870
+ if self.password_value.get() {
871
+ self.editor_text.obscured(true);
872
+ }
873
+ self.selection_range(caret, caret);
874
+ }
875
+ self.sync_semantic_label();
876
+ self.sync_placeholder_visibility();
877
+ if let Some(callback) = self.changed_callback.borrow().clone() {
878
+ callback(TextChangedEventArgs { text: value });
879
+ }
880
+ if let Some(callback) = self.selection_changed_callback.borrow().clone() {
881
+ callback(SelectionChangedEventArgs {
882
+ start: caret,
883
+ end: caret,
884
+ });
885
+ }
886
+ }
887
+
888
+ fn sync_editor_limits(&self) {
889
+ self.editor_text
890
+ .text_limits(self.max_chars_value.get(), self.profile.max_lines);
891
+ }
892
+
893
+ fn sync_editor_editability(&self) {
894
+ self.editor_text.selectable(is_enabled(&self.root), 0);
895
+ self.editor_text
896
+ .editable(!self.read_only_value.get() && is_enabled(&self.root));
897
+ }
898
+
899
+ fn sync_browser_input_metadata(&self) {
900
+ let handle = self.editor_text.handle();
901
+ if handle != NodeHandle::INVALID {
902
+ ui::register_text_input_metadata(
903
+ handle.raw(),
904
+ self.password_value.get(),
905
+ self.host_autofill_hint_value.borrow().as_deref(),
906
+ );
907
+ }
908
+ }
909
+
910
+ fn sync_semantic_label(&self) {
911
+ let label = if !self.placeholder_value.borrow().is_empty() {
912
+ self.placeholder_value.borrow().clone()
913
+ } else if self.read_only_value.get() && !self.text_value.borrow().is_empty() {
914
+ self.text_value.borrow().clone()
915
+ } else if self.password_value.get() {
916
+ "Password input".to_string()
917
+ } else {
918
+ self.profile.default_semantic_label.to_string()
919
+ };
920
+ self.editor_text.default_semantic_label(label);
921
+ }
922
+
923
+ fn sync_editor_wrapping(&self) {
924
+ self.editor_text.wrapping(self.wrapping_value.get());
925
+ self.placeholder_text.wrapping(self.wrapping_value.get());
926
+ if self.profile.multiline {
927
+ self.editor_text.width(
928
+ if self.should_editor_track_viewport_width() {
929
+ 100.0
930
+ } else {
931
+ 0.0
932
+ },
933
+ if self.should_editor_track_viewport_width() {
934
+ Unit::Percent
935
+ } else {
936
+ Unit::Auto
937
+ },
938
+ );
939
+ }
940
+ }
941
+
942
+ fn sync_scroll_chrome_state(&self) {
943
+ if let Some(scroll_box) = &self.editor_scroll_box {
944
+ let allow_horizontal_scroll = !self.wrapping_value.get();
945
+ scroll_box
946
+ .vertical_scrollbar_visibility(self.vertical_scrollbar_visibility_value.get())
947
+ .horizontal_scrollbar_visibility(if allow_horizontal_scroll {
948
+ self.horizontal_scrollbar_visibility_value.get()
949
+ } else {
950
+ ScrollBarVisibility::Never
951
+ });
952
+ if !self.profile.multiline {
953
+ scroll_box.scroll_enabled_y(false);
954
+ } else {
955
+ scroll_box.scroll_enabled_y(true);
956
+ }
957
+ scroll_box.scroll_enabled_x(allow_horizontal_scroll);
958
+ }
959
+ }
960
+
961
+ fn should_editor_track_viewport_width(&self) -> bool {
962
+ !self.profile.multiline || self.wrapping_value.get()
963
+ }
964
+
965
+ fn sync_placeholder_visibility(&self) {
966
+ let should_show =
967
+ self.text_value.borrow().is_empty() && !self.placeholder_value.borrow().is_empty();
968
+ self.placeholder_text.text(if should_show {
969
+ self.placeholder_value.borrow().clone()
970
+ } else {
971
+ String::new()
972
+ });
973
+ if should_show {
974
+ if !self.placeholder_attached.get() {
975
+ self.placeholder_attached.set(true);
976
+ self.root.child(&self.placeholder_host);
977
+ }
978
+ return;
979
+ }
980
+ if !self.placeholder_attached.get() {
981
+ return;
982
+ }
983
+ self.placeholder_attached.set(false);
984
+ self.root.remove_child(&self.placeholder_host);
985
+ }
986
+
987
+ fn clamp_selection_to_text(&self) {
988
+ let text = self.text_value.borrow();
989
+ let char_len = char_count(&text);
990
+ let start_chars = min(self.selection_start_chars.get(), char_len);
991
+ let end_chars = min(self.selection_end_chars.get(), char_len);
992
+ let start_bytes = char_to_byte(&text, start_chars);
993
+ let end_bytes = char_to_byte(&text, end_chars);
994
+ drop(text);
995
+ self.selection_start_chars.set(start_chars);
996
+ self.selection_end_chars.set(end_chars);
997
+ self.selection_start_bytes.set(start_bytes);
998
+ self.selection_end_bytes.set(end_bytes);
999
+ }
1000
+
1001
+ fn apply_persisted_text(&self, value: String) {
1002
+ if *self.text_value.borrow() == value {
1003
+ return;
1004
+ }
1005
+ self.text(value.clone());
1006
+ if let Some(callback) = self.changed_callback.borrow().clone() {
1007
+ callback(TextChangedEventArgs { text: value });
1008
+ }
1009
+ }
1010
+
1011
+ fn sync_focus_chrome(&self) {
1012
+ if self.focused_state.get()
1013
+ && is_enabled(&self.root)
1014
+ && focus_visibility::keyboard_focus_visible()
1015
+ {
1016
+ focus_adorner::show_standard(&self.root, current_theme().spacing.sm);
1017
+ return;
1018
+ }
1019
+ focus_adorner::hide_owner(&self.root);
1020
+ }
1021
+
1022
+ fn focus_editor(&self) {
1023
+ if is_enabled(&self.root) {
1024
+ self.editor_text.focus_now();
1025
+ }
1026
+ }
1027
+
1028
+ fn handle_shell_pointer_down(&self) {
1029
+ if !is_enabled(&self.root) {
1030
+ return;
1031
+ }
1032
+ if !self.profile.multiline {
1033
+ if self.focused_state.get() {
1034
+ self.focus_editor();
1035
+ } else {
1036
+ self.focus_now();
1037
+ }
1038
+ return;
1039
+ }
1040
+ self.handle_viewport_pointer_down();
1041
+ }
1042
+
1043
+ fn handle_viewport_pointer_down(&self) {
1044
+ if !is_enabled(&self.root) {
1045
+ return;
1046
+ }
1047
+ if self.focused_state.get() {
1048
+ self.focus_editor();
1049
+ return;
1050
+ }
1051
+ self.focus_now();
1052
+ }
1053
+ }
1054
+
1055
+ impl HasFlexBoxRoot for TextInputCore {
1056
+ fn flex_box_root(&self) -> &FlexBox {
1057
+ &self.root
1058
+ }
1059
+ }
1060
+
1061
+ impl TextInputCore {
1062
+ pub(crate) fn build_control(&self) {
1063
+ self.root.build();
1064
+ self.sync_browser_input_metadata();
1065
+ self.editor_text.selection_range(
1066
+ self.selection_start_bytes.get(),
1067
+ self.selection_end_bytes.get(),
1068
+ );
1069
+ }
1070
+
1071
+ pub(crate) fn editor_node(&self) -> TextCore {
1072
+ self.editor_text.clone()
1073
+ }
1074
+ }