@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.
- package/Cargo.toml +1 -1
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/controls/anti_selection_area.rs +0 -18
- package/src/controls/button.rs +14 -42
- package/src/controls/checkbox.rs +7 -0
- package/src/controls/combobox.rs +7 -6
- package/src/controls/context_menu.rs +38 -5
- package/src/controls/dialog.rs +41 -3
- package/src/controls/dropdown.rs +2 -2
- package/src/controls/form.rs +16 -20
- package/src/controls/internal/button_presenter.rs +5 -5
- package/src/controls/internal/checkbox_indicator_presenter.rs +1 -1
- package/src/controls/internal/pressable_labeled_control.rs +21 -7
- package/src/controls/internal/selectable_popup_list.rs +2 -2
- package/src/controls/internal/text_input_core.rs +38 -68
- package/src/controls/internal/text_input_presenter.rs +4 -4
- package/src/controls/mod.rs +11 -4
- package/src/controls/popup.rs +35 -20
- package/src/controls/radio_button.rs +7 -0
- package/src/controls/radio_group.rs +17 -0
- package/src/controls/selection_area.rs +0 -18
- package/src/controls/shared.rs +2 -17
- package/src/controls/switch.rs +7 -0
- package/src/controls/tests.rs +230 -6
- package/src/controls/text_area.rs +28 -142
- package/src/controls/text_editor_surface.rs +167 -0
- package/src/controls/text_input.rs +30 -144
- package/src/lib.rs +26 -20
- package/src/mobile_text_selection_toolbar.rs +2 -2
- package/src/node/core.rs +451 -60
- package/src/node/custom_drawable.rs +20 -0
- package/src/node/flex_box.rs +39 -47
- package/src/node/grid.rs +43 -128
- package/src/node/helpers.rs +34 -43
- package/src/node/image.rs +47 -56
- package/src/node/mod.rs +398 -73
- package/src/node/scroll_bar.rs +6 -0
- package/src/node/scroll_box.rs +8 -21
- package/src/node/scroll_view.rs +144 -13
- package/src/node/svg_node.rs +47 -66
- package/src/node/text_node.rs +358 -79
- package/src/node/virtual_list.rs +17 -0
- package/src/popup_presenter.rs +31 -0
- package/src/text.rs +59 -0
- package/src/text_indices.rs +48 -0
package/src/node/text_node.rs
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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(),
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
553
|
+
*self.selection_changed_callback.borrow_mut() = Some(Rc::new(handler));
|
|
486
554
|
self
|
|
487
555
|
}
|
|
488
556
|
|
|
489
|
-
pub fn
|
|
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(¤t, byte_to_scalar(¤t, start));
|
|
636
|
+
let end_byte =
|
|
637
|
+
scalar_to_byte(¤t, byte_to_scalar(¤t, 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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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 =
|
|
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
|
}
|
package/src/node/virtual_list.rs
CHANGED
|
@@ -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> {
|
package/src/popup_presenter.rs
CHANGED
|
@@ -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
|