@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,492 @@
1
+ use crate::controls::LabeledControlColors;
2
+ use crate::event::{FocusChangedEventArgs, KeyEventArgs, PointerEventArgs};
3
+ use crate::ffi::{
4
+ AlignItems, CursorStyle, FlexDirection, KeyEventType, PointerEventType, SemanticRole, Unit,
5
+ };
6
+ use crate::node::{flex_box, FlexBox, Node, TextCore, WeakFlexBox};
7
+ use crate::signal::SubscriptionGuard;
8
+ use crate::theme::{current_theme, subscribe};
9
+ use crate::{focus_adorner, focus_visibility};
10
+ use std::cell::{Cell, RefCell};
11
+ use std::rc::Rc;
12
+
13
+ const TRANSPARENT: u32 = 0x00000000;
14
+
15
+ type ActivationCallback = Rc<dyn Fn(PressableLabeledControlState)>;
16
+ type StateCallback = Rc<dyn Fn(PressableLabeledControlState)>;
17
+ type KeyCallback = Rc<dyn Fn(&mut KeyEventArgs) -> bool>;
18
+
19
+ #[derive(Clone, Copy, Debug, Default)]
20
+ pub(crate) struct PressableLabeledControlState {
21
+ pub hovered: bool,
22
+ pub pressed: bool,
23
+ pub focused: bool,
24
+ pub enabled: bool,
25
+ }
26
+
27
+ #[derive(Clone)]
28
+ pub(crate) struct PressableLabeledControl {
29
+ root: FlexBox,
30
+ indicator_root: Rc<RefCell<FlexBox>>,
31
+ label_node: TextCore,
32
+ gap_node: FlexBox,
33
+ label_host: FlexBox,
34
+ hovered_state: Rc<Cell<bool>>,
35
+ pressed_state: Rc<Cell<bool>>,
36
+ focused_state: Rc<Cell<bool>>,
37
+ key_pressed_state: Rc<Cell<bool>>,
38
+ pointer_pressed_state: Rc<Cell<bool>>,
39
+ enabled_state: Rc<Cell<bool>>,
40
+ label_font_size_override: Rc<Cell<f32>>,
41
+ colors_value: Rc<Cell<Option<LabeledControlColors>>>,
42
+ activation: Rc<RefCell<Option<ActivationCallback>>>,
43
+ state_changed: Rc<RefCell<Option<StateCallback>>>,
44
+ key_handler: Rc<RefCell<Option<KeyCallback>>>,
45
+ theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
46
+ focus_visibility_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
47
+ }
48
+
49
+ impl PressableLabeledControl {
50
+ pub fn new(role: SemanticRole, label: impl Into<String>, indicator_root: FlexBox) -> Self {
51
+ let label = label.into();
52
+ let root = flex_box();
53
+ let label_node = TextCore::new(&label);
54
+ let gap_node = flex_box();
55
+ let label_host = flex_box();
56
+ label_host.child(&label_node);
57
+ root.flex_direction(FlexDirection::Row)
58
+ .align_items(AlignItems::Center)
59
+ .interactive(true)
60
+ .focusable(true, 0)
61
+ .reflect_semantic_disabled_from_enabled()
62
+ .cursor(CursorStyle::Pointer)
63
+ .semantic_role(role)
64
+ .semantic_label(label)
65
+ .child(&indicator_root)
66
+ .child(&gap_node)
67
+ .child(&label_host);
68
+
69
+ let control = Self {
70
+ root,
71
+ indicator_root: Rc::new(RefCell::new(indicator_root)),
72
+ label_node,
73
+ gap_node,
74
+ label_host,
75
+ hovered_state: Rc::new(Cell::new(false)),
76
+ pressed_state: Rc::new(Cell::new(false)),
77
+ focused_state: Rc::new(Cell::new(false)),
78
+ key_pressed_state: Rc::new(Cell::new(false)),
79
+ pointer_pressed_state: Rc::new(Cell::new(false)),
80
+ enabled_state: Rc::new(Cell::new(true)),
81
+ label_font_size_override: Rc::new(Cell::new(0.0)),
82
+ colors_value: Rc::new(Cell::new(None)),
83
+ activation: Rc::new(RefCell::new(None)),
84
+ state_changed: Rc::new(RefCell::new(None)),
85
+ key_handler: Rc::new(RefCell::new(None)),
86
+ theme_guard: Rc::new(RefCell::new(None)),
87
+ focus_visibility_guard: Rc::new(RefCell::new(None)),
88
+ };
89
+ control.install_handlers();
90
+ control.install_theme_subscription();
91
+ control.sync_base_theme();
92
+ control.sync_focus_chrome();
93
+ control
94
+ }
95
+
96
+ pub(crate) fn root(&self) -> FlexBox {
97
+ self.root.clone()
98
+ }
99
+
100
+ #[cfg(test)]
101
+ pub(crate) fn test_parts(&self) -> (FlexBox, FlexBox, FlexBox, FlexBox) {
102
+ (
103
+ self.root.clone(),
104
+ self.indicator_root.borrow().clone(),
105
+ self.gap_node.clone(),
106
+ self.label_host.clone(),
107
+ )
108
+ }
109
+
110
+ pub(crate) fn set_activation(&self, callback: impl Fn(PressableLabeledControlState) + 'static) {
111
+ *self.activation.borrow_mut() = Some(Rc::new(callback));
112
+ }
113
+
114
+ pub(crate) fn set_state_changed(
115
+ &self,
116
+ callback: impl Fn(PressableLabeledControlState) + 'static,
117
+ ) {
118
+ *self.state_changed.borrow_mut() = Some(Rc::new(callback));
119
+ }
120
+
121
+ pub(crate) fn set_key_handler(&self, callback: impl Fn(&mut KeyEventArgs) -> bool + 'static) {
122
+ *self.key_handler.borrow_mut() = Some(Rc::new(callback));
123
+ }
124
+
125
+ pub(crate) fn colors(&self, colors: Option<LabeledControlColors>) {
126
+ self.colors_value.set(colors);
127
+ self.sync_base_theme();
128
+ self.event_target().notify_state_changed();
129
+ }
130
+
131
+ pub(crate) fn set_label_font_size_override(&self, font_size: f32) {
132
+ self.label_font_size_override
133
+ .set(if font_size > 0.0 { font_size } else { 0.0 });
134
+ self.sync_base_theme();
135
+ }
136
+
137
+ pub(crate) fn replace_indicator_root(&self, next_root: FlexBox) {
138
+ if next_root
139
+ .retained_node_ref()
140
+ .ptr_eq(&self.indicator_root.borrow().retained_node_ref())
141
+ {
142
+ return;
143
+ }
144
+ let previous_root = self.indicator_root.borrow().clone();
145
+ self.root.remove_child(&previous_root);
146
+ self.root.remove_child(&self.gap_node);
147
+ self.root.remove_child(&self.label_host);
148
+ self.root
149
+ .child(&next_root)
150
+ .child(&self.gap_node)
151
+ .child(&self.label_host);
152
+ previous_root.dispose();
153
+ self.indicator_root.replace(next_root);
154
+ }
155
+
156
+ pub(crate) fn enabled(&self, enabled: bool) {
157
+ let previous = self.enabled_state.get();
158
+ self.root.enabled(enabled);
159
+ let effective = self.root.retained_node_ref().is_enabled_for_routing();
160
+ if previous == effective {
161
+ return;
162
+ }
163
+ self.enabled_state.set(effective);
164
+ if !effective {
165
+ self.hovered_state.set(false);
166
+ self.pointer_pressed_state.set(false);
167
+ self.key_pressed_state.set(false);
168
+ self.pressed_state.set(false);
169
+ }
170
+ self.sync_base_theme();
171
+ self.sync_focus_chrome();
172
+ self.event_target().notify_state_changed();
173
+ }
174
+
175
+ pub(crate) fn snapshot_state(&self) -> PressableLabeledControlState {
176
+ PressableLabeledControlState {
177
+ hovered: self.hovered_state.get(),
178
+ pressed: self.pressed_state.get(),
179
+ focused: self.focused_state.get(),
180
+ enabled: self.enabled_state.get(),
181
+ }
182
+ }
183
+
184
+ pub(crate) fn state_snapshotter(&self) -> Rc<dyn Fn() -> PressableLabeledControlState> {
185
+ let hovered_state = self.hovered_state.clone();
186
+ let pressed_state = self.pressed_state.clone();
187
+ let focused_state = self.focused_state.clone();
188
+ let enabled_state = self.enabled_state.clone();
189
+ Rc::new(move || PressableLabeledControlState {
190
+ hovered: hovered_state.get(),
191
+ pressed: pressed_state.get(),
192
+ focused: focused_state.get(),
193
+ enabled: enabled_state.get(),
194
+ })
195
+ }
196
+
197
+ fn install_handlers(&self) {
198
+ let target = self.event_target();
199
+ self.root.on_pointer_enter(move |event| {
200
+ target.handle_pointer_event(event);
201
+ });
202
+ let target = self.event_target();
203
+ self.root.on_pointer_leave(move |event| {
204
+ target.handle_pointer_event(event);
205
+ });
206
+ let target = self.event_target();
207
+ self.root.on_pointer_down(move |event| {
208
+ target.handle_pointer_event(event);
209
+ });
210
+ let target = self.event_target();
211
+ self.root.on_pointer_up(move |event| {
212
+ target.handle_pointer_event(event);
213
+ });
214
+ let target = self.event_target();
215
+ self.root.on_key_down(move |event| {
216
+ target.handle_key_event(event);
217
+ });
218
+ let target = self.event_target();
219
+ self.root.on_key_up(move |event| {
220
+ target.handle_key_event(event);
221
+ });
222
+ let target = self.event_target();
223
+ self.root.on_focus_changed(move |event| {
224
+ target.handle_focus_changed(event);
225
+ });
226
+ }
227
+
228
+ fn install_theme_subscription(&self) {
229
+ let target = self.event_target();
230
+ *self.theme_guard.borrow_mut() = Some(subscribe(move |_theme| {
231
+ target.sync_base_theme();
232
+ target.notify_state_changed();
233
+ target.sync_focus_chrome();
234
+ }));
235
+
236
+ let target = self.event_target();
237
+ *self.focus_visibility_guard.borrow_mut() =
238
+ Some(focus_visibility::subscribe(move |_visible| {
239
+ target.sync_focus_chrome();
240
+ }));
241
+ }
242
+
243
+ fn event_target(&self) -> PressableLabeledControlEventTarget {
244
+ PressableLabeledControlEventTarget {
245
+ weak_root: self.root.downgrade(),
246
+ label_node: self.label_node.clone(),
247
+ gap_node: self.gap_node.clone(),
248
+ hovered_state: self.hovered_state.clone(),
249
+ pressed_state: self.pressed_state.clone(),
250
+ focused_state: self.focused_state.clone(),
251
+ key_pressed_state: self.key_pressed_state.clone(),
252
+ pointer_pressed_state: self.pointer_pressed_state.clone(),
253
+ enabled_state: self.enabled_state.clone(),
254
+ label_font_size_override: self.label_font_size_override.clone(),
255
+ colors_value: self.colors_value.clone(),
256
+ activation: self.activation.clone(),
257
+ state_changed: self.state_changed.clone(),
258
+ key_handler: self.key_handler.clone(),
259
+ }
260
+ }
261
+
262
+ fn sync_base_theme(&self) {
263
+ sync_base_theme_parts(
264
+ &self.root,
265
+ &self.label_node,
266
+ &self.gap_node,
267
+ self.label_font_size_override.get(),
268
+ self.colors_value.get(),
269
+ self.enabled_state.get(),
270
+ );
271
+ }
272
+
273
+ fn sync_focus_chrome(&self) {
274
+ sync_focus_chrome_parts(
275
+ &self.root,
276
+ self.focused_state.get(),
277
+ self.enabled_state.get(),
278
+ );
279
+ }
280
+ }
281
+
282
+ fn sync_base_theme_parts(
283
+ root: &FlexBox,
284
+ label_node: &TextCore,
285
+ gap_node: &FlexBox,
286
+ label_font_size_override: f32,
287
+ colors: Option<LabeledControlColors>,
288
+ enabled: bool,
289
+ ) {
290
+ let theme = current_theme();
291
+ root.cursor(if enabled {
292
+ CursorStyle::Pointer
293
+ } else {
294
+ CursorStyle::Default
295
+ });
296
+ root.corner_radius(theme.spacing.sm);
297
+ root.border(2.0, TRANSPARENT);
298
+ root.padding(
299
+ theme.spacing.xs,
300
+ theme.spacing.xs,
301
+ theme.spacing.xs,
302
+ theme.spacing.xs,
303
+ );
304
+ root.opacity(if enabled { 1.0 } else { 0.6 });
305
+ gap_node.width(theme.spacing.sm, Unit::Pixel);
306
+ label_node
307
+ .font_family(theme.fonts.body_family.clone())
308
+ .font_size(if label_font_size_override > 0.0 {
309
+ label_font_size_override
310
+ } else {
311
+ theme.fonts.size_body
312
+ });
313
+ let label_color = if enabled {
314
+ colors
315
+ .filter(|colors| colors.has_text_primary())
316
+ .map(|colors| colors.text_primary_color())
317
+ .unwrap_or(theme.colors.text_primary)
318
+ } else {
319
+ colors
320
+ .filter(|colors| colors.has_text_muted())
321
+ .map(|colors| colors.text_muted_color())
322
+ .unwrap_or(theme.colors.text_muted)
323
+ };
324
+ label_node.text_color(label_color);
325
+ }
326
+
327
+ fn sync_focus_chrome_parts(root: &FlexBox, focused: bool, enabled: bool) {
328
+ if focused && enabled && focus_visibility::keyboard_focus_visible() {
329
+ focus_adorner::show_standard(root, current_theme().spacing.sm);
330
+ return;
331
+ }
332
+ focus_adorner::hide_owner(root);
333
+ }
334
+
335
+ fn is_space_key(event: &KeyEventArgs) -> bool {
336
+ event.key == " " || event.key == "Space" || event.key == "Spacebar"
337
+ }
338
+
339
+ #[derive(Clone)]
340
+ struct PressableLabeledControlEventTarget {
341
+ weak_root: WeakFlexBox,
342
+ label_node: TextCore,
343
+ gap_node: FlexBox,
344
+ hovered_state: Rc<Cell<bool>>,
345
+ pressed_state: Rc<Cell<bool>>,
346
+ focused_state: Rc<Cell<bool>>,
347
+ key_pressed_state: Rc<Cell<bool>>,
348
+ pointer_pressed_state: Rc<Cell<bool>>,
349
+ enabled_state: Rc<Cell<bool>>,
350
+ label_font_size_override: Rc<Cell<f32>>,
351
+ colors_value: Rc<Cell<Option<LabeledControlColors>>>,
352
+ activation: Rc<RefCell<Option<ActivationCallback>>>,
353
+ state_changed: Rc<RefCell<Option<StateCallback>>>,
354
+ key_handler: Rc<RefCell<Option<KeyCallback>>>,
355
+ }
356
+
357
+ impl PressableLabeledControlEventTarget {
358
+ fn snapshot_state(&self) -> PressableLabeledControlState {
359
+ PressableLabeledControlState {
360
+ hovered: self.hovered_state.get(),
361
+ pressed: self.pressed_state.get(),
362
+ focused: self.focused_state.get(),
363
+ enabled: self.enabled_state.get(),
364
+ }
365
+ }
366
+
367
+ fn sync_base_theme(&self) {
368
+ let Some(root) = self.weak_root.upgrade() else {
369
+ return;
370
+ };
371
+ sync_base_theme_parts(
372
+ &root,
373
+ &self.label_node,
374
+ &self.gap_node,
375
+ self.label_font_size_override.get(),
376
+ self.colors_value.get(),
377
+ self.enabled_state.get(),
378
+ );
379
+ }
380
+
381
+ fn sync_focus_chrome(&self) {
382
+ let Some(root) = self.weak_root.upgrade() else {
383
+ return;
384
+ };
385
+ sync_focus_chrome_parts(&root, self.focused_state.get(), self.enabled_state.get());
386
+ }
387
+
388
+ fn activate(&self) {
389
+ if let Some(callback) = self.activation.borrow().clone() {
390
+ callback(self.snapshot_state());
391
+ }
392
+ }
393
+
394
+ fn clear_pointer_state(&self) {
395
+ self.pointer_pressed_state.set(false);
396
+ self.pressed_state.set(false);
397
+ self.notify_state_changed();
398
+ self.sync_focus_chrome();
399
+ }
400
+
401
+ fn notify_state_changed(&self) {
402
+ if let Some(callback) = self.state_changed.borrow().clone() {
403
+ callback(self.snapshot_state());
404
+ }
405
+ }
406
+
407
+ fn handle_pointer_event(&self, event: &mut PointerEventArgs) {
408
+ if !self.enabled_state.get() {
409
+ self.clear_pointer_state();
410
+ return;
411
+ }
412
+ match event.event_type {
413
+ PointerEventType::Enter => {
414
+ self.hovered_state.set(true);
415
+ self.notify_state_changed();
416
+ self.sync_focus_chrome();
417
+ }
418
+ PointerEventType::Leave => {
419
+ self.hovered_state.set(false);
420
+ self.clear_pointer_state();
421
+ }
422
+ PointerEventType::Down => {
423
+ self.pointer_pressed_state.set(true);
424
+ self.pressed_state.set(true);
425
+ self.notify_state_changed();
426
+ self.sync_focus_chrome();
427
+ }
428
+ PointerEventType::Up => {
429
+ if self.pointer_pressed_state.get() {
430
+ self.pointer_pressed_state.set(false);
431
+ self.pressed_state.set(false);
432
+ self.notify_state_changed();
433
+ self.sync_focus_chrome();
434
+ self.activate();
435
+ }
436
+ }
437
+ PointerEventType::Move | PointerEventType::Cancel => {}
438
+ }
439
+ }
440
+
441
+ fn handle_key_event(&self, event: &mut KeyEventArgs) -> bool {
442
+ if event.event_type == KeyEventType::Down {
443
+ self.sync_focus_chrome();
444
+ }
445
+ if !self.enabled_state.get() {
446
+ return false;
447
+ }
448
+ if is_space_key(event) && event.modifiers == 0 {
449
+ return match event.event_type {
450
+ KeyEventType::Down => {
451
+ self.key_pressed_state.set(true);
452
+ self.pressed_state.set(true);
453
+ self.notify_state_changed();
454
+ event.handled = true;
455
+ true
456
+ }
457
+ KeyEventType::Up => {
458
+ if self.key_pressed_state.get() {
459
+ self.key_pressed_state.set(false);
460
+ self.pressed_state.set(false);
461
+ self.notify_state_changed();
462
+ self.activate();
463
+ event.handled = true;
464
+ true
465
+ } else {
466
+ false
467
+ }
468
+ }
469
+ };
470
+ }
471
+ if let Some(callback) = self.key_handler.borrow().clone() {
472
+ if callback(event) {
473
+ event.handled = true;
474
+ return true;
475
+ }
476
+ }
477
+ false
478
+ }
479
+
480
+ fn handle_focus_changed(&self, event: FocusChangedEventArgs) {
481
+ if self.focused_state.get() == event.focused {
482
+ return;
483
+ }
484
+ self.focused_state.set(event.focused);
485
+ if !event.focused {
486
+ self.key_pressed_state.set(false);
487
+ self.pressed_state.set(false);
488
+ }
489
+ self.sync_focus_chrome();
490
+ self.notify_state_changed();
491
+ }
492
+ }
@@ -0,0 +1,208 @@
1
+ use super::pressable_indicator_presenter::{
2
+ PressableIndicatorMetrics, PressableIndicatorPresenter, PressableIndicatorVisualState,
3
+ };
4
+ use crate::controls::{LabeledControlColors, LabeledControlSizing};
5
+ use crate::ffi::{AlignItems, JustifyContent, Unit};
6
+ use crate::node::{flex_box, FlexBox};
7
+ use crate::theme::Theme;
8
+ use std::rc::Rc;
9
+
10
+ #[derive(Clone, Copy, Debug, PartialEq)]
11
+ pub struct RadioIndicatorMetrics {
12
+ pub indicator_size: f32,
13
+ pub dot_size: f32,
14
+ pub border_width: f32,
15
+ }
16
+
17
+ impl RadioIndicatorMetrics {
18
+ pub const fn new(indicator_size: f32, dot_size: f32, border_width: f32) -> Self {
19
+ Self {
20
+ indicator_size,
21
+ dot_size,
22
+ border_width,
23
+ }
24
+ }
25
+ }
26
+
27
+ const DEFAULT_RADIO_METRICS: RadioIndicatorMetrics = RadioIndicatorMetrics::new(20.0, 8.0, 1.0);
28
+
29
+ fn centered_inset(outer_size: f32, inner_size: f32) -> f32 {
30
+ if outer_size > inner_size {
31
+ (outer_size - inner_size) * 0.5
32
+ } else {
33
+ 0.0
34
+ }
35
+ }
36
+
37
+ fn dot_inset(metrics: RadioIndicatorMetrics) -> f32 {
38
+ let inset = centered_inset(metrics.indicator_size, metrics.dot_size);
39
+ if inset > metrics.border_width {
40
+ inset - metrics.border_width
41
+ } else {
42
+ 0.0
43
+ }
44
+ }
45
+
46
+ pub fn resolve_radio_metrics(sizing: Option<LabeledControlSizing>) -> RadioIndicatorMetrics {
47
+ let Some(sizing) = sizing else {
48
+ return DEFAULT_RADIO_METRICS;
49
+ };
50
+ if !sizing.has_indicator_size() {
51
+ return DEFAULT_RADIO_METRICS;
52
+ }
53
+ let indicator_size = sizing.indicator_size_px();
54
+ RadioIndicatorMetrics::new(
55
+ indicator_size,
56
+ indicator_size * 0.4,
57
+ if indicator_size >= 24.0 { 2.0 } else { 1.0 },
58
+ )
59
+ }
60
+
61
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
62
+ pub struct RadioIndicatorVisualState {
63
+ pub checked: bool,
64
+ pub hovered: bool,
65
+ pub pressed: bool,
66
+ pub focused: bool,
67
+ pub enabled: bool,
68
+ }
69
+
70
+ impl RadioIndicatorVisualState {
71
+ pub fn new(checked: bool, hovered: bool, pressed: bool, focused: bool, enabled: bool) -> Self {
72
+ Self {
73
+ checked,
74
+ hovered,
75
+ pressed,
76
+ focused,
77
+ enabled,
78
+ }
79
+ }
80
+
81
+ pub fn pressable_state(&self) -> PressableIndicatorVisualState {
82
+ PressableIndicatorVisualState {
83
+ hovered: self.hovered,
84
+ pressed: self.pressed,
85
+ focused: self.focused,
86
+ enabled: self.enabled,
87
+ }
88
+ }
89
+ }
90
+
91
+ pub trait RadioIndicatorPresenter: PressableIndicatorPresenter {
92
+ fn apply(
93
+ &self,
94
+ theme: Theme,
95
+ state: RadioIndicatorVisualState,
96
+ colors: Option<LabeledControlColors>,
97
+ );
98
+ }
99
+
100
+ pub trait RadioIndicatorTemplate {
101
+ fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn RadioIndicatorPresenter>;
102
+ }
103
+
104
+ #[derive(Clone)]
105
+ pub struct DefaultRadioIndicatorPresenter {
106
+ root: FlexBox,
107
+ metrics: RadioIndicatorMetrics,
108
+ dot_node: FlexBox,
109
+ }
110
+
111
+ impl DefaultRadioIndicatorPresenter {
112
+ pub fn new(metrics: RadioIndicatorMetrics) -> Self {
113
+ let root = flex_box();
114
+ root.width(metrics.indicator_size, Unit::Pixel)
115
+ .height(metrics.indicator_size, Unit::Pixel)
116
+ .align_items(AlignItems::Center)
117
+ .justify_content(JustifyContent::Center);
118
+ let dot_node = flex_box();
119
+ dot_node
120
+ .position_type(crate::ffi::PositionType::Absolute)
121
+ .position(dot_inset(metrics), dot_inset(metrics))
122
+ .width(metrics.dot_size, Unit::Pixel)
123
+ .height(metrics.dot_size, Unit::Pixel);
124
+ root.child(&dot_node);
125
+ Self {
126
+ root,
127
+ metrics,
128
+ dot_node,
129
+ }
130
+ }
131
+ }
132
+
133
+ impl PressableIndicatorPresenter for DefaultRadioIndicatorPresenter {
134
+ fn root(&self) -> FlexBox {
135
+ self.root.clone()
136
+ }
137
+
138
+ fn metrics(&self) -> PressableIndicatorMetrics {
139
+ PressableIndicatorMetrics::new(self.metrics.indicator_size, self.metrics.indicator_size)
140
+ }
141
+ }
142
+
143
+ impl RadioIndicatorPresenter for DefaultRadioIndicatorPresenter {
144
+ fn apply(
145
+ &self,
146
+ theme: Theme,
147
+ state: RadioIndicatorVisualState,
148
+ colors: Option<LabeledControlColors>,
149
+ ) {
150
+ let metrics = self.metrics;
151
+ let accent = colors
152
+ .filter(|colors| colors.has_accent())
153
+ .map(|colors| colors.accent_color())
154
+ .unwrap_or_else(|| {
155
+ if state.pressed {
156
+ theme.colors.accent_pressed
157
+ } else if state.hovered {
158
+ theme.colors.accent_hovered
159
+ } else {
160
+ theme.colors.accent
161
+ }
162
+ });
163
+ let outer_color = if state.checked {
164
+ accent
165
+ } else {
166
+ colors
167
+ .filter(|colors| colors.has_border())
168
+ .map(|colors| colors.border_color())
169
+ .unwrap_or(theme.colors.border)
170
+ };
171
+ self.root
172
+ .corner_radius(metrics.indicator_size * 0.5)
173
+ .border(metrics.border_width, outer_color)
174
+ .bg_color(
175
+ colors
176
+ .filter(|colors| colors.has_background())
177
+ .map(|colors| colors.background_color())
178
+ .unwrap_or(theme.colors.surface),
179
+ );
180
+ self.dot_node
181
+ .corner_radius(metrics.dot_size * 0.5)
182
+ .position(dot_inset(metrics), dot_inset(metrics))
183
+ .width(metrics.dot_size, Unit::Pixel)
184
+ .height(metrics.dot_size, Unit::Pixel)
185
+ .bg_color(outer_color)
186
+ .opacity(if state.checked { 1.0 } else { 0.0 });
187
+ }
188
+ }
189
+
190
+ #[derive(Clone, Copy, Debug, Default)]
191
+ pub struct DefaultRadioIndicatorTemplate;
192
+
193
+ impl RadioIndicatorTemplate for DefaultRadioIndicatorTemplate {
194
+ fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn RadioIndicatorPresenter> {
195
+ create_default_radio_indicator_presenter(sizing)
196
+ }
197
+ }
198
+
199
+ pub const DEFAULT_RADIO_INDICATOR_TEMPLATE: DefaultRadioIndicatorTemplate =
200
+ DefaultRadioIndicatorTemplate;
201
+
202
+ pub fn create_default_radio_indicator_presenter(
203
+ sizing: Option<LabeledControlSizing>,
204
+ ) -> Rc<dyn RadioIndicatorPresenter> {
205
+ Rc::new(DefaultRadioIndicatorPresenter::new(resolve_radio_metrics(
206
+ sizing,
207
+ )))
208
+ }