@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,845 @@
1
+ use super::*;
2
+ use crate::bindings::ui;
3
+ use crate::event::PointerEventArgs;
4
+ use crate::ffi::{CursorStyle, Orientation, Unit};
5
+ use crate::platform;
6
+ use crate::signal::Subscription;
7
+ use crate::theme::{current_theme, subscribe, Theme};
8
+ use std::cell::{Cell, RefCell};
9
+ use std::rc::{Rc, Weak};
10
+
11
+ const DEFAULT_TRACK_THICKNESS: f32 = 8.0;
12
+ const DEFAULT_THUMB_THICKNESS: f32 = 8.0;
13
+ const DEFAULT_MIN_THUMB_SIZE: f32 = 18.0;
14
+
15
+ fn clamp(value: f32, min: f32, max: f32) -> f32 {
16
+ if value < min {
17
+ return min;
18
+ }
19
+ if value > max {
20
+ return max;
21
+ }
22
+ value
23
+ }
24
+
25
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
26
+ pub enum ScrollBarVisibility {
27
+ Always,
28
+ Auto,
29
+ Never,
30
+ }
31
+
32
+ #[derive(Clone, Copy, Default)]
33
+ struct ScrollMetrics {
34
+ viewport_size: f32,
35
+ max_offset: f32,
36
+ thumb_size: f32,
37
+ max_thumb_offset: f32,
38
+ thumb_offset: f32,
39
+ trailing_spacer_size: f32,
40
+ }
41
+
42
+ struct ScrollBarInner {
43
+ scroll_state: ScrollState,
44
+ orientation: Orientation,
45
+ target_handle: Cell<u64>,
46
+ drag_start_axis: Cell<f32>,
47
+ drag_start_offset: Cell<f32>,
48
+ dragging: Cell<bool>,
49
+ chrome_visible: Cell<bool>,
50
+ track_thickness: Cell<f32>,
51
+ thumb_thickness: Cell<f32>,
52
+ min_thumb_size: Cell<f32>,
53
+ track_corner_radius: Cell<f32>,
54
+ thumb_corner_radius: Cell<f32>,
55
+ track_color: Cell<u32>,
56
+ thumb_color: Cell<u32>,
57
+ track_color_overridden: Cell<bool>,
58
+ thumb_color_overridden: Cell<bool>,
59
+ track_node: FlexBox,
60
+ track_strip: FlexBox,
61
+ leading_spacer_node: FlexBox,
62
+ thumb_node: FlexBox,
63
+ trailing_spacer_node: FlexBox,
64
+ subscriptions: RefCell<Vec<Subscription>>,
65
+ chrome_metric_changed: RefCell<Option<Rc<dyn Fn()>>>,
66
+ }
67
+
68
+ #[derive(Clone)]
69
+ pub struct ScrollBar {
70
+ inner: Rc<ScrollBarInner>,
71
+ }
72
+
73
+ impl ScrollBar {
74
+ pub fn new(scroll_state: ScrollState, orientation: Orientation) -> Self {
75
+ let orientation = if orientation == Orientation::Horizontal {
76
+ Orientation::Horizontal
77
+ } else {
78
+ Orientation::Vertical
79
+ };
80
+ let track_node = flex_box();
81
+ let track_strip = if orientation == Orientation::Horizontal {
82
+ row()
83
+ } else {
84
+ column()
85
+ };
86
+ let leading_spacer_node = flex_box();
87
+ let thumb_node = flex_box();
88
+ let trailing_spacer_node = flex_box();
89
+ track_strip
90
+ .align_items(crate::ffi::AlignItems::Center)
91
+ .child(&leading_spacer_node)
92
+ .child(&thumb_node)
93
+ .child(&trailing_spacer_node);
94
+ let interactive = !platform::is_coarse_pointer();
95
+ track_node.clip_to_bounds(true).child(&track_strip);
96
+ if interactive {
97
+ track_node.on_pointer_down(|_event: &mut PointerEventArgs| {});
98
+ thumb_node
99
+ .on_pointer_down(|_event: &mut PointerEventArgs| {})
100
+ .on_pointer_enter(|_event: &mut PointerEventArgs| {})
101
+ .on_pointer_leave(|_event: &mut PointerEventArgs| {})
102
+ .on_pointer_move(|_event: &mut PointerEventArgs| {})
103
+ .on_pointer_up(|_event: &mut PointerEventArgs| {});
104
+ }
105
+
106
+ let theme = current_theme();
107
+ let inner = Rc::new(ScrollBarInner {
108
+ scroll_state,
109
+ orientation,
110
+ target_handle: Cell::new(HandleValue::Invalid as u64),
111
+ drag_start_axis: Cell::new(0.0),
112
+ drag_start_offset: Cell::new(0.0),
113
+ dragging: Cell::new(false),
114
+ chrome_visible: Cell::new(true),
115
+ track_thickness: Cell::new(DEFAULT_TRACK_THICKNESS),
116
+ thumb_thickness: Cell::new(DEFAULT_THUMB_THICKNESS),
117
+ min_thumb_size: Cell::new(DEFAULT_MIN_THUMB_SIZE),
118
+ track_corner_radius: Cell::new(0.0),
119
+ thumb_corner_radius: Cell::new(0.0),
120
+ track_color: Cell::new(theme.colors.scrollbar_track),
121
+ thumb_color: Cell::new(theme.colors.scrollbar_thumb),
122
+ track_color_overridden: Cell::new(false),
123
+ thumb_color_overridden: Cell::new(false),
124
+ track_node,
125
+ track_strip,
126
+ leading_spacer_node,
127
+ thumb_node,
128
+ trailing_spacer_node,
129
+ subscriptions: RefCell::new(Vec::new()),
130
+ chrome_metric_changed: RefCell::new(None),
131
+ });
132
+ let bar = Self { inner };
133
+ if interactive {
134
+ bar.attach_handlers();
135
+ }
136
+ bar.attach_listeners();
137
+ bar.apply_geometry_style();
138
+ bar.apply_color_style();
139
+ bar.sync_visual_state();
140
+ bar.apply_thumb_cursor();
141
+ bar
142
+ }
143
+
144
+ pub fn thickness(&self) -> f32 {
145
+ self.inner.track_thickness.get()
146
+ }
147
+
148
+ pub fn render(&self) -> FlexBox {
149
+ self.inner.track_node.clone()
150
+ }
151
+
152
+ pub fn track_width(&self, value: f32) -> &Self {
153
+ self.set_track_thickness(value);
154
+ self
155
+ }
156
+
157
+ pub fn track_thickness(&self, value: f32) -> &Self {
158
+ self.set_track_thickness(value);
159
+ self
160
+ }
161
+
162
+ pub fn thumb_width(&self, value: f32) -> &Self {
163
+ self.set_thumb_thickness(value);
164
+ self
165
+ }
166
+
167
+ pub fn thumb_thickness(&self, value: f32) -> &Self {
168
+ self.set_thumb_thickness(value);
169
+ self
170
+ }
171
+
172
+ pub fn thumb_min_height(&self, value: f32) -> &Self {
173
+ self.set_thumb_min_size(value);
174
+ self
175
+ }
176
+
177
+ fn set_track_thickness(&self, value: f32) {
178
+ let next = if value > 0.0 { value } else { 1.0 };
179
+ if value <= 0.0 {
180
+ crate::logger::warn(
181
+ "Layout",
182
+ &format!("ScrollBar.trackThickness() received {value}; clamping to 1.0."),
183
+ );
184
+ }
185
+ if self.inner.track_thickness.get() == next {
186
+ return;
187
+ }
188
+ self.inner.track_thickness.set(next);
189
+ self.apply_geometry_style();
190
+ self.notify_chrome_metric_changed();
191
+ }
192
+
193
+ fn set_thumb_thickness(&self, value: f32) {
194
+ let next = if value > 0.0 { value } else { 1.0 };
195
+ if value <= 0.0 {
196
+ crate::logger::warn(
197
+ "Layout",
198
+ &format!("ScrollBar.thumbThickness() received {value}; clamping to 1.0."),
199
+ );
200
+ }
201
+ self.inner.thumb_thickness.set(next);
202
+ self.apply_geometry_style();
203
+ }
204
+
205
+ fn set_thumb_min_size(&self, value: f32) {
206
+ let next = if value > 0.0 { value } else { 1.0 };
207
+ if value <= 0.0 {
208
+ crate::logger::warn(
209
+ "Layout",
210
+ &format!("ScrollBar.thumbMinHeight() received {value}; clamping to 1.0."),
211
+ );
212
+ }
213
+ self.inner.min_thumb_size.set(next);
214
+ self.sync_visual_state();
215
+ }
216
+
217
+ pub fn track_corner_radius(&self, radius: f32) -> &Self {
218
+ self.inner.track_corner_radius.set(radius.max(0.0));
219
+ self.inner
220
+ .track_node
221
+ .corner_radius(self.inner.track_corner_radius.get());
222
+ self
223
+ }
224
+
225
+ pub fn thumb_corner_radius(&self, radius: f32) -> &Self {
226
+ self.inner.thumb_corner_radius.set(radius.max(0.0));
227
+ self.inner
228
+ .thumb_node
229
+ .corner_radius(self.inner.thumb_corner_radius.get());
230
+ self
231
+ }
232
+
233
+ pub fn track_color(&self, color: u32) -> &Self {
234
+ self.inner.track_color_overridden.set(true);
235
+ self.inner.track_color.set(color);
236
+ self.apply_color_style();
237
+ self
238
+ }
239
+
240
+ pub fn thumb_color(&self, color: u32) -> &Self {
241
+ self.inner.thumb_color_overridden.set(true);
242
+ self.inner.thumb_color.set(color);
243
+ self.apply_color_style();
244
+ self
245
+ }
246
+
247
+ pub fn bind_scroll_handle(&self, handle: u64) {
248
+ self.inner.target_handle.set(handle);
249
+ self.inner
250
+ .track_node
251
+ .bind_scroll_proxy_target_handle(handle);
252
+ self.inner
253
+ .track_strip
254
+ .bind_scroll_proxy_target_handle(handle);
255
+ self.inner
256
+ .leading_spacer_node
257
+ .bind_scroll_proxy_target_handle(handle);
258
+ self.inner
259
+ .thumb_node
260
+ .bind_scroll_proxy_target_handle(handle);
261
+ self.inner
262
+ .trailing_spacer_node
263
+ .bind_scroll_proxy_target_handle(handle);
264
+ }
265
+
266
+ pub fn clear_scroll_handle(&self, handle: u64) {
267
+ if self.inner.target_handle.get() == handle {
268
+ self.bind_scroll_handle(HandleValue::Invalid as u64);
269
+ }
270
+ }
271
+
272
+ pub fn chrome_visible(&self, visible: bool) {
273
+ if self.inner.chrome_visible.get() == visible {
274
+ return;
275
+ }
276
+ self.inner.chrome_visible.set(visible);
277
+ if !visible {
278
+ self.end_drag();
279
+ }
280
+ self.apply_geometry_style();
281
+ self.sync_visual_state();
282
+ self.apply_thumb_cursor();
283
+ }
284
+
285
+ pub fn refresh_now(&self) {
286
+ self.sync_visual_state();
287
+ }
288
+
289
+ pub fn can_start_thumb_drag(&self) -> bool {
290
+ let metrics = self.compute_metrics();
291
+ self.inner.chrome_visible.get()
292
+ && metrics.max_offset > 0.0
293
+ && metrics.max_thumb_offset > 0.0
294
+ }
295
+
296
+ pub(crate) fn set_chrome_metric_changed_handler(&self, handler: impl Fn() + 'static) {
297
+ *self.inner.chrome_metric_changed.borrow_mut() = Some(Rc::new(handler));
298
+ }
299
+
300
+ fn attach_handlers(&self) {
301
+ let weak = Rc::downgrade(&self.inner);
302
+ self.inner.track_node.on_pointer_down(move |event| {
303
+ if let Some(inner) = Weak::upgrade(&weak) {
304
+ ScrollBar { inner }.handle_track_pointer_down(event.scene_x, event.scene_y);
305
+ }
306
+ });
307
+
308
+ let weak = Rc::downgrade(&self.inner);
309
+ self.inner.thumb_node.on_pointer_down(move |event| {
310
+ if let Some(inner) = Weak::upgrade(&weak) {
311
+ let bar = ScrollBar { inner };
312
+ if bar.can_start_thumb_drag() {
313
+ bar.inner.dragging.set(true);
314
+ bar.inner
315
+ .drag_start_axis
316
+ .set(bar.axis_position(event.scene_x, event.scene_y));
317
+ bar.inner.drag_start_offset.set(bar.axis_offset());
318
+ event.capture_pointer();
319
+ event.handled = true;
320
+ bar.apply_thumb_cursor();
321
+ }
322
+ }
323
+ });
324
+
325
+ let weak = Rc::downgrade(&self.inner);
326
+ self.inner.thumb_node.on_pointer_move(move |event| {
327
+ if let Some(inner) = Weak::upgrade(&weak) {
328
+ let bar = ScrollBar { inner };
329
+ if !bar.inner.dragging.get() {
330
+ return;
331
+ }
332
+ let metrics = bar.compute_metrics();
333
+ if metrics.max_offset <= 0.0 || metrics.max_thumb_offset <= 0.0 {
334
+ return;
335
+ }
336
+ let delta = bar.axis_position(event.scene_x, event.scene_y)
337
+ - bar.inner.drag_start_axis.get();
338
+ let offset_per_thumb_pixel = metrics.max_offset / metrics.max_thumb_offset;
339
+ bar.set_scroll_offset(
340
+ bar.inner.drag_start_offset.get() + (delta * offset_per_thumb_pixel),
341
+ metrics.max_offset,
342
+ );
343
+ event.handled = true;
344
+ }
345
+ });
346
+
347
+ let weak = Rc::downgrade(&self.inner);
348
+ self.inner.thumb_node.on_pointer_up(move |event| {
349
+ if let Some(inner) = Weak::upgrade(&weak) {
350
+ let bar = ScrollBar { inner };
351
+ if bar.inner.dragging.get() {
352
+ bar.end_drag();
353
+ event.handled = true;
354
+ }
355
+ }
356
+ });
357
+ }
358
+
359
+ fn attach_listeners(&self) {
360
+ let weak = Rc::downgrade(&self.inner);
361
+ self.inner
362
+ .subscriptions
363
+ .borrow_mut()
364
+ .push(self.inner.scroll_state.subscribe_offset_x(move || {
365
+ if let Some(inner) = Weak::upgrade(&weak) {
366
+ ScrollBar { inner }.sync_visual_state();
367
+ }
368
+ }));
369
+ let weak = Rc::downgrade(&self.inner);
370
+ self.inner
371
+ .subscriptions
372
+ .borrow_mut()
373
+ .push(self.inner.scroll_state.subscribe_offset_y(move || {
374
+ if let Some(inner) = Weak::upgrade(&weak) {
375
+ ScrollBar { inner }.sync_visual_state();
376
+ }
377
+ }));
378
+ let weak = Rc::downgrade(&self.inner);
379
+ self.inner.subscriptions.borrow_mut().push(
380
+ self.inner.scroll_state.subscribe_content_width(move || {
381
+ if let Some(inner) = Weak::upgrade(&weak) {
382
+ ScrollBar { inner }.sync_visual_state();
383
+ }
384
+ }),
385
+ );
386
+ let weak = Rc::downgrade(&self.inner);
387
+ self.inner.subscriptions.borrow_mut().push(
388
+ self.inner.scroll_state.subscribe_content_height(move || {
389
+ if let Some(inner) = Weak::upgrade(&weak) {
390
+ ScrollBar { inner }.sync_visual_state();
391
+ }
392
+ }),
393
+ );
394
+ let weak = Rc::downgrade(&self.inner);
395
+ self.inner.subscriptions.borrow_mut().push(
396
+ self.inner.scroll_state.subscribe_viewport_width(move || {
397
+ if let Some(inner) = Weak::upgrade(&weak) {
398
+ ScrollBar { inner }.sync_visual_state();
399
+ }
400
+ }),
401
+ );
402
+ let weak = Rc::downgrade(&self.inner);
403
+ self.inner.subscriptions.borrow_mut().push(
404
+ self.inner.scroll_state.subscribe_viewport_height(move || {
405
+ if let Some(inner) = Weak::upgrade(&weak) {
406
+ ScrollBar { inner }.sync_visual_state();
407
+ }
408
+ }),
409
+ );
410
+ let weak = Rc::downgrade(&self.inner);
411
+ self.inner
412
+ .subscriptions
413
+ .borrow_mut()
414
+ .push(subscribe(move |theme| {
415
+ if let Some(inner) = Weak::upgrade(&weak) {
416
+ ScrollBar { inner }.handle_theme_changed(theme);
417
+ }
418
+ }));
419
+ }
420
+
421
+ fn handle_theme_changed(&self, theme: Theme) {
422
+ if !self.inner.track_color_overridden.get() {
423
+ self.inner.track_color.set(theme.colors.scrollbar_track);
424
+ }
425
+ if !self.inner.thumb_color_overridden.get() {
426
+ self.inner.thumb_color.set(theme.colors.scrollbar_thumb);
427
+ }
428
+ self.apply_color_style();
429
+ }
430
+
431
+ fn notify_chrome_metric_changed(&self) {
432
+ if let Some(handler) = self.inner.chrome_metric_changed.borrow().clone() {
433
+ handler();
434
+ }
435
+ }
436
+
437
+ fn end_drag(&self) {
438
+ self.inner.dragging.set(false);
439
+ crate::event::release_pointer(self.inner.thumb_node.handle());
440
+ unsafe { crate::ffi::fui_release_pointer_capture() };
441
+ self.apply_thumb_cursor();
442
+ }
443
+
444
+ fn compute_metrics(&self) -> ScrollMetrics {
445
+ let viewport_size = if self.inner.orientation == Orientation::Horizontal {
446
+ self.inner.scroll_state.viewport_width()
447
+ } else {
448
+ self.inner.scroll_state.viewport_height()
449
+ }
450
+ .max(0.0);
451
+ let content_size = if self.inner.orientation == Orientation::Horizontal {
452
+ self.inner.scroll_state.content_width()
453
+ } else {
454
+ self.inner.scroll_state.content_height()
455
+ }
456
+ .max(viewport_size);
457
+ let max_offset = content_size - viewport_size;
458
+ let raw_thumb_size = if content_size > 0.0 {
459
+ viewport_size * (viewport_size / content_size)
460
+ } else {
461
+ 0.0
462
+ };
463
+ let thumb_size = if viewport_size > 0.0 {
464
+ clamp(
465
+ raw_thumb_size,
466
+ self.inner.min_thumb_size.get(),
467
+ viewport_size,
468
+ )
469
+ } else {
470
+ 0.0
471
+ };
472
+ let max_thumb_offset = if viewport_size > thumb_size {
473
+ viewport_size - thumb_size
474
+ } else {
475
+ 0.0
476
+ };
477
+ let offset = self.axis_offset();
478
+ let thumb_offset = if max_offset > 0.0 && max_thumb_offset > 0.0 {
479
+ clamp(
480
+ (offset / max_offset) * max_thumb_offset,
481
+ 0.0,
482
+ max_thumb_offset,
483
+ )
484
+ } else {
485
+ 0.0
486
+ };
487
+ ScrollMetrics {
488
+ viewport_size,
489
+ max_offset,
490
+ thumb_size,
491
+ max_thumb_offset,
492
+ thumb_offset,
493
+ trailing_spacer_size: (viewport_size - thumb_size - thumb_offset).max(0.0),
494
+ }
495
+ }
496
+
497
+ fn sync_visual_state(&self) {
498
+ if !self.inner.chrome_visible.get() {
499
+ if self.inner.orientation == Orientation::Horizontal {
500
+ self.inner.track_node.width(0.0, Unit::Pixel);
501
+ self.inner.track_strip.width(0.0, Unit::Pixel);
502
+ self.inner.leading_spacer_node.width(0.0, Unit::Pixel);
503
+ self.inner.thumb_node.width(0.0, Unit::Pixel);
504
+ self.inner.trailing_spacer_node.width(0.0, Unit::Pixel);
505
+ } else {
506
+ self.inner.track_node.height(0.0, Unit::Pixel);
507
+ self.inner.track_strip.height(0.0, Unit::Pixel);
508
+ self.inner.leading_spacer_node.height(0.0, Unit::Pixel);
509
+ self.inner.thumb_node.height(0.0, Unit::Pixel);
510
+ self.inner.trailing_spacer_node.height(0.0, Unit::Pixel);
511
+ }
512
+ return;
513
+ }
514
+
515
+ let metrics = self.compute_metrics();
516
+ if self.inner.orientation == Orientation::Horizontal {
517
+ self.inner
518
+ .track_node
519
+ .width(metrics.viewport_size, Unit::Pixel);
520
+ self.inner
521
+ .track_strip
522
+ .width(metrics.viewport_size, Unit::Pixel);
523
+ self.inner
524
+ .leading_spacer_node
525
+ .width(metrics.thumb_offset, Unit::Pixel);
526
+ self.inner.thumb_node.width(metrics.thumb_size, Unit::Pixel);
527
+ self.inner
528
+ .trailing_spacer_node
529
+ .width(metrics.trailing_spacer_size, Unit::Pixel);
530
+ } else {
531
+ self.inner
532
+ .track_node
533
+ .height(metrics.viewport_size, Unit::Pixel);
534
+ self.inner
535
+ .track_strip
536
+ .height(metrics.viewport_size, Unit::Pixel);
537
+ self.inner
538
+ .leading_spacer_node
539
+ .height(metrics.thumb_offset, Unit::Pixel);
540
+ self.inner
541
+ .thumb_node
542
+ .height(metrics.thumb_size, Unit::Pixel);
543
+ self.inner
544
+ .trailing_spacer_node
545
+ .height(metrics.trailing_spacer_size, Unit::Pixel);
546
+ }
547
+ }
548
+
549
+ fn apply_geometry_style(&self) {
550
+ let track_thickness = if self.inner.chrome_visible.get() {
551
+ self.inner.track_thickness.get()
552
+ } else {
553
+ 0.0
554
+ };
555
+ let thumb_thickness = if self.inner.chrome_visible.get() {
556
+ self.inner.thumb_thickness.get()
557
+ } else {
558
+ 0.0
559
+ };
560
+ if self.inner.orientation == Orientation::Horizontal {
561
+ self.inner.track_node.height(track_thickness, Unit::Pixel);
562
+ self.inner.track_strip.height(track_thickness, Unit::Pixel);
563
+ self.inner
564
+ .leading_spacer_node
565
+ .height(track_thickness, Unit::Pixel);
566
+ self.inner
567
+ .trailing_spacer_node
568
+ .height(track_thickness, Unit::Pixel);
569
+ self.inner.thumb_node.height(thumb_thickness, Unit::Pixel);
570
+ } else {
571
+ self.inner.track_node.width(track_thickness, Unit::Pixel);
572
+ self.inner.track_strip.width(track_thickness, Unit::Pixel);
573
+ self.inner
574
+ .leading_spacer_node
575
+ .width(track_thickness, Unit::Pixel);
576
+ self.inner
577
+ .trailing_spacer_node
578
+ .width(track_thickness, Unit::Pixel);
579
+ self.inner.thumb_node.width(thumb_thickness, Unit::Pixel);
580
+ }
581
+ self.inner
582
+ .track_node
583
+ .corner_radius(self.inner.track_corner_radius.get());
584
+ self.inner
585
+ .thumb_node
586
+ .corner_radius(self.inner.thumb_corner_radius.get());
587
+ }
588
+
589
+ fn apply_color_style(&self) {
590
+ self.inner.track_node.bg_color(self.inner.track_color.get());
591
+ self.inner.thumb_node.bg_color(self.inner.thumb_color.get());
592
+ }
593
+
594
+ fn apply_thumb_cursor(&self) {
595
+ if platform::is_coarse_pointer() {
596
+ return;
597
+ }
598
+ self.inner
599
+ .thumb_node
600
+ .cursor(if !self.inner.chrome_visible.get() {
601
+ CursorStyle::Default
602
+ } else if self.inner.dragging.get() {
603
+ CursorStyle::Grabbing
604
+ } else {
605
+ CursorStyle::Grab
606
+ });
607
+ }
608
+
609
+ fn axis_position(&self, x: f32, y: f32) -> f32 {
610
+ if self.inner.orientation == Orientation::Horizontal {
611
+ x
612
+ } else {
613
+ y
614
+ }
615
+ }
616
+
617
+ fn axis_offset(&self) -> f32 {
618
+ if self.inner.orientation == Orientation::Horizontal {
619
+ self.inner.scroll_state.offset_x()
620
+ } else {
621
+ self.inner.scroll_state.offset_y()
622
+ }
623
+ }
624
+
625
+ fn handle_track_pointer_down(&self, pointer_x: f32, pointer_y: f32) {
626
+ if self.inner.dragging.get() {
627
+ return;
628
+ }
629
+ let metrics = self.compute_metrics();
630
+ if metrics.max_offset <= 0.0 || metrics.max_thumb_offset <= 0.0 {
631
+ return;
632
+ }
633
+ let Some(bounds) = ui::get_bounds(self.inner.track_node.handle().raw()) else {
634
+ return;
635
+ };
636
+ let local_pointer = if self.inner.orientation == Orientation::Horizontal {
637
+ pointer_x - bounds[0]
638
+ } else {
639
+ pointer_y - bounds[1]
640
+ };
641
+ let target_thumb_offset = clamp(
642
+ local_pointer - (metrics.thumb_size * 0.5),
643
+ 0.0,
644
+ metrics.max_thumb_offset,
645
+ );
646
+ let next_offset = (target_thumb_offset / metrics.max_thumb_offset) * metrics.max_offset;
647
+ self.set_scroll_offset(next_offset, metrics.max_offset);
648
+ }
649
+
650
+ fn set_scroll_offset(&self, offset: f32, max_offset: f32) {
651
+ let clamped_offset = clamp(offset, 0.0, max_offset);
652
+ if self.inner.orientation == Orientation::Horizontal {
653
+ self.inner.scroll_state.set_offset_x(clamped_offset);
654
+ if self.inner.target_handle.get() != HandleValue::Invalid as u64 {
655
+ ui::set_scroll_offset(
656
+ self.inner.target_handle.get(),
657
+ clamped_offset,
658
+ self.inner.scroll_state.offset_y(),
659
+ );
660
+ crate::frame_scheduler::mark_needs_commit();
661
+ }
662
+ } else {
663
+ self.inner.scroll_state.set_offset_y(clamped_offset);
664
+ if self.inner.target_handle.get() != HandleValue::Invalid as u64 {
665
+ ui::set_scroll_offset(
666
+ self.inner.target_handle.get(),
667
+ self.inner.scroll_state.offset_x(),
668
+ clamped_offset,
669
+ );
670
+ crate::frame_scheduler::mark_needs_commit();
671
+ }
672
+ }
673
+ }
674
+ }
675
+
676
+ #[cfg(test)]
677
+ mod tests {
678
+ use super::*;
679
+ use crate::ffi::{self, Call};
680
+ use crate::theme::{generate_theme, use_custom_theme, use_system_theme};
681
+ use crate::Application;
682
+
683
+ #[test]
684
+ fn scrollbar_style_surface_matches_fui_as_vertical_and_horizontal_geometry() {
685
+ ffi::test::reset();
686
+ let vertical_state = ScrollState::new();
687
+ vertical_state.set_viewport_height(100.0);
688
+ vertical_state.set_content_height(400.0);
689
+ let vertical = ScrollBar::new(vertical_state, Orientation::Vertical);
690
+ vertical
691
+ .track_width(12.0)
692
+ .thumb_width(6.0)
693
+ .thumb_min_height(30.0);
694
+ Application::mount(vertical.render());
695
+ let vertical_track_handle = vertical.inner.track_node.handle().raw();
696
+ let vertical_thumb_handle = vertical.inner.thumb_node.handle().raw();
697
+ let calls = ffi::test::take_calls();
698
+
699
+ assert!(calls.iter().any(|call| matches!(
700
+ call,
701
+ Call::SetWidth { handle, value, .. } if *handle == vertical_track_handle && (*value - 12.0).abs() < f32::EPSILON
702
+ )));
703
+ assert!(calls.iter().any(|call| matches!(
704
+ call,
705
+ Call::SetWidth { handle, value, .. } if *handle == vertical_thumb_handle && (*value - 6.0).abs() < f32::EPSILON
706
+ )));
707
+ assert!(calls.iter().any(|call| matches!(
708
+ call,
709
+ Call::SetHeight { handle, value, .. } if *handle == vertical_thumb_handle && (*value - 30.0).abs() < f32::EPSILON
710
+ )));
711
+
712
+ ffi::test::reset();
713
+ let horizontal_state = ScrollState::new();
714
+ horizontal_state.set_viewport_width(100.0);
715
+ horizontal_state.set_content_width(400.0);
716
+ let horizontal = ScrollBar::new(horizontal_state, Orientation::Horizontal);
717
+ horizontal
718
+ .track_width(14.0)
719
+ .thumb_width(9.0)
720
+ .thumb_min_height(32.0);
721
+ Application::mount(horizontal.render());
722
+ let horizontal_track_handle = horizontal.inner.track_node.handle().raw();
723
+ let horizontal_thumb_handle = horizontal.inner.thumb_node.handle().raw();
724
+ let calls = ffi::test::take_calls();
725
+
726
+ assert!(calls.iter().any(|call| matches!(
727
+ call,
728
+ Call::SetHeight { handle, value, .. } if *handle == horizontal_track_handle && (*value - 14.0).abs() < f32::EPSILON
729
+ )));
730
+ assert!(calls.iter().any(|call| matches!(
731
+ call,
732
+ Call::SetHeight { handle, value, .. } if *handle == horizontal_thumb_handle && (*value - 9.0).abs() < f32::EPSILON
733
+ )));
734
+ assert!(calls.iter().any(|call| matches!(
735
+ call,
736
+ Call::SetWidth { handle, value, .. } if *handle == horizontal_thumb_handle && (*value - 32.0).abs() < f32::EPSILON
737
+ )));
738
+ }
739
+
740
+ #[test]
741
+ fn scrollbar_theme_changes_update_default_colors_and_overrides_survive() {
742
+ ffi::test::reset();
743
+ use_custom_theme(generate_theme(true, 0x112233FF));
744
+ let state = ScrollState::new();
745
+ state.set_viewport_height(100.0);
746
+ state.set_content_height(400.0);
747
+ let scrollbar = ScrollBar::new(state, Orientation::Vertical);
748
+ Application::mount(scrollbar.render());
749
+ let _ = ffi::test::take_calls();
750
+
751
+ use_custom_theme(generate_theme(false, 0x445566FF));
752
+ let theme = current_theme();
753
+ let calls = ffi::test::take_calls();
754
+ assert!(calls.iter().any(|call| matches!(
755
+ call,
756
+ Call::SetBgColor { color, .. } if *color == theme.colors.scrollbar_track
757
+ )));
758
+ assert!(calls.iter().any(|call| matches!(
759
+ call,
760
+ Call::SetBgColor { color, .. } if *color == theme.colors.scrollbar_thumb
761
+ )));
762
+
763
+ ffi::test::reset();
764
+ let state = ScrollState::new();
765
+ state.set_viewport_height(100.0);
766
+ state.set_content_height(400.0);
767
+ let scrollbar = ScrollBar::new(state, Orientation::Vertical);
768
+ scrollbar
769
+ .track_width(10.0)
770
+ .thumb_width(7.0)
771
+ .track_corner_radius(5.0)
772
+ .thumb_corner_radius(4.0)
773
+ .track_color(0x123456FF)
774
+ .thumb_color(0xABCDEF88);
775
+ Application::mount(scrollbar.render());
776
+ let mount_calls = ffi::test::take_calls();
777
+ assert!(mount_calls.iter().any(|call| matches!(
778
+ call,
779
+ Call::SetBoxStyle { radius_tl, radius_tr, radius_br, radius_bl, .. }
780
+ if *radius_tl == 5.0
781
+ && *radius_tr == 5.0
782
+ && *radius_br == 5.0
783
+ && *radius_bl == 5.0
784
+ )));
785
+ assert!(mount_calls.iter().any(|call| matches!(
786
+ call,
787
+ Call::SetBoxStyle { radius_tl, radius_tr, radius_br, radius_bl, .. }
788
+ if *radius_tl == 4.0
789
+ && *radius_tr == 4.0
790
+ && *radius_br == 4.0
791
+ && *radius_bl == 4.0
792
+ )));
793
+
794
+ use_custom_theme(generate_theme(true, 0x556677FF));
795
+ let calls = ffi::test::take_calls();
796
+ assert!(calls.iter().any(|call| matches!(
797
+ call,
798
+ Call::SetBgColor { color, .. } if *color == 0x123456FF
799
+ )));
800
+ assert!(calls.iter().any(|call| matches!(
801
+ call,
802
+ Call::SetBgColor { color, .. } if *color == 0xABCDEF88
803
+ )));
804
+
805
+ use_system_theme();
806
+ }
807
+
808
+ #[test]
809
+ fn scrollbar_invalid_recipe_values_warn_and_clamp_like_fui_as() {
810
+ ffi::test::reset();
811
+ let state = ScrollState::new();
812
+ state.set_viewport_height(100.0);
813
+ state.set_content_height(400.0);
814
+ let scrollbar = ScrollBar::new(state, Orientation::Vertical);
815
+ scrollbar
816
+ .track_width(0.0)
817
+ .thumb_width(-2.0)
818
+ .thumb_min_height(0.0);
819
+ Application::mount(scrollbar.render());
820
+ let calls = ffi::test::take_calls();
821
+
822
+ assert!(calls.iter().any(|call| matches!(
823
+ call,
824
+ Call::Log { category, message }
825
+ if category == "Warning/Layout"
826
+ && message == "ScrollBar.trackThickness() received 0; clamping to 1.0."
827
+ )));
828
+ assert!(calls.iter().any(|call| matches!(
829
+ call,
830
+ Call::Log { category, message }
831
+ if category == "Warning/Layout"
832
+ && message == "ScrollBar.thumbThickness() received -2; clamping to 1.0."
833
+ )));
834
+ assert!(calls.iter().any(|call| matches!(
835
+ call,
836
+ Call::Log { category, message }
837
+ if category == "Warning/Layout"
838
+ && message == "ScrollBar.thumbMinHeight() received 0; clamping to 1.0."
839
+ )));
840
+ assert!(calls.iter().any(|call| matches!(
841
+ call,
842
+ Call::SetWidth { value, .. } if (*value - 1.0).abs() < f32::EPSILON
843
+ )));
844
+ }
845
+ }