@effindomv2/fui-rs 0.1.7 → 0.1.9

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 (37) hide show
  1. package/Cargo.toml +1 -1
  2. package/package.json +1 -1
  3. package/src/bridge_callbacks.rs +1 -0
  4. package/src/controls/appearance.rs +200 -0
  5. package/src/controls/button.rs +48 -122
  6. package/src/controls/checkbox.rs +27 -3
  7. package/src/controls/combobox.rs +61 -23
  8. package/src/controls/context_menu.rs +156 -256
  9. package/src/controls/control_template_set.rs +2 -3
  10. package/src/controls/control_tokens.rs +76 -0
  11. package/src/controls/dialog.rs +80 -78
  12. package/src/controls/dropdown.rs +46 -5
  13. package/src/controls/form.rs +6 -0
  14. package/src/controls/internal/button_presenter.rs +21 -20
  15. package/src/controls/internal/pressable_labeled_control.rs +11 -11
  16. package/src/controls/internal/text_input_core.rs +40 -22
  17. package/src/controls/internal/text_input_presenter.rs +29 -23
  18. package/src/controls/mod.rs +7 -2
  19. package/src/controls/nav_link.rs +15 -11
  20. package/src/controls/popup.rs +24 -40
  21. package/src/controls/progress_bar.rs +80 -109
  22. package/src/controls/radio_button.rs +27 -3
  23. package/src/controls/radio_group.rs +6 -0
  24. package/src/controls/slider.rs +38 -14
  25. package/src/controls/switch.rs +27 -3
  26. package/src/controls/tests.rs +796 -56
  27. package/src/controls/text_area.rs +12 -2
  28. package/src/controls/text_input.rs +19 -3
  29. package/src/lib.rs +44 -38
  30. package/src/node/core.rs +2 -0
  31. package/src/node/flex_box.rs +245 -2
  32. package/src/node/grid.rs +5 -0
  33. package/src/node/helpers.rs +6 -3
  34. package/src/node/mod.rs +48 -0
  35. package/src/node/presenter_host_style.rs +177 -0
  36. package/src/node/text_node.rs +35 -0
  37. package/src/popup_presenter.rs +32 -0
@@ -87,6 +87,44 @@ impl SliderSizing {
87
87
  }
88
88
  }
89
89
 
90
+ #[derive(Clone, Copy, Debug, Default, PartialEq)]
91
+ pub struct ProgressBarSizing {
92
+ length: f32,
93
+ thickness: f32,
94
+ }
95
+
96
+ impl ProgressBarSizing {
97
+ pub fn new() -> Self {
98
+ Self::default()
99
+ }
100
+
101
+ pub fn length(mut self, value: f32) -> Self {
102
+ self.length = sanitize_positive("ProgressBarSizing", "length", value);
103
+ self
104
+ }
105
+
106
+ pub fn thickness(mut self, value: f32) -> Self {
107
+ self.thickness = sanitize_positive("ProgressBarSizing", "thickness", value);
108
+ self
109
+ }
110
+
111
+ pub fn has_length(&self) -> bool {
112
+ self.length > 0.0
113
+ }
114
+
115
+ pub fn has_thickness(&self) -> bool {
116
+ self.thickness > 0.0
117
+ }
118
+
119
+ pub fn length_px(&self) -> f32 {
120
+ self.length
121
+ }
122
+
123
+ pub fn thickness_px(&self) -> f32 {
124
+ self.thickness
125
+ }
126
+ }
127
+
90
128
  #[derive(Clone, Copy, Debug, Default, PartialEq)]
91
129
  pub struct DropdownSizing {
92
130
  field_font_size: f32,
@@ -407,6 +445,44 @@ impl SliderColors {
407
445
  }
408
446
  }
409
447
 
448
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
449
+ pub struct ProgressBarColors {
450
+ track: Option<u32>,
451
+ fill: Option<u32>,
452
+ }
453
+
454
+ impl ProgressBarColors {
455
+ pub fn new() -> Self {
456
+ Self::default()
457
+ }
458
+
459
+ pub fn track(mut self, color: u32) -> Self {
460
+ self.track = Some(color);
461
+ self
462
+ }
463
+
464
+ pub fn fill(mut self, color: u32) -> Self {
465
+ self.fill = Some(color);
466
+ self
467
+ }
468
+
469
+ pub fn has_track(&self) -> bool {
470
+ self.track.is_some()
471
+ }
472
+
473
+ pub fn track_color(&self) -> u32 {
474
+ self.track.unwrap_or(0)
475
+ }
476
+
477
+ pub fn has_fill(&self) -> bool {
478
+ self.fill.is_some()
479
+ }
480
+
481
+ pub fn fill_color(&self) -> u32 {
482
+ self.fill.unwrap_or(0)
483
+ }
484
+ }
485
+
410
486
  #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
411
487
  pub struct DropdownColors {
412
488
  pub(crate) background: Option<u32>,
@@ -1,3 +1,4 @@
1
+ use super::DialogAppearance;
1
2
  use super::{Button, Form};
2
3
  use crate::app;
3
4
  use crate::bindings::ui;
@@ -5,7 +6,6 @@ use crate::ffi::{
5
6
  AlignItems, BorderStyle, FlexDirection, JustifyContent, PositionType, SemanticRole, Unit,
6
7
  };
7
8
  use crate::node::{flex_box, portal, Border, FlexBox, Node, NodeRef, TextNode, WeakFlexBox};
8
- use crate::signal::SubscriptionGuard;
9
9
  use crate::theme::{current_theme, subscribe, Theme};
10
10
  use std::cell::RefCell;
11
11
  use std::rc::Rc;
@@ -40,7 +40,6 @@ pub struct Dialog {
40
40
  card: FlexBox,
41
41
  overlay: FlexBox,
42
42
  state: Rc<RefCell<DialogState>>,
43
- theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
44
43
  }
45
44
 
46
45
  #[derive(Clone)]
@@ -69,7 +68,12 @@ struct DialogState {
69
68
  card_border_width_value: f32,
70
69
  card_border_color_value: u32,
71
70
  card_border_style_value: BorderStyle,
72
- card_corner_radius_value: f32,
71
+ card_border_dash_on_value: f32,
72
+ card_border_dash_off_value: f32,
73
+ card_corner_top_left_value: f32,
74
+ card_corner_top_right_value: f32,
75
+ card_corner_bottom_right_value: f32,
76
+ card_corner_bottom_left_value: f32,
73
77
  backdrop_color_overridden: bool,
74
78
  shadow_overridden: bool,
75
79
  card_background_overridden: bool,
@@ -96,7 +100,12 @@ impl DialogState {
96
100
  card_border_width_value: 1.0,
97
101
  card_border_color_value: theme.colors.border,
98
102
  card_border_style_value: BorderStyle::Solid,
99
- card_corner_radius_value: theme.spacing.md,
103
+ card_border_dash_on_value: 0.0,
104
+ card_border_dash_off_value: 0.0,
105
+ card_corner_top_left_value: theme.spacing.md,
106
+ card_corner_top_right_value: theme.spacing.md,
107
+ card_corner_bottom_right_value: theme.spacing.md,
108
+ card_corner_bottom_left_value: theme.spacing.md,
100
109
  backdrop_color_overridden: false,
101
110
  shadow_overridden: false,
102
111
  card_background_overridden: false,
@@ -184,7 +193,6 @@ impl Dialog {
184
193
  .align_items(AlignItems::Center)
185
194
  .child(&card);
186
195
  let state = Rc::new(RefCell::new(DialogState::from_theme(theme)));
187
- let theme_guard = Rc::new(RefCell::new(None));
188
196
 
189
197
  root.position_type(PositionType::Absolute)
190
198
  .position(0.0, 0.0)
@@ -203,7 +211,6 @@ impl Dialog {
203
211
  card,
204
212
  overlay,
205
213
  state,
206
- theme_guard,
207
214
  };
208
215
  dialog.content(title, body);
209
216
  dialog.bind_events();
@@ -265,79 +272,51 @@ impl Dialog {
265
272
  self.cancel_button.clone()
266
273
  }
267
274
 
268
- pub fn backdrop_color(&self, color: u32) -> &Self {
275
+ pub fn appearance(&self, appearance: DialogAppearance) -> &Self {
269
276
  let mut state = self.state.borrow_mut();
270
- state.backdrop_color_overridden = true;
271
- state.backdrop_color_value = color;
272
- drop(state);
273
- self.apply_theme();
274
- self
275
- }
276
-
277
- pub fn background_blur(&self, sigma: f32) -> &Self {
278
- self.state.borrow_mut().dialog_background_blur_sigma_value = sigma.max(0.0);
279
- self.apply_theme();
280
- self
281
- }
282
-
283
- pub fn card_shadow(
284
- &self,
285
- color: u32,
286
- offset_x: f32,
287
- offset_y: f32,
288
- blur_sigma: f32,
289
- spread: f32,
290
- ) -> &Self {
291
- let mut state = self.state.borrow_mut();
292
- state.shadow_overridden = true;
293
- state.shadow_color_value = color;
294
- state.shadow_offset_x_value = offset_x;
295
- state.shadow_offset_y_value = offset_y;
296
- state.shadow_blur_sigma_value = blur_sigma;
297
- state.shadow_spread_value = spread;
298
- drop(state);
299
- self.apply_theme();
300
- self
301
- }
302
-
303
- pub fn card_color(&self, color: u32) -> &Self {
304
- let mut state = self.state.borrow_mut();
305
- state.card_background_overridden = true;
306
- state.card_background_color_value = color;
307
- drop(state);
308
- self.apply_theme();
309
- self
310
- }
311
-
312
- pub fn card_border(&self, width: f32, color: u32) -> &Self {
313
- let mut state = self.state.borrow_mut();
314
- state.card_border_overridden = true;
315
- state.card_border_width_value = width;
316
- state.card_border_color_value = color;
317
- state.card_border_style_value = BorderStyle::Solid;
318
- drop(state);
319
- self.apply_theme();
320
- self
321
- }
277
+ let backdrop = appearance.backdrop.unwrap_or_default();
278
+ state.backdrop_color_overridden = backdrop.color.is_some();
279
+ if let Some(color) = backdrop.color {
280
+ state.backdrop_color_value = color;
281
+ }
282
+ state.dialog_background_blur_sigma_value = backdrop.blur.unwrap_or(DIALOG_BACKGROUND_BLUR);
322
283
 
323
- pub fn card_border_config(&self, border: Border) -> &Self {
324
- let mut state = self.state.borrow_mut();
325
- state.card_border_overridden = true;
326
- state.card_border_width_value = border.width;
327
- state.card_border_color_value = border.color;
328
- state.card_border_style_value = border.style;
284
+ let card = appearance.card.unwrap_or_default();
285
+ state.card_background_overridden = card.background.is_some();
286
+ if let Some(color) = card.background {
287
+ state.card_background_color_value = color;
288
+ }
289
+ state.card_border_overridden = card.border.is_some();
290
+ if let Some(border) = card.border {
291
+ state.card_border_width_value = border.width;
292
+ state.card_border_color_value = border.color;
293
+ state.card_border_style_value = border.style;
294
+ state.card_border_dash_on_value = border.dash_on;
295
+ state.card_border_dash_off_value = border.dash_off;
296
+ }
297
+ state.card_corner_radius_overridden = card.corners.is_some();
298
+ if let Some(corners) = card.corners {
299
+ state.card_corner_top_left_value = corners.top_left;
300
+ state.card_corner_top_right_value = corners.top_right;
301
+ state.card_corner_bottom_right_value = corners.bottom_right;
302
+ state.card_corner_bottom_left_value = corners.bottom_left;
303
+ }
304
+ state.shadow_overridden = card.shadow.is_some();
305
+ if let Some(shadow) = card.shadow {
306
+ state.shadow_color_value = shadow.color;
307
+ state.shadow_offset_x_value = shadow.offset_x;
308
+ state.shadow_offset_y_value = shadow.offset_y;
309
+ state.shadow_blur_sigma_value = shadow.blur_sigma;
310
+ state.shadow_spread_value = shadow.spread;
311
+ }
329
312
  drop(state);
313
+ self.event_target().handle_theme_changed(current_theme());
330
314
  self.apply_theme();
331
315
  self
332
316
  }
333
317
 
334
- pub fn card_corner_radius(&self, radius: f32) -> &Self {
335
- let mut state = self.state.borrow_mut();
336
- state.card_corner_radius_overridden = true;
337
- state.card_corner_radius_value = radius;
338
- drop(state);
339
- self.apply_theme();
340
- self
318
+ pub fn clear_appearance(&self) -> &Self {
319
+ self.appearance(DialogAppearance::new())
341
320
  }
342
321
 
343
322
  pub fn content(&self, title: impl Into<String>, body: impl Into<String>) -> &Self {
@@ -401,10 +380,13 @@ impl Dialog {
401
380
  let event_target = self.event_target();
402
381
  let title = self.title_node.clone();
403
382
  let body = self.body_node.clone();
404
- *self.theme_guard.borrow_mut() = Some(subscribe(move |theme| {
383
+ let guard = subscribe(move |theme| {
405
384
  event_target.handle_theme_changed(theme);
406
385
  event_target.apply_theme(&title, &body);
407
- }));
386
+ });
387
+ self.root
388
+ .retained_node_ref()
389
+ .retain_attachment(Rc::new(guard));
408
390
  }
409
391
 
410
392
  fn apply_theme(&self) {
@@ -428,6 +410,10 @@ impl Node for Dialog {
428
410
  self.root.retained_node_ref()
429
411
  }
430
412
 
413
+ fn retained_owner_attachment(&self) -> Option<Rc<dyn std::any::Any>> {
414
+ Some(Rc::new(self.clone()))
415
+ }
416
+
431
417
  fn build_self(&self) {
432
418
  self.apply_theme();
433
419
  self.root.build_self();
@@ -442,6 +428,12 @@ impl Node for Dialog {
442
428
  }
443
429
  }
444
430
 
431
+ impl crate::node::HasFlexBoxRoot for Dialog {
432
+ fn flex_box_root(&self) -> &FlexBox {
433
+ &self.root
434
+ }
435
+ }
436
+
445
437
  impl DialogEventTarget {
446
438
  fn hide(&self) {
447
439
  let Some(root) = self.root.upgrade() else {
@@ -524,9 +516,14 @@ impl DialogEventTarget {
524
516
  state.card_border_width_value = 1.0;
525
517
  state.card_border_color_value = theme.colors.border;
526
518
  state.card_border_style_value = BorderStyle::Solid;
519
+ state.card_border_dash_on_value = 0.0;
520
+ state.card_border_dash_off_value = 0.0;
527
521
  }
528
522
  if !state.card_corner_radius_overridden {
529
- state.card_corner_radius_value = theme.spacing.md;
523
+ state.card_corner_top_left_value = theme.spacing.md;
524
+ state.card_corner_top_right_value = theme.spacing.md;
525
+ state.card_corner_bottom_right_value = theme.spacing.md;
526
+ state.card_corner_bottom_left_value = theme.spacing.md;
530
527
  }
531
528
  }
532
529
 
@@ -540,13 +537,18 @@ impl DialogEventTarget {
540
537
  }
541
538
  if let Some(card) = self.card.upgrade() {
542
539
  card.bg_color(state.card_background_color_value)
543
- .corner_radius(state.card_corner_radius_value)
540
+ .corners(
541
+ state.card_corner_top_left_value,
542
+ state.card_corner_top_right_value,
543
+ state.card_corner_bottom_right_value,
544
+ state.card_corner_bottom_left_value,
545
+ )
544
546
  .border_config(Border {
545
547
  width: state.card_border_width_value,
546
548
  color: state.card_border_color_value,
547
549
  style: state.card_border_style_value,
548
- dash_on: 0.0,
549
- dash_off: 0.0,
550
+ dash_on: state.card_border_dash_on_value,
551
+ dash_off: state.card_border_dash_off_value,
550
552
  })
551
553
  .drop_shadow(
552
554
  state.shadow_color_value,
@@ -238,6 +238,7 @@ impl Dropdown {
238
238
  });
239
239
  *shared.self_weak.borrow_mut() = Rc::downgrade(&shared);
240
240
  *shared_slot.borrow_mut() = Some(Rc::downgrade(&shared));
241
+ root.retained_node_ref().retain_attachment(shared.clone());
241
242
 
242
243
  popup_list.popup_presenter.overlay_node().on_click({
243
244
  let weak_shared = Rc::downgrade(&shared);
@@ -470,7 +471,15 @@ impl Dropdown {
470
471
  self
471
472
  }
472
473
 
473
- pub fn sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
474
+ pub fn sizing(&self, sizing: DropdownSizing) -> &Self {
475
+ self.set_sizing(Some(sizing))
476
+ }
477
+
478
+ pub fn clear_sizing(&self) -> &Self {
479
+ self.set_sizing(None)
480
+ }
481
+
482
+ fn set_sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
474
483
  self.shared.close();
475
484
  self.shared.sizing_value.set(sizing);
476
485
  if self.shared.uses_default_field_presenter() {
@@ -506,14 +515,30 @@ impl Dropdown {
506
515
  self
507
516
  }
508
517
 
509
- pub fn colors(&self, colors: Option<DropdownColors>) -> &Self {
518
+ pub fn colors(&self, colors: DropdownColors) -> &Self {
519
+ self.set_colors(Some(colors))
520
+ }
521
+
522
+ pub fn clear_colors(&self) -> &Self {
523
+ self.set_colors(None)
524
+ }
525
+
526
+ fn set_colors(&self, colors: Option<DropdownColors>) -> &Self {
510
527
  self.shared.colors_value.set(colors);
511
528
  self.shared.popup_list.colors(colors);
512
529
  self.shared.handle_theme_changed();
513
530
  self
514
531
  }
515
532
 
516
- pub fn field_template(&self, template: Option<Rc<dyn DropdownFieldTemplate>>) -> &Self {
533
+ pub fn field_template(&self, template: Rc<dyn DropdownFieldTemplate>) -> &Self {
534
+ self.set_field_template(Some(template))
535
+ }
536
+
537
+ pub fn clear_field_template(&self) -> &Self {
538
+ self.set_field_template(None)
539
+ }
540
+
541
+ fn set_field_template(&self, template: Option<Rc<dyn DropdownFieldTemplate>>) -> &Self {
517
542
  self.shared.close();
518
543
  *self.shared.field_template_value.borrow_mut() = template.clone();
519
544
  let next_field_presenter = create_field_presenter(template, self.shared.sizing_value.get());
@@ -528,7 +553,15 @@ impl Dropdown {
528
553
  self
529
554
  }
530
555
 
531
- pub fn chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
556
+ pub fn chevron_template(&self, template: Rc<dyn DropdownChevronTemplate>) -> &Self {
557
+ self.set_chevron_template(Some(template))
558
+ }
559
+
560
+ pub fn clear_chevron_template(&self) -> &Self {
561
+ self.set_chevron_template(None)
562
+ }
563
+
564
+ fn set_chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
532
565
  self.shared.close();
533
566
  *self.shared.chevron_template_value.borrow_mut() = template.clone();
534
567
  let previous_presenter = self.shared.chevron_presenter.borrow().clone();
@@ -549,7 +582,15 @@ impl Dropdown {
549
582
  self
550
583
  }
551
584
 
552
- pub fn option_row_template(
585
+ pub fn option_row_template(&self, template: Rc<dyn DropdownOptionRowTemplate>) -> &Self {
586
+ self.set_option_row_template(Some(template))
587
+ }
588
+
589
+ pub fn clear_option_row_template(&self) -> &Self {
590
+ self.set_option_row_template(None)
591
+ }
592
+
593
+ fn set_option_row_template(
553
594
  &self,
554
595
  template: Option<Rc<dyn DropdownOptionRowTemplate>>,
555
596
  ) -> &Self {
@@ -227,3 +227,9 @@ impl Node for Form {
227
227
  self.shared.root.dispose();
228
228
  }
229
229
  }
230
+
231
+ impl crate::node::HasFlexBoxRoot for Form {
232
+ fn flex_box_root(&self) -> &FlexBox {
233
+ &self.shared.root
234
+ }
235
+ }
@@ -1,6 +1,8 @@
1
1
  use crate::controls::ButtonColors;
2
2
  use crate::ffi::{AlignItems, FlexDirection, JustifyContent};
3
- use crate::node::{flex_box, FlexBox, TextCore};
3
+ use crate::node::{
4
+ flex_box, Border, Corners, EdgeInsets, FlexBox, PresenterHostStyle, Shadow, TextCore,
5
+ };
4
6
  use crate::theme::Theme;
5
7
  use crate::{FontStyle, FontWeight};
6
8
  use std::rc::Rc;
@@ -16,13 +18,12 @@ pub struct ButtonVisualState {
16
18
  pub trait ButtonPresenter {
17
19
  fn content_root(&self) -> FlexBox;
18
20
  fn label_node(&self) -> TextCore;
19
- fn apply(
21
+ fn present(
20
22
  &self,
21
- host: &FlexBox,
22
23
  theme: Theme,
23
24
  state: ButtonVisualState,
24
25
  colors: Option<ButtonColors>,
25
- );
26
+ ) -> PresenterHostStyle;
26
27
  }
27
28
 
28
29
  pub trait ButtonTemplate {
@@ -60,13 +61,12 @@ impl ButtonPresenter for DefaultButtonPresenter {
60
61
  self.label_node.clone()
61
62
  }
62
63
 
63
- fn apply(
64
+ fn present(
64
65
  &self,
65
- host: &FlexBox,
66
66
  theme: Theme,
67
67
  state: ButtonVisualState,
68
68
  colors: Option<ButtonColors>,
69
- ) {
69
+ ) -> PresenterHostStyle {
70
70
  let background = if !state.enabled {
71
71
  colors
72
72
  .and_then(|colors| colors.background)
@@ -100,19 +100,6 @@ impl ButtonPresenter for DefaultButtonPresenter {
100
100
  .and_then(|colors| colors.text_primary)
101
101
  .unwrap_or(theme.colors.text_on_accent)
102
102
  };
103
- host.flex_direction(FlexDirection::Row)
104
- .justify_content(JustifyContent::Center)
105
- .align_items(AlignItems::Center)
106
- .corner_radius(theme.spacing.sm)
107
- .border(1.0, border)
108
- .padding(
109
- theme.spacing.md,
110
- theme.spacing.sm,
111
- theme.spacing.md,
112
- theme.spacing.sm,
113
- )
114
- .drop_shadow(0x00000000, 0.0, 0.0, 0.0, 0.0)
115
- .bg_color(background);
116
103
  self.content_root
117
104
  .flex_direction(FlexDirection::Row)
118
105
  .align_items(AlignItems::Center)
@@ -123,6 +110,20 @@ impl ButtonPresenter for DefaultButtonPresenter {
123
110
  .font_style(FontStyle::Normal)
124
111
  .font_size(theme.fonts.size_body)
125
112
  .text_color(text_color);
113
+ PresenterHostStyle::new()
114
+ .flex_direction(FlexDirection::Row)
115
+ .justify_content(JustifyContent::Center)
116
+ .align_items(AlignItems::Center)
117
+ .corners(Corners::all(theme.spacing.sm))
118
+ .border(Border::solid(1.0, border))
119
+ .padding(EdgeInsets::new(
120
+ theme.spacing.md,
121
+ theme.spacing.sm,
122
+ theme.spacing.md,
123
+ theme.spacing.sm,
124
+ ))
125
+ .shadow(Shadow::new(0x00000000, 0.0, 0.0, 0.0, 0.0))
126
+ .background(background)
126
127
  }
127
128
  }
128
129
 
@@ -4,7 +4,6 @@ use crate::ffi::{
4
4
  AlignItems, CursorStyle, FlexDirection, KeyEventType, PointerEventType, SemanticRole, Unit,
5
5
  };
6
6
  use crate::node::{flex_box, FlexBox, Node, TextCore, WeakFlexBox};
7
- use crate::signal::SubscriptionGuard;
8
7
  use crate::theme::{current_theme, subscribe};
9
8
  use crate::{focus_adorner, focus_visibility};
10
9
  use std::cell::{Cell, RefCell};
@@ -44,8 +43,6 @@ pub(crate) struct PressableLabeledControl {
44
43
  activation: Rc<RefCell<Option<ActivationCallback>>>,
45
44
  state_changed: Rc<RefCell<Option<StateCallback>>>,
46
45
  key_handler: Rc<RefCell<Option<KeyCallback>>>,
47
- theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
48
- focus_visibility_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
49
46
  }
50
47
 
51
48
  impl PressableLabeledControl {
@@ -87,8 +84,6 @@ impl PressableLabeledControl {
87
84
  activation: Rc::new(RefCell::new(None)),
88
85
  state_changed: Rc::new(RefCell::new(None)),
89
86
  key_handler: Rc::new(RefCell::new(None)),
90
- theme_guard: Rc::new(RefCell::new(None)),
91
- focus_visibility_guard: Rc::new(RefCell::new(None)),
92
87
  };
93
88
  control.install_handlers();
94
89
  control.install_theme_subscription();
@@ -246,17 +241,22 @@ impl PressableLabeledControl {
246
241
 
247
242
  fn install_theme_subscription(&self) {
248
243
  let target = self.event_target();
249
- *self.theme_guard.borrow_mut() = Some(subscribe(move |_theme| {
244
+ let theme_guard = subscribe(move |_theme| {
250
245
  target.sync_base_theme();
251
246
  target.notify_state_changed();
252
247
  target.sync_focus_chrome();
253
- }));
248
+ });
249
+ self.root
250
+ .retained_node_ref()
251
+ .retain_attachment(Rc::new(theme_guard));
254
252
 
255
253
  let target = self.event_target();
256
- *self.focus_visibility_guard.borrow_mut() =
257
- Some(focus_visibility::subscribe(move |_visible| {
258
- target.sync_focus_chrome();
259
- }));
254
+ let focus_guard = focus_visibility::subscribe(move |_visible| {
255
+ target.sync_focus_chrome();
256
+ });
257
+ self.root
258
+ .retained_node_ref()
259
+ .retain_attachment(Rc::new(focus_guard));
260
260
  }
261
261
 
262
262
  fn event_target(&self) -> PressableLabeledControlEventTarget {