@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
@@ -12,11 +12,12 @@ use crate::ffi::{
12
12
  CursorStyle, KeyEventType, KeyModifier, PositionType, SemanticRole, TextVerticalAlign, Unit,
13
13
  };
14
14
  use crate::node::{
15
- flex_box, FlexBox, FlexBoxSurface, HasFlexBoxRoot, Node, NodeHandle, ScrollBarVisibility,
16
- ScrollBox, TextCore,
15
+ flex_box, ChildContainerSurface, FlexBox, HasFlexBoxRoot, LayoutSurface, Node, NodeHandle,
16
+ ScrollBarVisibility, ScrollBox, TextNode,
17
17
  };
18
18
  use crate::persisted::{persisted_value_adapter, PersistedStringCodec};
19
19
  use crate::signal::SubscriptionGuard;
20
+ use crate::text_indices::{scalar_count, scalar_to_byte};
20
21
  use crate::theme::{current_theme, subscribe};
21
22
  use crate::{focus_adorner, focus_visibility};
22
23
  use std::cell::{Cell, RefCell};
@@ -77,39 +78,6 @@ fn create_presenter(
77
78
  create_default_text_input_presenter()
78
79
  }
79
80
 
80
- fn char_count(text: &str) -> u32 {
81
- text.chars().count() as u32
82
- }
83
-
84
- fn char_to_byte(text: &str, index: u32) -> u32 {
85
- let target = min(index, char_count(text)) as usize;
86
- if target == 0 {
87
- return 0;
88
- }
89
- for (position, (byte, _)) in text.char_indices().enumerate() {
90
- if position == target {
91
- return byte as u32;
92
- }
93
- }
94
- text.len() as u32
95
- }
96
-
97
- fn byte_to_char(text: &str, byte_index: u32) -> u32 {
98
- let target = min(byte_index as usize, text.len());
99
- let mut count = 0;
100
- for (byte, _) in text.char_indices() {
101
- if byte >= target {
102
- break;
103
- }
104
- count += 1;
105
- }
106
- if target == text.len() {
107
- text.chars().count() as u32
108
- } else {
109
- count
110
- }
111
- }
112
-
113
81
  fn is_enabled(root: &FlexBox) -> bool {
114
82
  root.retained_node_ref().is_enabled_for_routing()
115
83
  }
@@ -118,9 +86,9 @@ pub struct TextInputCore {
118
86
  self_weak: RefCell<Weak<TextInputCore>>,
119
87
  profile: TextInputProfile,
120
88
  root: FlexBox,
121
- editor_text: TextCore,
89
+ editor_text: TextNode,
122
90
  editor_scroll_box: Option<ScrollBox>,
123
- placeholder_text: TextCore,
91
+ placeholder_text: TextNode,
124
92
  placeholder_host: FlexBox,
125
93
  placeholder_attached: Cell<bool>,
126
94
  presenter: RefCell<Rc<dyn TextInputPresenter>>,
@@ -167,14 +135,14 @@ impl TextInputCore {
167
135
  .reflect_semantic_disabled_from_enabled()
168
136
  .selection_area_barrier(true);
169
137
 
170
- let editor_text = TextCore::new("");
138
+ let editor_text = TextNode::new_core("");
171
139
  editor_text
172
140
  .semantic_role(SemanticRole::Textbox)
173
141
  .reflect_semantic_disabled_from_enabled()
174
142
  .focusable(true, 0)
175
143
  .selectable(true)
176
144
  .editable(true);
177
- let placeholder_text = TextCore::new("");
145
+ let placeholder_text = TextNode::new_core("");
178
146
  let placeholder_host = flex_box();
179
147
  placeholder_host
180
148
  .position_type(PositionType::Absolute)
@@ -333,16 +301,16 @@ impl TextInputCore {
333
301
 
334
302
  pub fn selection_range(&self, start: u32, end: u32) -> &Self {
335
303
  let text = self.text_value.borrow();
336
- let start = min(start, char_count(&text));
337
- let end = min(end, char_count(&text));
338
- let start_bytes = char_to_byte(&text, start);
339
- let end_bytes = char_to_byte(&text, end);
304
+ let start = min(start, scalar_count(&text));
305
+ let end = min(end, scalar_count(&text));
306
+ let start_bytes = scalar_to_byte(&text, start);
307
+ let end_bytes = scalar_to_byte(&text, end);
340
308
  drop(text);
341
309
  self.selection_start_chars.set(start);
342
310
  self.selection_end_chars.set(end);
343
311
  self.selection_start_bytes.set(start_bytes);
344
312
  self.selection_end_bytes.set(end_bytes);
345
- self.editor_text.selection_range(start_bytes, end_bytes);
313
+ self.editor_text.selection_range(start, end);
346
314
  self
347
315
  }
348
316
 
@@ -351,7 +319,7 @@ impl TextInputCore {
351
319
  }
352
320
 
353
321
  pub fn caret_to_end(&self) -> &Self {
354
- let end = char_count(&self.text_value.borrow());
322
+ let end = scalar_count(&self.text_value.borrow());
355
323
  self.selection_range(end, end)
356
324
  }
357
325
 
@@ -387,6 +355,11 @@ impl TextInputCore {
387
355
  self
388
356
  }
389
357
 
358
+ pub fn semantic_label(&self, label: impl Into<String>) -> &Self {
359
+ self.editor_text.semantic_label(label);
360
+ self
361
+ }
362
+
390
363
  pub fn line_height(&self, value: f32) -> &Self {
391
364
  self.editor_text.line_height(value);
392
365
  self.placeholder_text.line_height(value);
@@ -463,16 +436,15 @@ impl TextInputCore {
463
436
  self
464
437
  }
465
438
 
466
- pub fn on_focus_changed(&self, handler: impl Fn(FocusChangedEventArgs) + 'static) -> &Self {
467
- *self.focus_changed_callback.borrow_mut() = Some(Rc::new(handler));
468
- self
439
+ pub(crate) fn set_focus_changed_callback(&self, handler: Rc<dyn Fn(FocusChangedEventArgs)>) {
440
+ *self.focus_changed_callback.borrow_mut() = Some(handler);
469
441
  }
470
442
 
471
443
  pub fn focus_now(&self) -> &Self {
472
444
  self.focus_editor();
473
445
  self.editor_text.selection_range(
474
- self.selection_start_bytes.get(),
475
- self.selection_end_bytes.get(),
446
+ self.selection_start_chars.get(),
447
+ self.selection_end_chars.get(),
476
448
  );
477
449
  self
478
450
  }
@@ -762,7 +734,7 @@ impl TextInputCore {
762
734
  }
763
735
 
764
736
  fn handle_editor_text_changed(&self) {
765
- let value = self.editor_text.text_value();
737
+ let value = self.editor_text.content();
766
738
  if *self.text_value.borrow() == value {
767
739
  return;
768
740
  }
@@ -779,13 +751,11 @@ impl TextInputCore {
779
751
  }
780
752
 
781
753
  fn handle_editor_selection_changed(&self, start: u32, end: u32) {
782
- self.selection_start_bytes.set(start);
783
- self.selection_end_bytes.set(end);
784
754
  let text = self.text_value.borrow();
785
- let start_chars = byte_to_char(&text, start);
786
- let end_chars = byte_to_char(&text, end);
787
- let start_bytes = char_to_byte(&text, start_chars);
788
- let end_bytes = char_to_byte(&text, end_chars);
755
+ let start_chars = min(start, scalar_count(&text));
756
+ let end_chars = min(end, scalar_count(&text));
757
+ let start_bytes = scalar_to_byte(&text, start_chars);
758
+ let end_bytes = scalar_to_byte(&text, end_chars);
789
759
  drop(text);
790
760
  self.selection_start_bytes.set(start_bytes);
791
761
  self.selection_end_bytes.set(end_bytes);
@@ -845,15 +815,15 @@ impl TextInputCore {
845
815
  .selection_start_chars
846
816
  .get()
847
817
  .max(self.selection_end_chars.get());
848
- let current_len = char_count(&text);
849
- let inserted_len = char_count(inserted);
818
+ let current_len = scalar_count(&text);
819
+ let inserted_len = scalar_count(inserted);
850
820
  let replaced_len = end - start;
851
821
  if current_len - replaced_len + inserted_len > self.max_chars_value.get() as u32 {
852
822
  return;
853
823
  }
854
824
 
855
- let start_byte = char_to_byte(&text, start) as usize;
856
- let end_byte = char_to_byte(&text, end) as usize;
825
+ let start_byte = scalar_to_byte(&text, start) as usize;
826
+ let end_byte = scalar_to_byte(&text, end) as usize;
857
827
  let mut value =
858
828
  String::with_capacity(text.len() - (end_byte - start_byte) + inserted.len());
859
829
  value.push_str(&text[..start_byte]);
@@ -864,7 +834,7 @@ impl TextInputCore {
864
834
  *self.text_value.borrow_mut() = value.clone();
865
835
  let handle = self.editor_text.handle();
866
836
  if handle != NodeHandle::INVALID {
867
- let caret_byte = char_to_byte(&value, caret);
837
+ let caret_byte = scalar_to_byte(&value, caret);
868
838
  ui::replace_text_range(
869
839
  handle.raw(),
870
840
  start_byte as u32,
@@ -997,11 +967,11 @@ impl TextInputCore {
997
967
 
998
968
  fn clamp_selection_to_text(&self) {
999
969
  let text = self.text_value.borrow();
1000
- let char_len = char_count(&text);
970
+ let char_len = scalar_count(&text);
1001
971
  let start_chars = min(self.selection_start_chars.get(), char_len);
1002
972
  let end_chars = min(self.selection_end_chars.get(), char_len);
1003
- let start_bytes = char_to_byte(&text, start_chars);
1004
- let end_bytes = char_to_byte(&text, end_chars);
973
+ let start_bytes = scalar_to_byte(&text, start_chars);
974
+ let end_bytes = scalar_to_byte(&text, end_chars);
1005
975
  drop(text);
1006
976
  self.selection_start_chars.set(start_chars);
1007
977
  self.selection_end_chars.set(end_chars);
@@ -1079,12 +1049,12 @@ impl TextInputCore {
1079
1049
  self.root.build();
1080
1050
  self.sync_browser_input_metadata();
1081
1051
  self.editor_text.selection_range(
1082
- self.selection_start_bytes.get(),
1083
- self.selection_end_bytes.get(),
1052
+ self.selection_start_chars.get(),
1053
+ self.selection_end_chars.get(),
1084
1054
  );
1085
1055
  }
1086
1056
 
1087
- pub(crate) fn editor_node(&self) -> TextCore {
1057
+ pub(crate) fn editor_node(&self) -> TextNode {
1088
1058
  self.editor_text.clone()
1089
1059
  }
1090
1060
  }
@@ -1,6 +1,6 @@
1
1
  use crate::controls::TextInputColors;
2
2
  use crate::ffi::{CursorStyle, Unit};
3
- use crate::node::{Border, Corners, EdgeInsets, FlexBox, PresenterHostStyle, TextCore};
3
+ use crate::node::{Border, Corners, EdgeInsets, FlexBox, PresenterHostStyle, TextNode};
4
4
  use crate::theme::Theme;
5
5
  use std::cell::RefCell;
6
6
  use std::rc::Rc;
@@ -13,7 +13,7 @@ pub struct TextInputVisualState {
13
13
  }
14
14
 
15
15
  pub trait TextInputPresenter {
16
- fn bind(&self, editor_host: TextCore, placeholder_host: FlexBox);
16
+ fn bind(&self, editor_host: TextNode, placeholder_host: FlexBox);
17
17
  fn present(
18
18
  &self,
19
19
  theme: Theme,
@@ -28,7 +28,7 @@ pub trait TextInputTemplate {
28
28
 
29
29
  #[derive(Clone, Default)]
30
30
  pub struct DefaultTextInputPresenter {
31
- editor_host: RefCell<Option<TextCore>>,
31
+ editor_host: RefCell<Option<TextNode>>,
32
32
  placeholder_host: RefCell<Option<FlexBox>>,
33
33
  }
34
34
 
@@ -39,7 +39,7 @@ impl DefaultTextInputPresenter {
39
39
  }
40
40
 
41
41
  impl TextInputPresenter for DefaultTextInputPresenter {
42
- fn bind(&self, editor_host: TextCore, placeholder_host: FlexBox) {
42
+ fn bind(&self, editor_host: TextNode, placeholder_host: FlexBox) {
43
43
  *self.editor_host.borrow_mut() = Some(editor_host);
44
44
  *self.placeholder_host.borrow_mut() = Some(placeholder_host);
45
45
  }
@@ -5,8 +5,7 @@ use crate::ffi::{
5
5
  SemanticCheckedState, SemanticRole, Unit,
6
6
  };
7
7
  use crate::node::{
8
- flex_box, row, Child, FlexBox, HasFlexBoxRoot, Node, NodeRef, TextNode, ThemeBindable,
9
- WeakNodeRef,
8
+ flex_box, row, FlexBox, HasFlexBoxRoot, Node, NodeRef, TextNode, ThemeBindable, WeakNodeRef,
10
9
  };
11
10
  use crate::theme::{current_theme, subscribe};
12
11
  use std::cell::{Cell, RefCell};
@@ -19,8 +18,14 @@ use radio_group::WeakRadioGroupEventTarget;
19
18
  pub(crate) use shared::*;
20
19
 
21
20
  #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22
- pub struct ClickEventArgs {
23
- pub click_count: i32,
21
+ pub struct ClickEventArgs;
22
+
23
+ /// Semantic click activation for controls whose primary action can be invoked
24
+ /// by pointer, keyboard, or another supported activation source.
25
+ ///
26
+ /// Use [`Node::on_pointer_click`] when raw routed pointer data is required.
27
+ pub trait Clickable: Sized {
28
+ fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self;
24
29
  }
25
30
 
26
31
  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -129,6 +134,7 @@ pub mod templating;
129
134
  #[cfg(test)]
130
135
  mod tests;
131
136
  pub mod text_area;
137
+ mod text_editor_surface;
132
138
  pub mod text_input;
133
139
 
134
140
  pub use anti_selection_area::AntiSelectionArea;
@@ -185,6 +191,7 @@ pub use templating::{
185
191
  DEFAULT_SLIDER_TEMPLATE, DEFAULT_SWITCH_INDICATOR_TEMPLATE, DEFAULT_TEXT_INPUT_TEMPLATE,
186
192
  };
187
193
  pub use text_area::TextArea;
194
+ pub use text_editor_surface::TextEditorSurface;
188
195
  pub use text_input::TextInput;
189
196
 
190
197
  pub fn button(label: impl Into<String>) -> Button {
@@ -1,6 +1,6 @@
1
1
  use super::*;
2
2
  use crate::ffi::{FlexDirection, PositionType, Unit};
3
- use crate::node::{portal, Child};
3
+ use crate::node::portal;
4
4
  use crate::popup_presenter::{PopupPlacement, PopupPresenter};
5
5
  use std::cell::Cell;
6
6
 
@@ -37,7 +37,7 @@ impl Popup {
37
37
  presenter
38
38
  .overlay_node()
39
39
  .interactive(true)
40
- .on_click(move |_event| {
40
+ .on_pointer_click(move |_event| {
41
41
  if dismiss_flag.get() {
42
42
  presenter_target.hide();
43
43
  }
@@ -55,6 +55,8 @@ impl Popup {
55
55
  self.presenter.is_open()
56
56
  }
57
57
 
58
+ /// Returns the presented content panel. Inherited `child`/`children` calls
59
+ /// are routed here rather than into the portal's overlay root.
58
60
  pub fn surface(&self) -> FlexBox {
59
61
  self.surface_node.clone()
60
62
  }
@@ -91,24 +93,6 @@ impl Popup {
91
93
  self
92
94
  }
93
95
 
94
- pub fn child<T: Node>(&self, node: &T) -> &Self {
95
- self.surface_node.child(node);
96
- self
97
- }
98
-
99
- pub fn children<I, C>(&self, nodes: I) -> &Self
100
- where
101
- I: IntoIterator<Item = C>,
102
- C: Into<Child>,
103
- {
104
- for node in nodes {
105
- self.surface_node
106
- .retained_node_ref()
107
- .append_child_ref(&node.into().node_ref);
108
- }
109
- self
110
- }
111
-
112
96
  fn sync_appearance(&self) {
113
97
  let appearance = self.appearance_value.borrow().clone().unwrap_or_default();
114
98
  let panel = appearance.panel.unwrap_or_default();
@@ -172,4 +156,35 @@ impl crate::node::HasFlexBoxRoot for Popup {
172
156
  fn flex_box_root(&self) -> &FlexBox {
173
157
  &self.root
174
158
  }
159
+
160
+ fn append_flex_box_surface_child(&self, child: NodeRef) {
161
+ self.surface_node
162
+ .retained_node_ref()
163
+ .append_child_ref(&child);
164
+ }
165
+ }
166
+
167
+ impl crate::node::ThemeBindable for Popup {
168
+ fn theme_binding_node(&self) -> NodeRef {
169
+ self.root.retained_node_ref()
170
+ }
171
+
172
+ fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
173
+ let root = self.root.downgrade();
174
+ let surface_node = self.surface_node.downgrade();
175
+ let presenter = self.presenter.downgrade();
176
+ let dismiss_on_backdrop_click = Rc::downgrade(&self.dismiss_on_backdrop_click);
177
+ // Appearance state does not own the portal root. Keep it alive with the
178
+ // retained theme binding so a dropped wrapper remains reconstructible.
179
+ let appearance_value = self.appearance_value.clone();
180
+ Box::new(move || {
181
+ Some(Self {
182
+ root: root.upgrade()?,
183
+ surface_node: surface_node.upgrade()?,
184
+ presenter: presenter.upgrade()?,
185
+ dismiss_on_backdrop_click: dismiss_on_backdrop_click.upgrade()?,
186
+ appearance_value: appearance_value.clone(),
187
+ })
188
+ })
189
+ }
175
190
  }
@@ -221,6 +221,13 @@ impl RadioButton {
221
221
  }
222
222
  }
223
223
 
224
+ impl Clickable for RadioButton {
225
+ fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self {
226
+ self.base.on_click(handler);
227
+ self
228
+ }
229
+ }
230
+
224
231
  impl LabeledControlTextStyle for RadioButton {
225
232
  fn set_label_font_family(&self, family: crate::FontFamily) {
226
233
  self.base.font_family(family);
@@ -286,4 +286,21 @@ impl crate::node::HasFlexBoxRoot for RadioGroup {
286
286
  }
287
287
  }
288
288
 
289
+ impl crate::node::ThemeBindable for RadioGroup {
290
+ fn theme_binding_node(&self) -> NodeRef {
291
+ self.root.retained_node_ref()
292
+ }
293
+
294
+ fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
295
+ let root = self.root.downgrade();
296
+ let event_target = Rc::downgrade(&self.event_target);
297
+ Box::new(move || {
298
+ Some(Self {
299
+ root: root.upgrade()?,
300
+ event_target: event_target.upgrade()?,
301
+ })
302
+ })
303
+ }
304
+ }
305
+
289
306
  pub(crate) type WeakRadioGroupEventTarget = Weak<RadioGroupEventTarget>;
@@ -27,24 +27,6 @@ impl SelectionArea {
27
27
  }
28
28
  }
29
29
 
30
- pub fn child<T: Node>(&self, node: &T) -> &Self {
31
- self.root.child(node);
32
- self
33
- }
34
-
35
- pub fn children<I, C>(&self, nodes: I) -> &Self
36
- where
37
- I: IntoIterator<Item = C>,
38
- C: Into<Child>,
39
- {
40
- for node in nodes {
41
- self.root
42
- .retained_node_ref()
43
- .append_child_ref(&node.into().node_ref);
44
- }
45
- self
46
- }
47
-
48
30
  pub fn selected_text(&self) -> String {
49
31
  self.selected_text_signal.borrow().get()
50
32
  }
@@ -20,24 +20,9 @@ pub(crate) fn is_activation_key(event: &KeyEventArgs) -> bool {
20
20
  event.key == "Enter" || event.key == " " || event.key == "Space" || event.key == "Spacebar"
21
21
  }
22
22
 
23
- pub(crate) fn fire_click_callbacks(
24
- click: &Rc<RefCell<Option<ClickCallback>>>,
25
- double_click: &Rc<RefCell<Option<ClickCallback>>>,
26
- triple_click: &Rc<RefCell<Option<ClickCallback>>>,
27
- click_count: i32,
28
- ) {
29
- let args = ClickEventArgs { click_count };
23
+ pub(crate) fn fire_click_callback(click: &Rc<RefCell<Option<ClickCallback>>>) {
30
24
  if let Some(callback) = click.borrow().clone() {
31
- callback(args);
32
- }
33
- if click_count == 3 {
34
- if let Some(callback) = triple_click.borrow().clone() {
35
- callback(args);
36
- }
37
- } else if click_count == 2 {
38
- if let Some(callback) = double_click.borrow().clone() {
39
- callback(args);
40
- }
25
+ callback(ClickEventArgs);
41
26
  }
42
27
  }
43
28
 
@@ -212,6 +212,13 @@ impl Switch {
212
212
  }
213
213
  }
214
214
 
215
+ impl Clickable for Switch {
216
+ fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self {
217
+ self.base.on_click(handler);
218
+ self
219
+ }
220
+ }
221
+
215
222
  impl LabeledControlTextStyle for Switch {
216
223
  fn set_label_font_family(&self, family: crate::FontFamily) {
217
224
  self.base.font_family(family);