@effindomv2/fui-rs 0.1.4 → 0.1.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fui-rs"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  edition = "2021"
5
5
  license = "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial"
6
6
 
package/README.md CHANGED
@@ -62,6 +62,7 @@ fui_app!(FlexBox, build_page);
62
62
  |---|---|
63
63
  | Retained app lifecycle macros | Available |
64
64
  | `ui!` mixed child tree macro | Available |
65
+ | `fui_component!` retained component delegation | Available |
65
66
  | Flex/Grid layout nodes | Available |
66
67
  | Text, rich text, image, SVG | Available |
67
68
  | Buttons, toggles, slider, dropdown, combobox | Available |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-rs",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
6
  "description": "EffinDom v2 Rust frontend framework SDK and host generators",
@@ -39,7 +39,7 @@
39
39
  "prepublishOnly": "npm run check:abi && npm run lint && npm run typecheck"
40
40
  },
41
41
  "dependencies": {
42
- "@effindomv2/runtime": "0.1.24"
42
+ "@effindomv2/runtime": "0.1.26"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@playwright/test": "1.59.1",
package/scripts/build.sh CHANGED
@@ -196,9 +196,14 @@ if [ -f "${SHARED_DEMO_TEXTURE}" ]; then
196
196
  cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage4/demo-texture.png"
197
197
  cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage5/demo-texture.png"
198
198
  fi
199
- cat > "${OUT_DIR}/effindom-runtime-config.js" << 'RUNTIMECFG'
199
+ RUNTIME_SET_HASH="$(node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1], "utf8")).runtime_set_hash; if (typeof value !== "string" || value.length === 0) process.exit(1); process.stdout.write(value);' "${RUNTIME_DIST_DIR}/effindom.v2.manifest.json")"
200
+ cat > "${OUT_DIR}/effindom-runtime-config.js" << RUNTIMECFG
200
201
  window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
201
- manifestUrl: './effindom.v2.manifest.json',
202
+ manifestUrls: [
203
+ 'https://runtimes.effindom.dev/v2/manifests/${RUNTIME_SET_HASH}.json',
204
+ './effindom.v2.manifest.json',
205
+ ],
206
+ expectedRuntimeSetHash: '${RUNTIME_SET_HASH}',
202
207
  });
203
208
  RUNTIMECFG
204
209
  cp "${OUT_DIR}/effindom-runtime-config.js" "${DEMO_OUT_DIR}/effindom-runtime-config.js"
package/src/app.rs CHANGED
@@ -109,7 +109,7 @@ impl ApplicationRegistration {
109
109
  }
110
110
  }
111
111
 
112
- pub fn page<TNode: Node>(mut self, build_page: impl Fn() -> TNode + 'static) -> Self {
112
+ pub fn page<TNode: Node + 'static>(mut self, build_page: impl Fn() -> TNode + 'static) -> Self {
113
113
  self.build_page_fn = Rc::new(move || {
114
114
  let node = build_page();
115
115
  let shell = flex_box();
@@ -133,7 +133,7 @@ pub struct ManagedApplication<TPage: 'static> {
133
133
  }
134
134
 
135
135
  impl<TPage: 'static> ManagedApplication<TPage> {
136
- pub fn new<TNode: Node>(
136
+ pub fn new<TNode: Node + 'static>(
137
137
  build_page: impl Fn() -> TPage + 'static,
138
138
  get_root: impl Fn(&TPage) -> TNode + 'static,
139
139
  ) -> Self {
@@ -26,6 +26,8 @@ pub struct Button {
26
26
  hover_background_override: Rc<Cell<Option<u32>>>,
27
27
  pressed_background_override: Rc<Cell<Option<u32>>>,
28
28
  border_override: Rc<Cell<Option<Border>>>,
29
+ font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
30
+ font_size_override: Rc<Cell<Option<f32>>>,
29
31
  text_color_override: Rc<Cell<Option<u32>>>,
30
32
  colors_value: Rc<Cell<Option<ButtonColors>>>,
31
33
  theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
@@ -66,6 +68,8 @@ impl Button {
66
68
  hover_background_override: Rc::new(Cell::new(None)),
67
69
  pressed_background_override: Rc::new(Cell::new(None)),
68
70
  border_override: Rc::new(Cell::new(None)),
71
+ font_family_override: Rc::new(RefCell::new(None)),
72
+ font_size_override: Rc::new(Cell::new(None)),
69
73
  text_color_override: Rc::new(Cell::new(None)),
70
74
  colors_value: Rc::new(Cell::new(None)),
71
75
  theme_guard: Rc::new(RefCell::new(None)),
@@ -250,10 +254,19 @@ impl Button {
250
254
  self
251
255
  }
252
256
 
253
- pub fn text_color(&self, color: u32) -> &Self {
257
+ fn set_explicit_text_color(&self, color: u32) {
254
258
  self.text_color_override.set(Some(color));
255
259
  self.sync_visual_state();
256
- self
260
+ }
261
+
262
+ fn set_explicit_font_family(&self, family: crate::FontFamily) {
263
+ self.font_family_override.replace(Some(family.clone()));
264
+ self.label.borrow().font_family(family);
265
+ }
266
+
267
+ fn set_explicit_font_size(&self, size: f32) {
268
+ self.font_size_override.set(Some(size));
269
+ self.label.borrow().font_size(size);
257
270
  }
258
271
 
259
272
  fn install_visual_subscriptions(&self) {
@@ -285,6 +298,8 @@ impl Button {
285
298
  hover_background_override: self.hover_background_override.clone(),
286
299
  pressed_background_override: self.pressed_background_override.clone(),
287
300
  border_override: self.border_override.clone(),
301
+ font_family_override: self.font_family_override.clone(),
302
+ font_size_override: self.font_size_override.clone(),
288
303
  text_color_override: self.text_color_override.clone(),
289
304
  colors_value: self.colors_value.clone(),
290
305
  }
@@ -328,6 +343,8 @@ impl Button {
328
343
  self.hover_background_override.get(),
329
344
  self.pressed_background_override.get(),
330
345
  self.border_override.get(),
346
+ self.font_family_override.borrow().clone(),
347
+ self.font_size_override.get(),
331
348
  self.text_color_override.get(),
332
349
  self.colors_value.get(),
333
350
  );
@@ -380,6 +397,20 @@ impl HasFlexBoxRoot for Button {
380
397
  }
381
398
  }
382
399
 
400
+ impl LabeledControlTextStyle for Button {
401
+ fn set_label_font_family(&self, family: crate::FontFamily) {
402
+ self.set_explicit_font_family(family);
403
+ }
404
+
405
+ fn set_label_font_size(&self, size: f32) {
406
+ self.set_explicit_font_size(size);
407
+ }
408
+
409
+ fn set_label_text_color(&self, color: u32) {
410
+ self.set_explicit_text_color(color);
411
+ }
412
+ }
413
+
383
414
  fn sync_button_visual_state(
384
415
  root: &FlexBox,
385
416
  presenter: &Rc<RefCell<Rc<dyn ButtonPresenter>>>,
@@ -391,6 +422,8 @@ fn sync_button_visual_state(
391
422
  hover_background_override: Option<u32>,
392
423
  pressed_background_override: Option<u32>,
393
424
  border_override: Option<Border>,
425
+ font_family_override: Option<crate::FontFamily>,
426
+ font_size_override: Option<f32>,
394
427
  text_color_override: Option<u32>,
395
428
  colors: Option<ButtonColors>,
396
429
  ) {
@@ -421,6 +454,12 @@ fn sync_button_visual_state(
421
454
  if let Some(border) = border_override {
422
455
  root.border_config(border);
423
456
  }
457
+ if let Some(family) = font_family_override {
458
+ presenter.label_node().font_family(family);
459
+ }
460
+ if let Some(size) = font_size_override {
461
+ presenter.label_node().font_size(size);
462
+ }
424
463
  if let Some(color) = text_color_override {
425
464
  presenter.label_node().text_color(color);
426
465
  }
@@ -462,6 +501,8 @@ struct ButtonEventTarget {
462
501
  hover_background_override: Rc<Cell<Option<u32>>>,
463
502
  pressed_background_override: Rc<Cell<Option<u32>>>,
464
503
  border_override: Rc<Cell<Option<Border>>>,
504
+ font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
505
+ font_size_override: Rc<Cell<Option<f32>>>,
465
506
  text_color_override: Rc<Cell<Option<u32>>>,
466
507
  colors_value: Rc<Cell<Option<ButtonColors>>>,
467
508
  }
@@ -488,6 +529,8 @@ impl ButtonEventTarget {
488
529
  self.hover_background_override.get(),
489
530
  self.pressed_background_override.get(),
490
531
  self.border_override.get(),
532
+ self.font_family_override.borrow().clone(),
533
+ self.font_size_override.get(),
491
534
  self.text_color_override.get(),
492
535
  self.colors_value.get(),
493
536
  );
@@ -263,6 +263,20 @@ impl Checkbox {
263
263
  }
264
264
  }
265
265
 
266
+ impl LabeledControlTextStyle for Checkbox {
267
+ fn set_label_font_family(&self, family: crate::FontFamily) {
268
+ self.base.font_family(family);
269
+ }
270
+
271
+ fn set_label_font_size(&self, size: f32) {
272
+ self.base.font_size(size);
273
+ }
274
+
275
+ fn set_label_text_color(&self, color: u32) {
276
+ self.base.text_color(color);
277
+ }
278
+ }
279
+
266
280
  impl Node for Checkbox {
267
281
  fn retained_node_ref(&self) -> NodeRef {
268
282
  self.root.retained_node_ref()
@@ -364,7 +364,7 @@ impl ContextMenuEntry {
364
364
  )
365
365
  .text_color(theme.context_menu.item.text_color)
366
366
  .text_overflow(TextOverflow::Ellipsis)
367
- .selectable(false, theme.colors.selection);
367
+ .selectable(false);
368
368
  let shortcut_node = TextNode::new("");
369
369
  shortcut_node
370
370
  .font_family(theme.context_menu.item.font_family.clone())
@@ -376,7 +376,7 @@ impl ContextMenuEntry {
376
376
  )
377
377
  .text_color(theme.colors.text_muted)
378
378
  .text_align(TextAlign::Left)
379
- .selectable(false, theme.colors.selection);
379
+ .selectable(false);
380
380
 
381
381
  root.width(100.0, Unit::Percent)
382
382
  .height(theme.context_menu.item.height, Unit::Pixel)
@@ -118,11 +118,11 @@ impl Dialog {
118
118
  let root = portal();
119
119
  let title_node = TextNode::new("");
120
120
  title_node
121
- .selectable(false, theme.colors.selection)
121
+ .selectable(false)
122
122
  .semantic_role(SemanticRole::Heading);
123
123
  let body_node = TextNode::new("");
124
124
  body_node
125
- .selectable(false, theme.colors.selection)
125
+ .selectable(false)
126
126
  .semantic_role(SemanticRole::StaticText);
127
127
  let content_host = flex_box();
128
128
  content_host
@@ -138,10 +138,9 @@ pub struct DefaultDropdownFieldPresenter {
138
138
 
139
139
  impl DefaultDropdownFieldPresenter {
140
140
  pub fn new(metrics: DropdownFieldMetrics) -> Self {
141
- let theme = crate::theme::current_theme();
142
141
  let value_node = text("");
143
142
  value_node
144
- .selectable(false, theme.colors.selection)
143
+ .selectable(false)
145
144
  .fill_size()
146
145
  .text_limits(0, 1)
147
146
  .wrapping(false);
@@ -93,10 +93,9 @@ pub struct DefaultDropdownOptionRowPresenter {
93
93
 
94
94
  impl DefaultDropdownOptionRowPresenter {
95
95
  pub fn new(metrics: DropdownOptionRowMetrics) -> Self {
96
- let theme = crate::theme::current_theme();
97
96
  let label_node = text("");
98
97
  label_node
99
- .selectable(false, theme.colors.selection)
98
+ .selectable(false)
100
99
  .fill_size()
101
100
  .text_limits(0, 1)
102
101
  .wrapping(false);
@@ -37,7 +37,9 @@ pub(crate) struct PressableLabeledControl {
37
37
  key_pressed_state: Rc<Cell<bool>>,
38
38
  pointer_pressed_state: Rc<Cell<bool>>,
39
39
  enabled_state: Rc<Cell<bool>>,
40
+ label_font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
40
41
  label_font_size_override: Rc<Cell<f32>>,
42
+ label_text_color_override: Rc<Cell<Option<u32>>>,
41
43
  colors_value: Rc<Cell<Option<LabeledControlColors>>>,
42
44
  activation: Rc<RefCell<Option<ActivationCallback>>>,
43
45
  state_changed: Rc<RefCell<Option<StateCallback>>>,
@@ -78,7 +80,9 @@ impl PressableLabeledControl {
78
80
  key_pressed_state: Rc::new(Cell::new(false)),
79
81
  pointer_pressed_state: Rc::new(Cell::new(false)),
80
82
  enabled_state: Rc::new(Cell::new(true)),
83
+ label_font_family_override: Rc::new(RefCell::new(None)),
81
84
  label_font_size_override: Rc::new(Cell::new(0.0)),
85
+ label_text_color_override: Rc::new(Cell::new(None)),
82
86
  colors_value: Rc::new(Cell::new(None)),
83
87
  activation: Rc::new(RefCell::new(None)),
84
88
  state_changed: Rc::new(RefCell::new(None)),
@@ -134,6 +138,21 @@ impl PressableLabeledControl {
134
138
  self.sync_base_theme();
135
139
  }
136
140
 
141
+ pub(crate) fn font_family(&self, family: crate::FontFamily) {
142
+ self.label_font_family_override.replace(Some(family));
143
+ self.sync_base_theme();
144
+ }
145
+
146
+ pub(crate) fn font_size(&self, size: f32) {
147
+ self.label_font_size_override.set(size);
148
+ self.sync_base_theme();
149
+ }
150
+
151
+ pub(crate) fn text_color(&self, color: u32) {
152
+ self.label_text_color_override.set(Some(color));
153
+ self.sync_base_theme();
154
+ }
155
+
137
156
  pub(crate) fn replace_indicator_root(&self, next_root: FlexBox) {
138
157
  if next_root
139
158
  .retained_node_ref()
@@ -251,7 +270,9 @@ impl PressableLabeledControl {
251
270
  key_pressed_state: self.key_pressed_state.clone(),
252
271
  pointer_pressed_state: self.pointer_pressed_state.clone(),
253
272
  enabled_state: self.enabled_state.clone(),
273
+ label_font_family_override: self.label_font_family_override.clone(),
254
274
  label_font_size_override: self.label_font_size_override.clone(),
275
+ label_text_color_override: self.label_text_color_override.clone(),
255
276
  colors_value: self.colors_value.clone(),
256
277
  activation: self.activation.clone(),
257
278
  state_changed: self.state_changed.clone(),
@@ -265,6 +286,8 @@ impl PressableLabeledControl {
265
286
  &self.label_node,
266
287
  &self.gap_node,
267
288
  self.label_font_size_override.get(),
289
+ self.label_font_family_override.borrow().clone(),
290
+ self.label_text_color_override.get(),
268
291
  self.colors_value.get(),
269
292
  self.enabled_state.get(),
270
293
  );
@@ -284,6 +307,8 @@ fn sync_base_theme_parts(
284
307
  label_node: &TextCore,
285
308
  gap_node: &FlexBox,
286
309
  label_font_size_override: f32,
310
+ label_font_family_override: Option<crate::FontFamily>,
311
+ label_text_color_override: Option<u32>,
287
312
  colors: Option<LabeledControlColors>,
288
313
  enabled: bool,
289
314
  ) {
@@ -304,7 +329,7 @@ fn sync_base_theme_parts(
304
329
  root.opacity(if enabled { 1.0 } else { 0.6 });
305
330
  gap_node.width(theme.spacing.sm, Unit::Pixel);
306
331
  label_node
307
- .font_family(theme.fonts.body_family.clone())
332
+ .font_family(label_font_family_override.unwrap_or_else(|| theme.fonts.body_family.clone()))
308
333
  .font_size(if label_font_size_override > 0.0 {
309
334
  label_font_size_override
310
335
  } else {
@@ -321,7 +346,7 @@ fn sync_base_theme_parts(
321
346
  .map(|colors| colors.text_muted_color())
322
347
  .unwrap_or(theme.colors.text_muted)
323
348
  };
324
- label_node.text_color(label_color);
349
+ label_node.text_color(label_text_color_override.unwrap_or(label_color));
325
350
  }
326
351
 
327
352
  fn sync_focus_chrome_parts(root: &FlexBox, focused: bool, enabled: bool) {
@@ -347,7 +372,9 @@ struct PressableLabeledControlEventTarget {
347
372
  key_pressed_state: Rc<Cell<bool>>,
348
373
  pointer_pressed_state: Rc<Cell<bool>>,
349
374
  enabled_state: Rc<Cell<bool>>,
375
+ label_font_family_override: Rc<RefCell<Option<crate::FontFamily>>>,
350
376
  label_font_size_override: Rc<Cell<f32>>,
377
+ label_text_color_override: Rc<Cell<Option<u32>>>,
351
378
  colors_value: Rc<Cell<Option<LabeledControlColors>>>,
352
379
  activation: Rc<RefCell<Option<ActivationCallback>>>,
353
380
  state_changed: Rc<RefCell<Option<StateCallback>>>,
@@ -373,6 +400,8 @@ impl PressableLabeledControlEventTarget {
373
400
  &self.label_node,
374
401
  &self.gap_node,
375
402
  self.label_font_size_override.get(),
403
+ self.label_font_family_override.borrow().clone(),
404
+ self.label_text_color_override.get(),
376
405
  self.colors_value.get(),
377
406
  self.enabled_state.get(),
378
407
  );
@@ -174,7 +174,7 @@ impl TextInputCore {
174
174
  .semantic_role(SemanticRole::Textbox)
175
175
  .reflect_semantic_disabled_from_enabled()
176
176
  .focusable(true, 0)
177
- .selectable(true, 0)
177
+ .selectable(true)
178
178
  .editable(true);
179
179
  let placeholder_text = TextCore::new("");
180
180
  let placeholder_host = flex_box();
@@ -891,7 +891,7 @@ impl TextInputCore {
891
891
  }
892
892
 
893
893
  fn sync_editor_editability(&self) {
894
- self.editor_text.selectable(is_enabled(&self.root), 0);
894
+ self.editor_text.selectable(is_enabled(&self.root));
895
895
  self.editor_text
896
896
  .editable(!self.read_only_value.get() && is_enabled(&self.root));
897
897
  }
@@ -69,6 +69,30 @@ pub struct SliderChangedEventArgs {
69
69
  pub value: f32,
70
70
  }
71
71
 
72
+ pub trait LabeledControlTextStyle: Sized {
73
+ #[doc(hidden)]
74
+ fn set_label_font_family(&self, family: crate::FontFamily);
75
+ #[doc(hidden)]
76
+ fn set_label_font_size(&self, size: f32);
77
+ #[doc(hidden)]
78
+ fn set_label_text_color(&self, color: u32);
79
+
80
+ fn font_family(&self, family: crate::FontFamily) -> &Self {
81
+ self.set_label_font_family(family);
82
+ self
83
+ }
84
+
85
+ fn font_size(&self, size: f32) -> &Self {
86
+ self.set_label_font_size(size);
87
+ self
88
+ }
89
+
90
+ fn text_color(&self, color: u32) -> &Self {
91
+ self.set_label_text_color(color);
92
+ self
93
+ }
94
+ }
95
+
72
96
  #[derive(Clone, Debug, PartialEq, Eq)]
73
97
  pub struct DropdownChangedEventArgs<T> {
74
98
  pub item: T,
@@ -42,6 +42,7 @@ pub struct NavLink {
42
42
  enter_pressed: Rc<Cell<bool>>,
43
43
  enter_pressed_open_in_new_tab: Rc<Cell<bool>>,
44
44
  preview_pinned_for_context_menu: Rc<Cell<bool>>,
45
+ text_color_override: Rc<Cell<Option<u32>>>,
45
46
  theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
46
47
  focus_visibility_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
47
48
  }
@@ -88,6 +89,7 @@ impl NavLink {
88
89
  enter_pressed: Rc::new(Cell::new(false)),
89
90
  enter_pressed_open_in_new_tab: Rc::new(Cell::new(false)),
90
91
  preview_pinned_for_context_menu: Rc::new(Cell::new(false)),
92
+ text_color_override: Rc::new(Cell::new(None)),
91
93
  theme_guard: Rc::new(RefCell::new(None)),
92
94
  focus_visibility_guard: Rc::new(RefCell::new(None)),
93
95
  };
@@ -209,7 +211,7 @@ impl NavLink {
209
211
  fn event_target(&self) -> NavLinkEventTarget {
210
212
  NavLinkEventTarget {
211
213
  weak_root: self.root.downgrade(),
212
- label: self.label.retained_node_ref(),
214
+ label: self.label.clone(),
213
215
  href: self.href.clone(),
214
216
  open_in_new_tab: self.open_in_new_tab.clone(),
215
217
  navigate: self.navigate.clone(),
@@ -220,9 +222,24 @@ impl NavLink {
220
222
  enter_pressed: self.enter_pressed.clone(),
221
223
  enter_pressed_open_in_new_tab: self.enter_pressed_open_in_new_tab.clone(),
222
224
  preview_pinned_for_context_menu: self.preview_pinned_for_context_menu.clone(),
225
+ text_color_override: self.text_color_override.clone(),
223
226
  }
224
227
  }
225
228
 
229
+ fn set_explicit_font_family(&self, family: crate::FontFamily) {
230
+ self.label.font_family(family);
231
+ }
232
+
233
+ fn set_explicit_font_size(&self, size: f32) {
234
+ self.label.font_size(size);
235
+ }
236
+
237
+ fn set_explicit_text_color(&self, color: u32) {
238
+ self.text_color_override.set(Some(color));
239
+ self.label.text_color(color);
240
+ self.sync_visual_state();
241
+ }
242
+
226
243
  pub fn href(&self) -> String {
227
244
  self.href.borrow().clone()
228
245
  }
@@ -281,10 +298,24 @@ impl HasFlexBoxRoot for NavLink {
281
298
  }
282
299
  }
283
300
 
301
+ impl LabeledControlTextStyle for NavLink {
302
+ fn set_label_font_family(&self, family: crate::FontFamily) {
303
+ self.set_explicit_font_family(family);
304
+ }
305
+
306
+ fn set_label_font_size(&self, size: f32) {
307
+ self.set_explicit_font_size(size);
308
+ }
309
+
310
+ fn set_label_text_color(&self, color: u32) {
311
+ self.set_explicit_text_color(color);
312
+ }
313
+ }
314
+
284
315
  #[derive(Clone)]
285
316
  struct NavLinkEventTarget {
286
317
  weak_root: WeakFlexBox,
287
- label: NodeRef,
318
+ label: TextNode,
288
319
  href: Rc<RefCell<String>>,
289
320
  open_in_new_tab: Rc<Cell<bool>>,
290
321
  navigate: Rc<RefCell<Option<NavigateCallback>>>,
@@ -295,6 +326,7 @@ struct NavLinkEventTarget {
295
326
  enter_pressed: Rc<Cell<bool>>,
296
327
  enter_pressed_open_in_new_tab: Rc<Cell<bool>>,
297
328
  preview_pinned_for_context_menu: Rc<Cell<bool>>,
329
+ text_color_override: Rc<Cell<Option<u32>>>,
298
330
  }
299
331
 
300
332
  impl NavLinkEventTarget {
@@ -371,14 +403,15 @@ impl NavLinkEventTarget {
371
403
  }
372
404
 
373
405
  fn sync_visual_state(&self) {
374
- let color = if self.hovered.get() {
375
- current_theme().colors.accent_hovered
376
- } else {
377
- current_theme().colors.accent
378
- };
379
- let label = &self.label;
380
- if label.handle() != NodeHandle::INVALID {
381
- ui::set_text_color(label.handle().raw(), color);
406
+ let color = self.text_color_override.get().unwrap_or_else(|| {
407
+ if self.hovered.get() {
408
+ current_theme().colors.accent_hovered
409
+ } else {
410
+ current_theme().colors.accent
411
+ }
412
+ });
413
+ if self.label.handle() != NodeHandle::INVALID {
414
+ ui::set_text_color(self.label.handle().raw(), color);
382
415
  }
383
416
  }
384
417
 
@@ -195,6 +195,20 @@ impl RadioButton {
195
195
  }
196
196
  }
197
197
 
198
+ impl LabeledControlTextStyle for RadioButton {
199
+ fn set_label_font_family(&self, family: crate::FontFamily) {
200
+ self.base.font_family(family);
201
+ }
202
+
203
+ fn set_label_font_size(&self, size: f32) {
204
+ self.base.font_size(size);
205
+ }
206
+
207
+ fn set_label_text_color(&self, color: u32) {
208
+ self.base.text_color(color);
209
+ }
210
+ }
211
+
198
212
  impl Node for RadioButton {
199
213
  fn retained_node_ref(&self) -> NodeRef {
200
214
  self.root.retained_node_ref()
@@ -186,6 +186,20 @@ impl Switch {
186
186
  }
187
187
  }
188
188
 
189
+ impl LabeledControlTextStyle for Switch {
190
+ fn set_label_font_family(&self, family: crate::FontFamily) {
191
+ self.base.font_family(family);
192
+ }
193
+
194
+ fn set_label_font_size(&self, size: f32) {
195
+ self.base.font_size(size);
196
+ }
197
+
198
+ fn set_label_text_color(&self, color: u32) {
199
+ self.base.text_color(color);
200
+ }
201
+ }
202
+
189
203
  impl Node for Switch {
190
204
  fn retained_node_ref(&self) -> NodeRef {
191
205
  self.root.retained_node_ref()
package/src/lib.rs CHANGED
@@ -314,6 +314,31 @@ macro_rules! ui {
314
314
  };
315
315
  }
316
316
 
317
+ #[macro_export]
318
+ macro_rules! fui_component {
319
+ ($component:ty => $root:ident) => {
320
+ impl $crate::Node for $component {
321
+ fn retained_node_ref(&self) -> $crate::node::NodeRef {
322
+ $crate::Node::retained_node_ref(&self.$root)
323
+ }
324
+
325
+ fn retained_owner_attachment(&self) -> Option<std::rc::Rc<dyn std::any::Any>> {
326
+ $crate::Node::retained_owner_attachment(&self.$root)
327
+ }
328
+
329
+ fn build_self(&self) {
330
+ $crate::Node::build_self(&self.$root);
331
+ }
332
+ }
333
+
334
+ impl $crate::HasFlexBoxRoot for $component {
335
+ fn flex_box_root(&self) -> &$crate::FlexBox {
336
+ $crate::HasFlexBoxRoot::flex_box_root(&self.$root)
337
+ }
338
+ }
339
+ };
340
+ }
341
+
317
342
  pub mod prelude {
318
343
  pub use crate::animation::{
319
344
  animate_color, animate_color_with, animate_float, animate_float_with,
@@ -345,19 +370,19 @@ pub mod prelude {
345
370
  DropdownChevronVisualState, DropdownColors, DropdownFieldMetrics, DropdownFieldPresenter,
346
371
  DropdownFieldTemplate, DropdownFieldVisualState, DropdownItem, DropdownOptionRowMetrics,
347
372
  DropdownOptionRowPresenter, DropdownOptionRowTemplate, DropdownOptionRowVisualState,
348
- DropdownSizing, Form, LabeledControlColors, LabeledControlSizing, MenuItem, NavLink,
349
- NavigateEventArgs, Popup, PressableIndicatorMetrics, PressableIndicatorPresenter,
350
- PressableIndicatorVisualState, ProgressBar, RadioButton, RadioButtonChangedEventArgs,
351
- RadioGroup, RadioGroupChangedEventArgs, RadioIndicatorPresenter, RadioIndicatorTemplate,
352
- RadioIndicatorVisualState, SelectionArea, Slider, SliderChangedEventArgs, SliderColors,
353
- SliderPresenter, SliderPresenterMetrics, SliderSizing, SliderTemplate, SliderVisualState,
354
- Switch, SwitchChangedEventArgs, SwitchIndicatorPresenter, SwitchIndicatorTemplate,
355
- SwitchIndicatorVisualState, TextArea, TextInput, TextInputColors, TextInputPresenter,
356
- TextInputTemplate, TextInputVisualState, DEFAULT_BUTTON_TEMPLATE,
357
- DEFAULT_CHECKBOX_INDICATOR_TEMPLATE, DEFAULT_DROPDOWN_CHEVRON_TEMPLATE,
358
- DEFAULT_DROPDOWN_FIELD_TEMPLATE, DEFAULT_DROPDOWN_OPTION_ROW_TEMPLATE,
359
- DEFAULT_RADIO_INDICATOR_TEMPLATE, DEFAULT_SLIDER_TEMPLATE,
360
- DEFAULT_SWITCH_INDICATOR_TEMPLATE, DEFAULT_TEXT_INPUT_TEMPLATE,
373
+ DropdownSizing, Form, LabeledControlColors, LabeledControlSizing, LabeledControlTextStyle,
374
+ MenuItem, NavLink, NavigateEventArgs, Popup, PressableIndicatorMetrics,
375
+ PressableIndicatorPresenter, PressableIndicatorVisualState, ProgressBar, RadioButton,
376
+ RadioButtonChangedEventArgs, RadioGroup, RadioGroupChangedEventArgs,
377
+ RadioIndicatorPresenter, RadioIndicatorTemplate, RadioIndicatorVisualState, SelectionArea,
378
+ Slider, SliderChangedEventArgs, SliderColors, SliderPresenter, SliderPresenterMetrics,
379
+ SliderSizing, SliderTemplate, SliderVisualState, Switch, SwitchChangedEventArgs,
380
+ SwitchIndicatorPresenter, SwitchIndicatorTemplate, SwitchIndicatorVisualState, TextArea,
381
+ TextInput, TextInputColors, TextInputPresenter, TextInputTemplate, TextInputVisualState,
382
+ DEFAULT_BUTTON_TEMPLATE, DEFAULT_CHECKBOX_INDICATOR_TEMPLATE,
383
+ DEFAULT_DROPDOWN_CHEVRON_TEMPLATE, DEFAULT_DROPDOWN_FIELD_TEMPLATE,
384
+ DEFAULT_DROPDOWN_OPTION_ROW_TEMPLATE, DEFAULT_RADIO_INDICATOR_TEMPLATE,
385
+ DEFAULT_SLIDER_TEMPLATE, DEFAULT_SWITCH_INDICATOR_TEMPLATE, DEFAULT_TEXT_INPUT_TEMPLATE,
361
386
  };
362
387
  pub use crate::drag_drop::{
363
388
  DragCompletedEventArgs, DragDataObject, DragDropEffects, DragEventArgs, DragSession,
@@ -386,6 +411,7 @@ pub mod prelude {
386
411
  FileWorkerProcessResult, FileWriteProgress,
387
412
  };
388
413
  pub use crate::focus_visibility::show_keyboard_focus_for_key_event;
414
+ pub use crate::fui_component;
389
415
  pub use crate::image_sampling::{ImageSampling, ImageSamplingMode};
390
416
  pub use crate::logger;
391
417
  pub use crate::navigation;
@@ -174,7 +174,7 @@ fn create_label(label: &str, slot: i32) -> TextNode {
174
174
  .font_size(theme.context_menu.item.font_size)
175
175
  .text_color(theme.context_menu.item.text_color)
176
176
  .text_overflow(TextOverflow::Ellipsis)
177
- .selectable(false, theme.colors.selection)
177
+ .selectable(false)
178
178
  .interactive(true)
179
179
  .preserve_selection_on_pointer_down(true)
180
180
  .on_pointer_up(move |event| {
package/src/node/core.rs CHANGED
@@ -514,7 +514,7 @@ impl NodeRef {
514
514
  }
515
515
  }
516
516
 
517
- pub(crate) fn from_node<T: Node>(inner: Rc<RefCell<NodeCore>>, node: T) -> Self {
517
+ pub(crate) fn from_node<T: Node + 'static>(inner: Rc<RefCell<NodeCore>>, node: T) -> Self {
518
518
  Self {
519
519
  inner,
520
520
  build_callback: Some(Rc::new(move || node.build())),
@@ -1561,7 +1561,7 @@ impl NodeRef {
1561
1561
  }
1562
1562
  }
1563
1563
 
1564
- pub trait Node: Clone + 'static {
1564
+ pub trait Node: Clone {
1565
1565
  #[doc(hidden)]
1566
1566
  fn node_ref(&self) -> NodeRef {
1567
1567
  self.retained_node_ref()
@@ -2208,3 +2208,17 @@ pub trait Node: Clone + 'static {
2208
2208
  self
2209
2209
  }
2210
2210
  }
2211
+
2212
+ impl<T: Node + ?Sized> Node for &T {
2213
+ fn retained_node_ref(&self) -> NodeRef {
2214
+ (*self).retained_node_ref()
2215
+ }
2216
+
2217
+ fn retained_owner_attachment(&self) -> Option<Rc<dyn Any>> {
2218
+ (*self).retained_owner_attachment()
2219
+ }
2220
+
2221
+ fn build_self(&self) {
2222
+ (*self).build_self();
2223
+ }
2224
+ }
package/src/node/mod.rs CHANGED
@@ -32,8 +32,10 @@ mod svg_node;
32
32
  mod text_node;
33
33
  mod virtual_list;
34
34
 
35
+ #[doc(hidden)]
36
+ pub use core::NodeRef;
35
37
  pub use core::{ContextMenuEventArgs, Node, NodeHandle};
36
- pub(crate) use core::{NodeRef, WeakFlexBox, WeakNodeRef};
38
+ pub(crate) use core::{WeakFlexBox, WeakNodeRef};
37
39
  pub use custom_drawable::CustomDrawable;
38
40
  pub use flex_box::{Border, FlexBox, GradientStop};
39
41
  pub use grid::{Grid, GridTrack};
@@ -59,7 +61,25 @@ pub trait HasFlexBoxRoot {
59
61
  fn flex_box_root(&self) -> &FlexBox;
60
62
  }
61
63
 
64
+ impl HasFlexBoxRoot for FlexBox {
65
+ fn flex_box_root(&self) -> &FlexBox {
66
+ self
67
+ }
68
+ }
69
+
62
70
  pub trait FlexBoxSurface: HasFlexBoxRoot {
71
+ fn bind_theme(&self, handler: impl Fn(&FlexBox, crate::theme::Theme) + 'static) -> &Self {
72
+ let root = self.flex_box_root();
73
+ let weak_root = root.downgrade();
74
+ let guard = crate::theme::subscribe(move |theme| {
75
+ if let Some(root) = weak_root.upgrade() {
76
+ handler(&root, theme);
77
+ }
78
+ });
79
+ root.retained_node_ref().retain_attachment(Rc::new(guard));
80
+ self
81
+ }
82
+
63
83
  fn width(&self, width: f32, unit: Unit) -> &Self {
64
84
  self.flex_box_root().width(width, unit);
65
85
  self
@@ -81,6 +81,19 @@ impl TextNode {
81
81
  self
82
82
  }
83
83
 
84
+ pub fn bind_theme(&self, handler: impl Fn(&TextNode, crate::theme::Theme) + 'static) -> &Self {
85
+ let weak_core = Rc::downgrade(&self.core);
86
+ let weak_props = Rc::downgrade(&self.props);
87
+ let guard = crate::theme::subscribe(move |theme| {
88
+ let (Some(core), Some(props)) = (weak_core.upgrade(), weak_props.upgrade()) else {
89
+ return;
90
+ };
91
+ handler(&TextNode { core, props }, theme);
92
+ });
93
+ self.retained_node_ref().retain_attachment(Rc::new(guard));
94
+ self
95
+ }
96
+
84
97
  pub fn width(&self, width: f32, unit: Unit) -> &Self {
85
98
  self.props.borrow_mut().width = Some((width, unit));
86
99
  {
@@ -281,15 +294,13 @@ impl TextNode {
281
294
  self
282
295
  }
283
296
 
284
- pub fn selectable(&self, selectable: bool, selection_color: u32) -> &Self {
285
- let resolved_selection_color = if selection_color == 0 {
286
- theme::current_theme().colors.selection
287
- } else {
288
- selection_color
289
- };
297
+ pub fn selectable(&self, selectable: bool) -> &Self {
290
298
  let mut props = self.props.borrow_mut();
299
+ let resolved_selection_color = props
300
+ .selectable
301
+ .map(|(_, color)| color)
302
+ .unwrap_or_else(|| theme::current_theme().colors.selection);
291
303
  props.selectable = Some((selectable, resolved_selection_color));
292
- props.uses_theme_selection_color = selection_color == 0;
293
304
  drop(props);
294
305
  self.core.borrow_mut().behavior.selectable_text = selectable;
295
306
  if self.has_built_handle() {
@@ -299,6 +310,19 @@ impl TextNode {
299
310
  self
300
311
  }
301
312
 
313
+ pub fn selection_color(&self, color: u32) -> &Self {
314
+ let mut props = self.props.borrow_mut();
315
+ let selectable = props.selectable.map(|(value, _)| value).unwrap_or(false);
316
+ props.selectable = Some((selectable, color));
317
+ props.uses_theme_selection_color = false;
318
+ drop(props);
319
+ if self.has_built_handle() {
320
+ ui::set_selectable(self.handle().raw(), selectable, color);
321
+ self.notify_retained_mutation();
322
+ }
323
+ self
324
+ }
325
+
302
326
  pub fn editable(&self, editable: bool) -> &Self {
303
327
  if editable {
304
328
  let selection_color = self