@effindomv2/fui-rs 0.1.22 → 0.1.24

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 (46) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +12 -0
  3. package/package.json +1 -1
  4. package/src/controls/anti_selection_area.rs +0 -18
  5. package/src/controls/button.rs +14 -42
  6. package/src/controls/checkbox.rs +7 -0
  7. package/src/controls/combobox.rs +7 -6
  8. package/src/controls/context_menu.rs +38 -5
  9. package/src/controls/dialog.rs +41 -3
  10. package/src/controls/dropdown.rs +2 -2
  11. package/src/controls/form.rs +16 -20
  12. package/src/controls/internal/button_presenter.rs +5 -5
  13. package/src/controls/internal/checkbox_indicator_presenter.rs +1 -1
  14. package/src/controls/internal/pressable_labeled_control.rs +21 -7
  15. package/src/controls/internal/selectable_popup_list.rs +2 -2
  16. package/src/controls/internal/text_input_core.rs +38 -68
  17. package/src/controls/internal/text_input_presenter.rs +4 -4
  18. package/src/controls/mod.rs +11 -4
  19. package/src/controls/popup.rs +35 -20
  20. package/src/controls/radio_button.rs +7 -0
  21. package/src/controls/radio_group.rs +17 -0
  22. package/src/controls/selection_area.rs +0 -18
  23. package/src/controls/shared.rs +2 -17
  24. package/src/controls/switch.rs +7 -0
  25. package/src/controls/tests.rs +230 -6
  26. package/src/controls/text_area.rs +28 -142
  27. package/src/controls/text_editor_surface.rs +167 -0
  28. package/src/controls/text_input.rs +30 -144
  29. package/src/lib.rs +26 -20
  30. package/src/mobile_text_selection_toolbar.rs +2 -2
  31. package/src/node/core.rs +451 -60
  32. package/src/node/custom_drawable.rs +20 -0
  33. package/src/node/flex_box.rs +39 -47
  34. package/src/node/grid.rs +43 -128
  35. package/src/node/helpers.rs +34 -43
  36. package/src/node/image.rs +47 -56
  37. package/src/node/mod.rs +398 -73
  38. package/src/node/scroll_bar.rs +6 -0
  39. package/src/node/scroll_box.rs +8 -21
  40. package/src/node/scroll_view.rs +144 -13
  41. package/src/node/svg_node.rs +47 -66
  42. package/src/node/text_node.rs +358 -79
  43. package/src/node/virtual_list.rs +17 -0
  44. package/src/popup_presenter.rs +31 -0
  45. package/src/text.rs +59 -0
  46. package/src/text_indices.rs +48 -0
@@ -1,44 +1,20 @@
1
1
  use super::core::*;
2
2
  use super::*;
3
3
  use crate::event::{SelectionChangedEventArgs, TextChangedEventArgs};
4
+ use crate::text_indices::{byte_to_scalar, scalar_count, scalar_to_byte};
4
5
  use crate::{FontFamily, FontStack, FontStyle, FontWeight};
5
- use std::ops::Deref;
6
6
 
7
- #[derive(Clone)]
8
- pub struct TextCore {
9
- inner: TextNode,
10
- }
11
-
12
- impl TextCore {
13
- pub fn new(content: impl Into<String>) -> Self {
14
- Self {
15
- inner: TextNode::new_core(content),
16
- }
17
- }
18
- }
19
-
20
- impl Deref for TextCore {
21
- type Target = TextNode;
22
-
23
- fn deref(&self) -> &Self::Target {
24
- &self.inner
25
- }
26
- }
27
-
28
- impl Node for TextCore {
29
- fn retained_node_ref(&self) -> NodeRef {
30
- self.inner.retained_node_ref()
31
- }
32
-
33
- fn build_self(&self) {
34
- self.inner.build_self();
35
- }
36
- }
7
+ type TextChangedCallback = Rc<dyn Fn(TextChangedEventArgs)>;
8
+ type TextReplacedCallback = Rc<dyn Fn(u32, u32, String)>;
9
+ type SelectionChangedCallback = Rc<dyn Fn(SelectionChangedEventArgs)>;
37
10
 
38
11
  #[derive(Clone)]
39
12
  pub struct TextNode {
40
13
  core: Rc<RefCell<NodeCore>>,
41
14
  props: Rc<RefCell<TextProps>>,
15
+ text_changed_callback: Rc<RefCell<Option<TextChangedCallback>>>,
16
+ text_replaced_callback: Rc<RefCell<Option<TextReplacedCallback>>>,
17
+ selection_changed_callback: Rc<RefCell<Option<SelectionChangedCallback>>>,
42
18
  }
43
19
 
44
20
  impl TextNode {
@@ -46,7 +22,7 @@ impl TextNode {
46
22
  Self::new_with_defaults(content, true)
47
23
  }
48
24
 
49
- fn new_core(content: impl Into<String>) -> Self {
25
+ pub(crate) fn new_core(content: impl Into<String>) -> Self {
50
26
  Self::new_with_defaults(content, false)
51
27
  }
52
28
 
@@ -72,7 +48,11 @@ impl TextNode {
72
48
  uses_theme_selection_color: selectable_by_default,
73
49
  ..TextProps::default()
74
50
  })),
51
+ text_changed_callback: Rc::new(RefCell::new(None)),
52
+ text_replaced_callback: Rc::new(RefCell::new(None)),
53
+ selection_changed_callback: Rc::new(RefCell::new(None)),
75
54
  };
55
+ node.install_runtime_state_handlers();
76
56
  node.bind_theme_defaults();
77
57
  node
78
58
  }
@@ -143,11 +123,73 @@ impl TextNode {
143
123
  self
144
124
  }
145
125
 
126
+ pub fn fill_width_percent(&self, percent: f32) -> &Self {
127
+ self.props.borrow_mut().width = None;
128
+ {
129
+ let mut core = self.core.borrow_mut();
130
+ core.behavior.fill_width = false;
131
+ core.behavior.fill_width_percent = Some(percent);
132
+ }
133
+ if self.has_built_handle() {
134
+ ui::set_fill_width_percent(self.handle().raw(), percent);
135
+ self.notify_retained_layout_mutation();
136
+ }
137
+ self
138
+ }
139
+
140
+ pub fn fill_height_percent(&self, percent: f32) -> &Self {
141
+ self.props.borrow_mut().height = None;
142
+ {
143
+ let mut core = self.core.borrow_mut();
144
+ core.behavior.fill_height = false;
145
+ core.behavior.fill_height_percent = Some(percent);
146
+ }
147
+ if self.has_built_handle() {
148
+ ui::set_fill_height_percent(self.handle().raw(), percent);
149
+ self.notify_retained_layout_mutation();
150
+ }
151
+ self
152
+ }
153
+
154
+ pub fn min_width(&self, value: f32, unit: Unit) -> &Self {
155
+ self.props.borrow_mut().min_width = Some((value, unit));
156
+ if self.has_built_handle() {
157
+ ui::set_min_width(self.handle().raw(), value, unit as u32);
158
+ self.notify_retained_layout_mutation();
159
+ }
160
+ self
161
+ }
162
+
163
+ pub fn max_width(&self, value: f32, unit: Unit) -> &Self {
164
+ self.props.borrow_mut().max_width = Some((value, unit));
165
+ if self.has_built_handle() {
166
+ ui::set_max_width(self.handle().raw(), value, unit as u32);
167
+ self.notify_retained_layout_mutation();
168
+ }
169
+ self
170
+ }
171
+
172
+ pub fn min_height(&self, value: f32, unit: Unit) -> &Self {
173
+ self.props.borrow_mut().min_height = Some((value, unit));
174
+ if self.has_built_handle() {
175
+ ui::set_min_height(self.handle().raw(), value, unit as u32);
176
+ self.notify_retained_layout_mutation();
177
+ }
178
+ self
179
+ }
180
+
181
+ pub fn max_height(&self, value: f32, unit: Unit) -> &Self {
182
+ self.props.borrow_mut().max_height = Some((value, unit));
183
+ if self.has_built_handle() {
184
+ ui::set_max_height(self.handle().raw(), value, unit as u32);
185
+ self.notify_retained_layout_mutation();
186
+ }
187
+ self
188
+ }
189
+
146
190
  pub fn text(&self, content: impl Into<String>) -> &Self {
147
191
  let content = content.into();
148
- self.props.borrow_mut().content = content.clone();
149
- self.retained_node_ref()
150
- .set_text_content_for_routing(Some(content.clone()));
192
+ Self::sync_content_state(&self.core, &self.props, content.clone());
151
193
  if self.has_built_handle() {
152
194
  ui::set_text(self.handle().raw(), &content);
153
195
  self.notify_retained_layout_mutation();
@@ -245,11 +287,19 @@ impl TextNode {
245
287
 
246
288
  pub fn text_align(&self, align: TextAlign) -> &Self {
247
289
  self.props.borrow_mut().text_align = Some(align);
290
+ if self.has_built_handle() {
291
+ ui::set_text_align(self.handle().raw(), align as u32);
292
+ self.notify_retained_layout_mutation();
293
+ }
248
294
  self
249
295
  }
250
296
 
251
297
  pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
252
298
  self.props.borrow_mut().text_vertical_align = Some(align);
299
+ if self.has_built_handle() {
300
+ ui::set_text_vertical_align(self.handle().raw(), align as u32);
301
+ self.notify_retained_layout_mutation();
302
+ }
253
303
  self
254
304
  }
255
305
 
@@ -277,11 +327,19 @@ impl TextNode {
277
327
 
278
328
  pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
279
329
  self.props.borrow_mut().overflow = Some(overflow);
330
+ if self.has_built_handle() {
331
+ ui::set_text_overflow(self.handle().raw(), overflow as u32);
332
+ self.notify_retained_layout_mutation();
333
+ }
280
334
  self
281
335
  }
282
336
 
283
337
  pub fn text_overflow_fade(&self, horizontal: bool, vertical: bool) -> &Self {
284
338
  self.props.borrow_mut().overflow_fade = Some((horizontal, vertical));
339
+ if self.has_built_handle() {
340
+ ui::set_text_overflow_fade(self.handle().raw(), horizontal, vertical);
341
+ self.notify_retained_layout_mutation();
342
+ }
285
343
  self
286
344
  }
287
345
 
@@ -293,7 +351,16 @@ impl TextNode {
293
351
  .unwrap_or_else(|| theme::current_theme().colors.selection);
294
352
  props.selectable = Some((selectable, resolved_selection_color));
295
353
  drop(props);
296
- self.core.borrow_mut().behavior.selectable_text = selectable;
354
+ {
355
+ let mut core = self.core.borrow_mut();
356
+ core.behavior.selectable_text = selectable;
357
+ if selectable {
358
+ core.behavior.cursor = Some(CursorStyle::Text);
359
+ } else if core.behavior.cursor == Some(CursorStyle::Text) {
360
+ core.behavior.cursor = Some(CursorStyle::Default);
361
+ }
362
+ }
363
+ crate::event::handle_cursor_style_changed(self.handle());
297
364
  if self.has_built_handle() {
298
365
  ui::set_selectable(self.handle().raw(), selectable, resolved_selection_color);
299
366
  self.notify_retained_mutation();
@@ -315,7 +382,7 @@ impl TextNode {
315
382
  }
316
383
 
317
384
  pub fn editable(&self, editable: bool) -> &Self {
318
- if editable {
385
+ if editable && self.props.borrow().selectable.is_none() {
319
386
  let selection_color = self
320
387
  .props
321
388
  .borrow()
@@ -371,8 +438,17 @@ impl TextNode {
371
438
  }
372
439
 
373
440
  pub fn selection_range(&self, start: u32, end: u32) -> &Self {
441
+ let mut props = self.props.borrow_mut();
442
+ let start = start.min(scalar_count(&props.content));
443
+ let end = end.min(scalar_count(&props.content));
444
+ let start_byte = scalar_to_byte(&props.content, start);
445
+ let end_byte = scalar_to_byte(&props.content, end);
446
+ props.selection_start = start;
447
+ props.selection_end = end;
448
+ props.selection_range_bytes = Some((start_byte, end_byte));
449
+ drop(props);
374
450
  if self.has_built_handle() {
375
- ui::set_text_selection_range(self.handle().raw(), start, end);
451
+ ui::set_text_selection_range(self.handle().raw(), start_byte, end_byte);
376
452
  self.notify_retained_mutation();
377
453
  }
378
454
  self
@@ -437,7 +513,7 @@ impl TextNode {
437
513
  self
438
514
  }
439
515
 
440
- pub fn on_click(&self, handler: impl Fn(&mut PointerEventArgs) + 'static) -> &Self {
516
+ pub fn on_pointer_click(&self, handler: impl Fn(&mut PointerEventArgs) + 'static) -> &Self {
441
517
  self.core.borrow_mut().handlers.pointer_click = Some(Rc::new(handler));
442
518
  self.retained_node_ref().require_interactive();
443
519
  self
@@ -461,20 +537,12 @@ impl TextNode {
461
537
  }
462
538
 
463
539
  pub fn on_text_changed(&self, handler: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
464
- let node = self.clone();
465
- self.core.borrow_mut().handlers.text_changed = Some(Rc::new(move |event| {
466
- node.sync_text_from_runtime(event.text.clone());
467
- handler(event);
468
- }));
540
+ *self.text_changed_callback.borrow_mut() = Some(Rc::new(handler));
469
541
  self
470
542
  }
471
543
 
472
544
  pub(crate) fn on_text_replaced(&self, handler: impl Fn(u32, u32, String) + 'static) -> &Self {
473
- let node = self.clone();
474
- self.core.borrow_mut().handlers.text_replaced = Some(Rc::new(move |start, end, text| {
475
- node.apply_text_replacement(start, end, &text);
476
- handler(start, end, text);
477
- }));
545
+ *self.text_replaced_callback.borrow_mut() = Some(Rc::new(handler));
478
546
  self
479
547
  }
480
548
 
@@ -482,14 +550,38 @@ impl TextNode {
482
550
  &self,
483
551
  handler: impl Fn(SelectionChangedEventArgs) + 'static,
484
552
  ) -> &Self {
485
- self.core.borrow_mut().handlers.selection_changed = Some(Rc::new(handler));
553
+ *self.selection_changed_callback.borrow_mut() = Some(Rc::new(handler));
486
554
  self
487
555
  }
488
556
 
489
- pub fn text_value(&self) -> String {
557
+ pub fn content(&self) -> String {
490
558
  self.props.borrow().content.clone()
491
559
  }
492
560
 
561
+ pub fn uses_default_selection_behavior(&self) -> bool {
562
+ self.props.borrow().selectable.is_none()
563
+ }
564
+
565
+ pub fn is_editable_text(&self) -> bool {
566
+ self.props.borrow().editable.unwrap_or(false)
567
+ }
568
+
569
+ pub fn is_selectable_text(&self) -> bool {
570
+ self.props
571
+ .borrow()
572
+ .selectable
573
+ .map(|(selectable, _)| selectable)
574
+ .unwrap_or(false)
575
+ }
576
+
577
+ pub fn selection_start(&self) -> u32 {
578
+ self.props.borrow().selection_start
579
+ }
580
+
581
+ pub fn selection_end(&self) -> u32 {
582
+ self.props.borrow().selection_end
583
+ }
584
+
493
585
  pub fn on_pan_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
494
586
  self.core.borrow_mut().handlers.pan_gesture = Some(Rc::new(handler));
495
587
  self
@@ -512,6 +604,105 @@ impl TextNode {
512
604
  self
513
605
  }
514
606
 
607
+ fn install_runtime_state_handlers(&self) {
608
+ let weak_core = Rc::downgrade(&self.core);
609
+ let weak_props = Rc::downgrade(&self.props);
610
+ let weak_callback = Rc::downgrade(&self.text_changed_callback);
611
+ self.core.borrow_mut().handlers.text_changed = Some(Rc::new(move |event| {
612
+ let (Some(core), Some(props)) = (weak_core.upgrade(), weak_props.upgrade()) else {
613
+ return;
614
+ };
615
+ Self::sync_content_state(&core, &props, event.text.clone());
616
+ if let Some(callbacks) = weak_callback.upgrade() {
617
+ let callback = callbacks.borrow().clone();
618
+ if let Some(callback) = callback {
619
+ callback(event);
620
+ }
621
+ }
622
+ }));
623
+
624
+ let weak_core = Rc::downgrade(&self.core);
625
+ let weak_props = Rc::downgrade(&self.props);
626
+ let weak_changed_callback = Rc::downgrade(&self.text_changed_callback);
627
+ let weak_replaced_callback = Rc::downgrade(&self.text_replaced_callback);
628
+ self.core.borrow_mut().handlers.text_replaced =
629
+ Some(Rc::new(move |start, end, replacement| {
630
+ let (Some(core), Some(props)) = (weak_core.upgrade(), weak_props.upgrade()) else {
631
+ return;
632
+ };
633
+ let content = {
634
+ let current = props.borrow().content.clone();
635
+ let start_byte = scalar_to_byte(&current, byte_to_scalar(&current, start));
636
+ let end_byte =
637
+ scalar_to_byte(&current, byte_to_scalar(&current, start.max(end)));
638
+ let mut updated = current;
639
+ updated.replace_range(start_byte as usize..end_byte as usize, &replacement);
640
+ updated
641
+ };
642
+ Self::sync_content_state(&core, &props, content.clone());
643
+ if let Some(callbacks) = weak_changed_callback.upgrade() {
644
+ let callback = callbacks.borrow().clone();
645
+ if let Some(callback) = callback {
646
+ callback(TextChangedEventArgs {
647
+ text: content.clone(),
648
+ });
649
+ }
650
+ }
651
+ if let Some(callbacks) = weak_replaced_callback.upgrade() {
652
+ let callback = callbacks.borrow().clone();
653
+ if let Some(callback) = callback {
654
+ callback(start, end, replacement);
655
+ }
656
+ }
657
+ }));
658
+
659
+ let weak_props = Rc::downgrade(&self.props);
660
+ let weak_callback = Rc::downgrade(&self.selection_changed_callback);
661
+ self.core.borrow_mut().handlers.selection_changed = Some(Rc::new(move |event| {
662
+ let Some(props) = weak_props.upgrade() else {
663
+ return;
664
+ };
665
+ let (start, end) = {
666
+ let mut props = props.borrow_mut();
667
+ let start = byte_to_scalar(&props.content, event.start);
668
+ let end = byte_to_scalar(&props.content, event.end);
669
+ let start_byte = scalar_to_byte(&props.content, start);
670
+ let end_byte = scalar_to_byte(&props.content, end);
671
+ props.selection_start = start;
672
+ props.selection_end = end;
673
+ props.selection_range_bytes = Some((start_byte, end_byte));
674
+ (start, end)
675
+ };
676
+ if let Some(callbacks) = weak_callback.upgrade() {
677
+ let callback = callbacks.borrow().clone();
678
+ if let Some(callback) = callback {
679
+ callback(SelectionChangedEventArgs { start, end });
680
+ }
681
+ }
682
+ }));
683
+ }
684
+
685
+ fn sync_content_state(
686
+ core: &Rc<RefCell<NodeCore>>,
687
+ props: &Rc<RefCell<TextProps>>,
688
+ content: String,
689
+ ) {
690
+ {
691
+ let mut props = props.borrow_mut();
692
+ let length = scalar_count(&content);
693
+ props.selection_start = props.selection_start.min(length);
694
+ props.selection_end = props.selection_end.min(length);
695
+ if props.selection_range_bytes.is_some() {
696
+ props.selection_range_bytes = Some((
697
+ scalar_to_byte(&content, props.selection_start),
698
+ scalar_to_byte(&content, props.selection_end),
699
+ ));
700
+ }
701
+ props.content = content.clone();
702
+ }
703
+ core.borrow_mut().behavior.text_content = Some(content);
704
+ }
705
+
515
706
  pub(crate) fn required_font_ids(&self) -> Vec<u32> {
516
707
  let props = self.props.borrow();
517
708
  let mut font_ids = Vec::new();
@@ -612,28 +803,6 @@ impl TextNode {
612
803
  ui::set_font(self.handle().raw(), font_id, font_size);
613
804
  self.notify_retained_layout_mutation();
614
805
  }
615
-
616
- fn sync_text_from_runtime(&self, content: String) {
617
- self.props.borrow_mut().content = content.clone();
618
- self.retained_node_ref()
619
- .set_text_content_for_routing(Some(content));
620
- }
621
-
622
- fn apply_text_replacement(&self, start: u32, end: u32, replacement: &str) {
623
- let mut props = self.props.borrow_mut();
624
- let start = start.min(props.content.len() as u32) as usize;
625
- let end = end.min(props.content.len() as u32) as usize;
626
- if start <= end
627
- && props.content.is_char_boundary(start)
628
- && props.content.is_char_boundary(end)
629
- {
630
- props.content.replace_range(start..end, replacement);
631
- }
632
- let content = props.content.clone();
633
- drop(props);
634
- self.retained_node_ref()
635
- .set_text_content_for_routing(Some(content));
636
- }
637
806
  }
638
807
 
639
808
  impl Node for TextNode {
@@ -645,11 +814,10 @@ impl Node for TextNode {
645
814
  if self.props.borrow().has_font {
646
815
  self.resolve_font_id();
647
816
  }
648
- apply_text_props(
649
- self.handle(),
650
- &self.props.borrow(),
651
- self.core.borrow().behavior.clone(),
652
- );
817
+ // Host setters may synchronously report text or selection state back into
818
+ // this retained node. Never hold a RefCell borrow across that boundary.
819
+ let props = self.props.borrow().clone();
820
+ apply_text_props(self.handle(), &props, self.core.borrow().behavior.clone());
653
821
  }
654
822
 
655
823
  fn required_font_ids_for_preparation(&self) -> Vec<u32> {
@@ -665,10 +833,16 @@ impl super::ThemeBindable for TextNode {
665
833
  fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
666
834
  let weak_core = Rc::downgrade(&self.core);
667
835
  let weak_props = Rc::downgrade(&self.props);
836
+ let weak_text_changed_callback = Rc::downgrade(&self.text_changed_callback);
837
+ let weak_text_replaced_callback = Rc::downgrade(&self.text_replaced_callback);
838
+ let weak_selection_changed_callback = Rc::downgrade(&self.selection_changed_callback);
668
839
  Box::new(move || {
669
840
  Some(TextNode {
670
841
  core: weak_core.upgrade()?,
671
842
  props: weak_props.upgrade()?,
843
+ text_changed_callback: weak_text_changed_callback.upgrade()?,
844
+ text_replaced_callback: weak_text_replaced_callback.upgrade()?,
845
+ selection_changed_callback: weak_selection_changed_callback.upgrade()?,
672
846
  })
673
847
  })
674
848
  }
@@ -684,7 +858,7 @@ mod tests {
684
858
  fn prebuild_font_family_weight_and_style_are_resolved_during_build() {
685
859
  ffi::test::reset();
686
860
  let theme = theme::current_theme();
687
- let label = TextCore::new("Bold before build");
861
+ let label = TextNode::new_core("Bold before build");
688
862
  label
689
863
  .font_family(theme.fonts.body_family)
690
864
  .font_weight(FontWeight::Bold)
@@ -704,4 +878,109 @@ mod tests {
704
878
  )));
705
879
  Application::unmount();
706
880
  }
881
+
882
+ #[test]
883
+ fn text_layout_surface_replays_before_build_and_mutates_after_build() {
884
+ ffi::test::reset();
885
+ let label = TextNode::new("Layout");
886
+ label
887
+ .fill_width_percent(75.0)
888
+ .fill_height_percent(50.0)
889
+ .min_width(12.0, Unit::Pixel)
890
+ .max_width(80.0, Unit::Percent)
891
+ .min_height(14.0, Unit::Pixel)
892
+ .max_height(90.0, Unit::Pixel)
893
+ .text_align(TextAlign::Center)
894
+ .text_vertical_align(TextVerticalAlign::Bottom)
895
+ .text_overflow(TextOverflow::Ellipsis)
896
+ .text_overflow_fade(true, false);
897
+
898
+ Application::mount(label.clone());
899
+ let calls = ffi::test::take_calls();
900
+ assert!(calls.iter().any(|call| matches!(call, Call::SetFillWidthPercent { percent, .. } if (*percent - 75.0).abs() < f32::EPSILON)));
901
+ assert!(calls.iter().any(|call| matches!(call, Call::SetFillHeightPercent { percent, .. } if (*percent - 50.0).abs() < f32::EPSILON)));
902
+ assert!(calls.iter().any(|call| matches!(call, Call::SetMinWidth { value, unit_enum, .. } if (*value - 12.0).abs() < f32::EPSILON && *unit_enum == Unit::Pixel as u32)));
903
+ assert!(calls.iter().any(|call| matches!(call, Call::SetMaxWidth { value, unit_enum, .. } if (*value - 80.0).abs() < f32::EPSILON && *unit_enum == Unit::Percent as u32)));
904
+ assert!(calls.iter().any(|call| matches!(call, Call::SetMinHeight { value, .. } if (*value - 14.0).abs() < f32::EPSILON)));
905
+ assert!(calls.iter().any(|call| matches!(call, Call::SetMaxHeight { value, .. } if (*value - 90.0).abs() < f32::EPSILON)));
906
+
907
+ label
908
+ .text_align(TextAlign::Right)
909
+ .text_vertical_align(TextVerticalAlign::Center)
910
+ .text_overflow(TextOverflow::Clip)
911
+ .text_overflow_fade(false, true);
912
+ let calls = ffi::test::take_calls();
913
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextAlign { align_enum, .. } if *align_enum == TextAlign::Right as u32)));
914
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextVerticalAlign { align_enum, .. } if *align_enum == TextVerticalAlign::Center as u32)));
915
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextOverflow { overflow_enum, .. } if *overflow_enum == TextOverflow::Clip as u32)));
916
+ assert!(calls.iter().any(|call| matches!(
917
+ call,
918
+ Call::SetTextOverflowFade {
919
+ horizontal: false,
920
+ vertical: true,
921
+ ..
922
+ }
923
+ )));
924
+ Application::unmount();
925
+ }
926
+
927
+ #[test]
928
+ fn public_selection_uses_scalar_indices_at_the_utf8_boundary() {
929
+ ffi::test::reset();
930
+ let label = TextNode::new("A你😀Z");
931
+ Application::mount(label.clone());
932
+ ffi::test::take_calls();
933
+
934
+ label.selection_range(2, 3);
935
+ assert_eq!(label.selection_start(), 2);
936
+ assert_eq!(label.selection_end(), 3);
937
+ assert!(ffi::test::take_calls().iter().any(|call| matches!(
938
+ call,
939
+ Call::SetTextSelectionRange {
940
+ start: 4,
941
+ end: 8,
942
+ ..
943
+ }
944
+ )));
945
+
946
+ let observed = Rc::new(RefCell::new(None));
947
+ label.on_selection_changed({
948
+ let observed = observed.clone();
949
+ move |event| *observed.borrow_mut() = Some((event.start, event.end))
950
+ });
951
+ crate::event::__fui_on_selection_changed(label.handle().raw(), 1, 8);
952
+ assert_eq!(label.selection_start(), 1);
953
+ assert_eq!(label.selection_end(), 3);
954
+ assert_eq!(*observed.borrow(), Some((1, 3)));
955
+ Application::unmount();
956
+ }
957
+
958
+ #[test]
959
+ fn runtime_text_state_stays_synchronized_without_user_callbacks() {
960
+ ffi::test::reset();
961
+ let label = TextNode::new("A你😀Z");
962
+ Application::mount(label.clone());
963
+ let replacement = "界";
964
+ unsafe {
965
+ crate::event::__fui_on_text_replaced(
966
+ label.handle().raw(),
967
+ 1,
968
+ 4,
969
+ replacement.as_ptr(),
970
+ replacement.len() as u32,
971
+ );
972
+ }
973
+ assert_eq!(label.content(), "A界😀Z");
974
+
975
+ let changed = "你好😀";
976
+ unsafe {
977
+ crate::event::__fui_on_text_changed(
978
+ label.handle().raw(),
979
+ changed.as_ptr(),
980
+ changed.len() as u32,
981
+ );
982
+ }
983
+ assert_eq!(label.content(), changed);
984
+ Application::unmount();
985
+ }
707
986
  }
@@ -196,6 +196,15 @@ impl<T: 'static> VirtualList<T> {
196
196
  }
197
197
  }
198
198
 
199
+ pub fn is_selection_barrier(&self) -> bool {
200
+ true
201
+ }
202
+
203
+ pub fn render(&self) -> &Self {
204
+ self.rebuild_visible_range(true);
205
+ self
206
+ }
207
+
199
208
  pub fn node_id(&self, id: impl Into<String>) -> &Self {
200
209
  self.inner.scroll_box.node_id(id);
201
210
  self
@@ -559,6 +568,14 @@ impl<T: 'static> HasFlexBoxRoot for VirtualList<T> {
559
568
  fn flex_box_root(&self) -> &FlexBox {
560
569
  &self.inner.root
561
570
  }
571
+
572
+ fn set_flex_box_surface_width(&self, value: f32, unit: Unit) {
573
+ self.width(value, unit);
574
+ }
575
+
576
+ fn set_flex_box_surface_height(&self, value: f32, unit: Unit) {
577
+ self.height(value, unit);
578
+ }
562
579
  }
563
580
 
564
581
  impl<T: 'static> ThemeBindable for VirtualList<T> {
@@ -22,6 +22,15 @@ pub struct PopupPresenter {
22
22
  state: Rc<RefCell<PopupPresenterState>>,
23
23
  }
24
24
 
25
+ #[derive(Clone)]
26
+ pub(crate) struct WeakPopupPresenter {
27
+ root: WeakFlexBox,
28
+ overlay_node: FlexBox,
29
+ surface_node: FlexBox,
30
+ semantic_scope_node: Option<FlexBox>,
31
+ state: Rc<RefCell<PopupPresenterState>>,
32
+ }
33
+
25
34
  #[derive(Clone)]
26
35
  pub(crate) struct PopupPresenterEventTarget {
27
36
  root: WeakFlexBox,
@@ -107,6 +116,16 @@ impl PopupPresenter {
107
116
  self.state.borrow().open
108
117
  }
109
118
 
119
+ pub(crate) fn downgrade(&self) -> WeakPopupPresenter {
120
+ WeakPopupPresenter {
121
+ root: self.root.downgrade(),
122
+ overlay_node: self.overlay_node.clone(),
123
+ surface_node: self.surface_node.clone(),
124
+ semantic_scope_node: self.semantic_scope_node.clone(),
125
+ state: self.state.clone(),
126
+ }
127
+ }
128
+
110
129
  pub fn surface_x(&self) -> f32 {
111
130
  self.state.borrow().surface_x
112
131
  }
@@ -322,6 +341,18 @@ impl PopupPresenter {
322
341
  }
323
342
  }
324
343
 
344
+ impl WeakPopupPresenter {
345
+ pub(crate) fn upgrade(&self) -> Option<PopupPresenter> {
346
+ Some(PopupPresenter {
347
+ root: self.root.upgrade()?,
348
+ overlay_node: self.overlay_node.clone(),
349
+ surface_node: self.surface_node.clone(),
350
+ semantic_scope_node: self.semantic_scope_node.clone(),
351
+ state: self.state.clone(),
352
+ })
353
+ }
354
+ }
355
+
325
356
  impl PopupPresenterEventTarget {
326
357
  pub(crate) fn is_open(&self) -> bool {
327
358
  self.state.borrow().open