@effindomv2/fui-rs 0.1.20 → 0.1.22

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.20"
3
+ version = "0.1.22"
4
4
  edition = "2021"
5
5
  license = "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial"
6
6
 
package/README.md CHANGED
@@ -7,27 +7,35 @@ macros for browser-hosted Rust WASM apps.
7
7
 
8
8
  ## Quickstart
9
9
 
10
+ Create a FUI-RS application with the published scaffolder:
11
+
10
12
  ```bash
11
- # Prerequisites: Rust + wasm32-unknown-unknown target + Binaryen
13
+ # Install Rust and the WebAssembly target once
12
14
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
15
+ source "$HOME/.cargo/env"
13
16
  rustup target add wasm32-unknown-unknown
14
- brew install binaryen
15
-
16
- # Clone and build
17
- git clone https://github.com/zion-sati/fui-rs.git
18
- cd fui-rs
19
- npm ci
20
- npm run build
21
- npm run serve
17
+
18
+ # Create and run an app
19
+ npx @effindomv2/create-fui-rs-app my-app
20
+ cd my-app
21
+ npm install
22
+ npm run dev
22
23
  ```
23
24
 
24
- Open:
25
+ For a routed application with one independently built WASM module per route:
25
26
 
26
- ```text
27
- http://127.0.0.1:8080/v2/fui-rs/demo/index.html
27
+ ```bash
28
+ npx @effindomv2/create-fui-rs-app my-routed-app -- --template routed
29
+ cd my-routed-app
30
+ npm install
31
+ npm run dev
28
32
  ```
29
33
 
30
- Full quickstart: [docs/v2/fui-rs/QUICKSTART.md](../../docs/v2/fui-rs/QUICKSTART.md)
34
+ Install [Binaryen](https://github.com/WebAssembly/binaryen) to make release
35
+ builds run `wasm-opt`. Development builds do not require it.
36
+
37
+ Application setup, retained-mode guidance, and entrypoint examples are covered
38
+ in the [FUI-RS developer quickstart](../../docs/v2/fui-rs/QUICKSTART.md).
31
39
 
32
40
  ## Minimal app
33
41
 
@@ -46,6 +54,23 @@ fn build_page() -> FlexBox {
46
54
  fui_app!(FlexBox, build_page);
47
55
  ```
48
56
 
57
+ ## Rich text
58
+
59
+ Use `rich_text!` to create retained rich text without manually constructing a
60
+ span vector. String literals become spans, braced expressions provide dynamic
61
+ text, and `span => expression` accepts an existing `RichTextSpan`:
62
+
63
+ ```rust
64
+ let value = 42;
65
+ let suffix = span("!").underline();
66
+ let label = rich_text![
67
+ "Current value: ".italic(),
68
+ { format!("{value}") }.bold().text_color(rgb(0x3a, 0xc5, 0x6c)),
69
+ span => suffix,
70
+ ]
71
+ .font_size(18.0);
72
+ ```
73
+
49
74
  ## SDK docs
50
75
 
51
76
  - [SDK docs index](../../docs/v2/fui-rs/SDK_INDEX.md)
@@ -56,6 +81,14 @@ fui_app!(FlexBox, build_page);
56
81
  - [Forms and autofill](../../docs/v2/fui-rs/FORMS_AND_AUTOFILL.md)
57
82
  - [Theming and style matrix](../../docs/v2/fui-rs/THEMING_STYLE_MATRIX.md)
58
83
 
84
+ ## Contributing to FUI-RS
85
+
86
+ The commands above are for developers building applications with the published
87
+ FUI-RS SDK. Contributors working on the SDK, EffinDom runtime, browser bridge,
88
+ or repository demos should follow the
89
+ [FUI-RS contributor quickstart](../../docs/v2/fui-rs/CONTRIBUTOR_QUICKSTART.md).
90
+ It covers the standalone repository toolchain, SDK build, lint, and test lanes.
91
+
59
92
  ## What is included
60
93
 
61
94
  | Area | Status |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-rs",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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",
@@ -33,11 +33,12 @@
33
33
  "generate:host": "npm run generate:host-services && npm run generate:host-events:home && npm run generate:host-events:workbench && npm run generate:host-events:stage4 && npm run generate:host-events:stage5 && npm run generate:host-events:immediate-drawing && npm run generate:worker-host-services && npm run generate:framework-host-services",
34
34
  "guard:runtime-dependency": "bash scripts/check-runtime-dependency.sh",
35
35
  "lint": "cd ../.. && npm run lint:v2",
36
+ "lint:rust": "bash -lc 'source \"$HOME/.cargo/env\" && cargo clippy --manifest-path Cargo.toml --all-targets -- -D warnings'",
36
37
  "build": "npm run guard:runtime-dependency && npm run check:abi && bash scripts/build.sh",
37
38
  "test:unit": "npm run guard:runtime-dependency && npm run check:abi && npm run generate:host && bash -lc 'source \"$HOME/.cargo/env\" && cargo test --manifest-path Cargo.toml'",
38
39
  "test:integration": "npm --prefix ../.. run build:v2:browser-bridge && npm run build && playwright test -c playwright.config.ts",
39
40
  "typecheck": "npm run guard:runtime-dependency && npm run check:abi && tsc -p tsconfig.json --noEmit",
40
- "prepublishOnly": "npm run check:abi && npm run lint && npm run typecheck"
41
+ "prepublishOnly": "npm run check:abi && npm run lint && npm run lint:rust && npm run typecheck"
41
42
  },
42
43
  "dependencies": {
43
44
  "@effindomv2/runtime": "0.1.26"
package/src/app.rs CHANGED
@@ -3,7 +3,7 @@ use crate::context_menu_manager;
3
3
  use crate::ffi::HandleValue;
4
4
  use crate::frame_scheduler;
5
5
  use crate::mobile_text_selection_toolbar;
6
- use crate::node::{flex_box, FlexBox, Node, NodeRef};
6
+ use crate::node::{flex_box, FlexBox, Node, NodeRef, ThemeBindable};
7
7
  use crate::panic_hook;
8
8
  use crate::selection_handle_adorner;
9
9
  use crate::theme;
@@ -95,6 +95,9 @@ fn application_shell<T: Node>(root: &T) -> FlexBox {
95
95
  .child(&focus_adorner::create_default_host())
96
96
  .child(&tool_tip_manager::ToolTipManager::create_default_host())
97
97
  .child(&context_menu_manager::create_default_menu());
98
+ shell.bind_theme(|shell, theme| {
99
+ shell.bg_color(theme.colors.background);
100
+ });
98
101
  shell
99
102
  }
100
103
 
package/src/color.rs CHANGED
@@ -106,7 +106,10 @@ mod tests {
106
106
  #[test]
107
107
  fn color_helpers_mask_channels_and_interpolate() {
108
108
  assert_eq!(rgba(0x112, 0x134, 0x156, 0x178), 0x12345678);
109
- assert_eq!(mix_color(rgb(0, 0, 0), rgb(255, 255, 255), 0.5), rgba(128, 128, 128, 255));
109
+ assert_eq!(
110
+ mix_color(rgb(0, 0, 0), rgb(255, 255, 255), 0.5),
111
+ rgba(128, 128, 128, 255)
112
+ );
110
113
  assert_eq!(hsl_to_color(0.0, 1.0, 0.5), rgb(255, 0, 0));
111
114
  }
112
115
  }
@@ -1,6 +1,6 @@
1
1
  use super::ContextMenuAppearance;
2
2
  use crate::bindings::ui;
3
- use crate::event::{self, PointerEventArgs, PointerType};
3
+ use crate::event::{self, PointerButton, PointerEventArgs, PointerType};
4
4
  use crate::ffi::{
5
5
  BorderStyle, CursorStyle, HandleValue, KeyEventType, SemanticRole, TextAlign, TextOverflow,
6
6
  Unit,
@@ -29,7 +29,7 @@ thread_local! {
29
29
  }
30
30
 
31
31
  fn is_primary_activation_pointer(event: &PointerEventArgs) -> bool {
32
- event.button == 0
32
+ event.button == PointerButton::Primary
33
33
  || event.pointer_type == PointerType::Touch
34
34
  || event.pointer_type == PointerType::Pen
35
35
  }
@@ -1,6 +1,6 @@
1
1
  use super::*;
2
2
  use crate::bindings::ui;
3
- use crate::event::PointerType;
3
+ use crate::event::{PointerButton, PointerType};
4
4
  use crate::ffi::{CursorStyle, HandleValue, SemanticRole};
5
5
  use crate::navigation;
6
6
  use crate::node::{text, NodeHandle, NodeRef, WeakFlexBox};
@@ -14,8 +14,13 @@ thread_local! {
14
14
  static ACTIVE_PREVIEW_OWNER: Cell<u64> = const { Cell::new(HandleValue::Invalid as u64) };
15
15
  }
16
16
 
17
- fn is_primary_activation_pointer(event: &PointerEventArgs) -> bool {
18
- event.button == 0
17
+ fn is_middle_mouse_button(event: &PointerEventArgs) -> bool {
18
+ event.pointer_type == PointerType::Mouse && event.button == PointerButton::Auxiliary
19
+ }
20
+
21
+ fn is_activation_pointer(event: &PointerEventArgs) -> bool {
22
+ event.button == PointerButton::Primary
23
+ || is_middle_mouse_button(event)
19
24
  || event.pointer_type == PointerType::Touch
20
25
  || event.pointer_type == PointerType::Pen
21
26
  }
@@ -126,21 +131,22 @@ impl NavLink {
126
131
  });
127
132
  let target = self.event_target();
128
133
  self.root.on_pointer_down(move |event| {
129
- if !is_primary_activation_pointer(event) {
134
+ if !is_activation_pointer(event) {
130
135
  target.pointer_pressed.set(false);
131
136
  target.pointer_pressed_open_in_new_tab.set(false);
132
137
  return;
133
138
  }
134
139
  target.pointer_pressed.set(true);
135
- target
136
- .pointer_pressed_open_in_new_tab
137
- .set(target.should_open_in_new_tab(event.modifiers));
140
+ target.pointer_pressed_open_in_new_tab.set(
141
+ is_middle_mouse_button(event) || target.should_open_in_new_tab(event.modifiers),
142
+ );
138
143
  event.handled = true;
139
144
  });
140
145
  let target = self.event_target();
141
146
  self.root.on_pointer_up(move |event| {
142
- if target.pointer_pressed.replace(false) && is_primary_activation_pointer(event) {
147
+ if target.pointer_pressed.replace(false) && is_activation_pointer(event) {
143
148
  let open_in_new_tab = target.pointer_pressed_open_in_new_tab.get()
149
+ || is_middle_mouse_button(event)
144
150
  || target.should_open_in_new_tab(event.modifiers);
145
151
  target.activate(open_in_new_tab);
146
152
  event.handled = true;
@@ -442,7 +448,9 @@ impl NavLinkEventTarget {
442
448
  let color = if self.hovered.get() {
443
449
  theme.colors.accent_hovered
444
450
  } else {
445
- self.text_color_override.get().unwrap_or(theme.colors.accent)
451
+ self.text_color_override
452
+ .get()
453
+ .unwrap_or(theme.colors.accent)
446
454
  };
447
455
  if self.label.handle() != NodeHandle::INVALID {
448
456
  ui::set_text_color(self.label.handle().raw(), color);
@@ -528,7 +528,7 @@ impl SliderEventTarget {
528
528
  }
529
529
 
530
530
  fn handle_pointer_move(&self, event: &mut PointerEventArgs) {
531
- if !self.is_enabled() || !self.dragging_state.get() || event.buttons == 0 {
531
+ if !self.is_enabled() || !self.dragging_state.get() || event.buttons.is_empty() {
532
532
  return;
533
533
  }
534
534
  self.update_value_from_pointer(event.scene_x, event.scene_y, true, true);
package/src/event.rs CHANGED
@@ -43,6 +43,58 @@ impl PointerType {
43
43
  }
44
44
  }
45
45
 
46
+ #[repr(i32)]
47
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
48
+ pub enum PointerButton {
49
+ None = -1,
50
+ Primary = 0,
51
+ Auxiliary = 1,
52
+ Secondary = 2,
53
+ Back = 3,
54
+ Forward = 4,
55
+ }
56
+
57
+ impl PointerButton {
58
+ pub fn from_raw(value: i32) -> Self {
59
+ match value {
60
+ 0 => Self::Primary,
61
+ 1 => Self::Auxiliary,
62
+ 2 => Self::Secondary,
63
+ 3 => Self::Back,
64
+ 4 => Self::Forward,
65
+ _ => Self::None,
66
+ }
67
+ }
68
+ }
69
+
70
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
71
+ pub struct PointerButtons(u32);
72
+
73
+ impl PointerButtons {
74
+ pub const NONE: Self = Self(0);
75
+ pub const PRIMARY: Self = Self(1 << 0);
76
+ pub const SECONDARY: Self = Self(1 << 1);
77
+ pub const AUXILIARY: Self = Self(1 << 2);
78
+ pub const BACK: Self = Self(1 << 3);
79
+ pub const FORWARD: Self = Self(1 << 4);
80
+
81
+ pub const fn from_raw(value: u32) -> Self {
82
+ Self(value)
83
+ }
84
+
85
+ pub const fn bits(self) -> u32 {
86
+ self.0
87
+ }
88
+
89
+ pub const fn is_empty(self) -> bool {
90
+ self.0 == 0
91
+ }
92
+
93
+ pub const fn contains(self, buttons: Self) -> bool {
94
+ self.0 & buttons.0 == buttons.0
95
+ }
96
+ }
97
+
46
98
  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
47
99
  pub enum GestureIntent {
48
100
  None = 0,
@@ -109,8 +161,8 @@ pub struct PointerEventArgs {
109
161
  pub modifiers: u32,
110
162
  pub pointer_id: i32,
111
163
  pub pointer_type: PointerType,
112
- pub button: i32,
113
- pub buttons: u32,
164
+ pub button: PointerButton,
165
+ pub buttons: PointerButtons,
114
166
  pub pressure: f32,
115
167
  pub width: f32,
116
168
  pub height: f32,
@@ -145,8 +197,8 @@ impl PointerEventArgs {
145
197
  modifiers,
146
198
  pointer_id,
147
199
  pointer_type,
148
- button,
149
- buttons,
200
+ button: PointerButton::from_raw(button),
201
+ buttons: PointerButtons::from_raw(buttons),
150
202
  pressure,
151
203
  width,
152
204
  height,
package/src/lib.rs CHANGED
@@ -4,9 +4,9 @@ pub mod assets;
4
4
  #[doc(hidden)]
5
5
  pub mod bindings;
6
6
  pub mod bitmap;
7
- pub mod color;
8
7
  #[doc(hidden)]
9
8
  pub mod bridge_callbacks;
9
+ pub mod color;
10
10
  pub(crate) mod context_menu_manager;
11
11
  pub mod controls;
12
12
  pub mod debug;
@@ -63,6 +63,45 @@ macro_rules! children {
63
63
  };
64
64
  }
65
65
 
66
+ #[doc(hidden)]
67
+ #[macro_export]
68
+ macro_rules! __fui_rs_rich_text_spans {
69
+ (@collect [$($span:expr,)*]) => {
70
+ vec![$($span,)*]
71
+ };
72
+ (@collect [$($span:expr,)*] , $($rest:tt)*) => {
73
+ $crate::__fui_rs_rich_text_spans!(@collect [$($span,)*] $($rest)*)
74
+ };
75
+ (@collect [$($span:expr,)*] span => $value:expr, $($rest:tt)*) => {
76
+ $crate::__fui_rs_rich_text_spans!(@collect [$($span,)* $value,] $($rest)*)
77
+ };
78
+ (@collect [$($span:expr,)*] { $text:expr } $(.$method:ident($($argument:expr),* $(,)?))* , $($rest:tt)*) => {
79
+ $crate::__fui_rs_rich_text_spans!(@collect [
80
+ $($span,)*
81
+ $crate::text::span($text)$(.$method($($argument),*))*,
82
+ ] $($rest)*)
83
+ };
84
+ (@collect [$($span:expr,)*] $text:literal $(.$method:ident($($argument:expr),* $(,)?))* , $($rest:tt)*) => {
85
+ $crate::__fui_rs_rich_text_spans!(@collect [
86
+ $($span,)*
87
+ $crate::text::span($text)$(.$method($($argument),*))*,
88
+ ] $($rest)*)
89
+ };
90
+ }
91
+
92
+ /// Builds retained rich text with fluent span styling.
93
+ ///
94
+ /// String literals become spans automatically. Wrap a dynamic text expression
95
+ /// in braces, or use `span => expression` to provide a prebuilt span.
96
+ #[macro_export]
97
+ macro_rules! rich_text {
98
+ ($($span:tt)*) => {
99
+ $crate::text::RichText::new(
100
+ $crate::__fui_rs_rich_text_spans!(@collect [] $($span)* ,)
101
+ )
102
+ };
103
+ }
104
+
66
105
  pub trait Configure: Sized {
67
106
  fn configure(self, configure: impl FnOnce(&Self)) -> Self {
68
107
  configure(&self);
@@ -456,8 +495,9 @@ pub mod prelude {
456
495
  pub use crate::drawing::{DrawContext, Paint, Path};
457
496
  pub use crate::event::{
458
497
  FocusChangedEventArgs, GestureEventArgs, GestureEventKind, GestureEventPhase,
459
- GestureIntent, KeyEventArgs, LongPressEventArgs, PointerEventArgs, PointerType,
460
- SelectionChangedEventArgs, TextChangedEventArgs, WheelEventArgs,
498
+ GestureIntent, KeyEventArgs, LongPressEventArgs, PointerButton, PointerButtons,
499
+ PointerEventArgs, PointerType, SelectionChangedEventArgs, TextChangedEventArgs,
500
+ WheelEventArgs,
461
501
  };
462
502
  pub use crate::external_drop::{
463
503
  ExternalDropEventArgs, ExternalDropItemInfo, ExternalDropItemKind,
@@ -524,7 +564,7 @@ pub mod prelude {
524
564
  pub use crate::worker_job::{WorkerJob, WorkerJobState};
525
565
  #[cfg(feature = "worker-runtime")]
526
566
  pub use crate::worker_runtime::{file_read_chunk, file_worker_write_chunk, WorkerRuntime};
527
- pub use crate::{children, fui_app, fui_managed_app, ui, Configure};
567
+ pub use crate::{children, fui_app, fui_managed_app, rich_text, ui, Configure};
528
568
  }
529
569
 
530
570
  pub use animation::{
@@ -581,8 +621,8 @@ pub use drag_drop::{
581
621
  pub use drawing::{DrawContext, Paint, Path};
582
622
  pub use event::{
583
623
  FocusChangedEventArgs, GestureEventArgs, GestureEventKind, GestureEventPhase, GestureIntent,
584
- KeyEventArgs, LongPressEventArgs, PointerEventArgs, PointerType, SelectionChangedEventArgs,
585
- TextChangedEventArgs, WheelEventArgs,
624
+ KeyEventArgs, LongPressEventArgs, PointerButton, PointerButtons, PointerEventArgs, PointerType,
625
+ SelectionChangedEventArgs, TextChangedEventArgs, WheelEventArgs,
586
626
  };
587
627
  pub use external_drop::{ExternalDropEventArgs, ExternalDropItemInfo, ExternalDropItemKind};
588
628
  pub use fetch::{Fetch, FetchErrorEventArgs, FetchRequest, FetchResponse};
package/src/node/core.rs CHANGED
@@ -3,7 +3,7 @@ use crate::drag_drop::{
3
3
  DragCompletedEventArgs, DragDataObject, DragDropEffects, DragEventArgs, DropProposal,
4
4
  };
5
5
  use crate::drag_gesture::{DragCompletedEvent, DragGesture, DragStartedEvent};
6
- use crate::event::{SelectionChangedEventArgs, TextChangedEventArgs};
6
+ use crate::event::{PointerButton, SelectionChangedEventArgs, TextChangedEventArgs};
7
7
  use crate::external_drop::ExternalDropEventArgs;
8
8
  use crate::persisted::PersistedStateAdapter;
9
9
  use crate::tool_tip::ToolTip;
@@ -31,7 +31,7 @@ pub(crate) type ExternalDragEventCallback = Rc<dyn Fn(ExternalDropEventArgs)>;
31
31
  pub(crate) type EffectiveEnabledChangedCallback = Rc<dyn Fn(bool)>;
32
32
 
33
33
  pub(crate) fn is_primary_activation_pointer(event: &PointerEventArgs) -> bool {
34
- event.button == 0
34
+ event.button == PointerButton::Primary
35
35
  || event.pointer_type == PointerType::Touch
36
36
  || event.pointer_type == PointerType::Pen
37
37
  }
@@ -513,11 +513,7 @@ impl NodeRef {
513
513
  self.inner.borrow().handle
514
514
  }
515
515
 
516
- pub(crate) fn set_semantic_checked(
517
- &self,
518
- state: SemanticCheckedState,
519
- announce: bool,
520
- ) {
516
+ pub(crate) fn set_semantic_checked(&self, state: SemanticCheckedState, announce: bool) {
521
517
  let handle = {
522
518
  let mut core = self.inner.borrow_mut();
523
519
  core.behavior.semantic_checked = Some(state);
package/src/theme.rs CHANGED
@@ -1,7 +1,7 @@
1
- use crate::generated::framework_host_services;
2
1
  use crate::color::{
3
2
  color_alpha, color_blue, color_green, color_red, mix_color, rgb, rgba, with_alpha,
4
3
  };
4
+ use crate::generated::framework_host_services;
5
5
  use crate::signal::{Callback, Signal, SubscriptionGuard};
6
6
  use crate::typography::{FontFamily, FontStack};
7
7
  use std::cell::RefCell;