@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,229 @@
1
+ use super::Button;
2
+ use crate::event;
3
+ use crate::ffi::{FlexDirection, KeyEventType, SemanticRole};
4
+ use crate::node::{flex_box, Child, FlexBox, Node, NodeRef};
5
+ use std::any::Any;
6
+ use std::cell::{Cell, RefCell};
7
+ use std::rc::Rc;
8
+
9
+ #[derive(Clone)]
10
+ pub struct Form {
11
+ shared: Rc<FormShared>,
12
+ }
13
+
14
+ struct FormShared {
15
+ root: FlexBox,
16
+ default_button: RefCell<Option<Button>>,
17
+ cancel_button: RefCell<Option<Button>>,
18
+ key_filter_token: Cell<u32>,
19
+ armed_key: RefCell<Option<String>>,
20
+ armed_button: RefCell<Option<Button>>,
21
+ }
22
+
23
+ impl Default for Form {
24
+ fn default() -> Self {
25
+ Self::new()
26
+ }
27
+ }
28
+
29
+ impl Form {
30
+ pub fn new() -> Self {
31
+ let root = flex_box();
32
+ root.flex_direction(FlexDirection::Column)
33
+ .semantic_role(SemanticRole::Form);
34
+ let form = Self {
35
+ shared: Rc::new(FormShared {
36
+ root,
37
+ default_button: RefCell::new(None),
38
+ cancel_button: RefCell::new(None),
39
+ key_filter_token: Cell::new(0),
40
+ armed_key: RefCell::new(None),
41
+ armed_button: RefCell::new(None),
42
+ }),
43
+ };
44
+ form.bind_events();
45
+ form
46
+ }
47
+
48
+ pub fn default_btn(&self, button: &Button) -> &Self {
49
+ self.shared
50
+ .default_button
51
+ .borrow_mut()
52
+ .replace(button.clone());
53
+ self
54
+ }
55
+
56
+ pub fn cancel_btn(&self, button: &Button) -> &Self {
57
+ self.shared
58
+ .cancel_button
59
+ .borrow_mut()
60
+ .replace(button.clone());
61
+ self
62
+ }
63
+
64
+ pub fn child<T: Node>(&self, node: &T) -> &Self {
65
+ self.shared.root.child(node);
66
+ self
67
+ }
68
+
69
+ pub fn children<I, C>(&self, nodes: I) -> &Self
70
+ where
71
+ I: IntoIterator<Item = C>,
72
+ C: Into<Child>,
73
+ {
74
+ for node in nodes {
75
+ self.shared
76
+ .root
77
+ .retained_node_ref()
78
+ .append_child_ref(&node.into().node_ref);
79
+ }
80
+ self
81
+ }
82
+
83
+ pub fn activate(&self) {
84
+ if self.shared.key_filter_token.get() != 0
85
+ || (self.shared.default_button.borrow().is_none()
86
+ && self.shared.cancel_button.borrow().is_none())
87
+ {
88
+ return;
89
+ }
90
+ let weak = Rc::downgrade(&self.shared);
91
+ let token = event::push_key_filter(move |event_type, key, modifiers| {
92
+ let Some(shared) = weak.upgrade() else {
93
+ return false;
94
+ };
95
+ shared.handle_global_key_event(event_type, key, modifiers)
96
+ });
97
+ self.shared.key_filter_token.set(token);
98
+ }
99
+
100
+ pub fn deactivate(&self) {
101
+ self.shared.deactivate();
102
+ }
103
+
104
+ fn bind_events(&self) {
105
+ let weak = Rc::downgrade(&self.shared);
106
+ self.shared.root.on_key_down(move |event| {
107
+ let Some(shared) = weak.upgrade() else {
108
+ return;
109
+ };
110
+ if shared.handle_global_key_event(
111
+ KeyEventType::Down,
112
+ event.key.as_str(),
113
+ event.modifiers,
114
+ ) {
115
+ event.handled = true;
116
+ }
117
+ });
118
+ let weak = Rc::downgrade(&self.shared);
119
+ self.shared.root.on_key_up(move |event| {
120
+ let Some(shared) = weak.upgrade() else {
121
+ return;
122
+ };
123
+ if shared.handle_global_key_event(KeyEventType::Up, event.key.as_str(), event.modifiers)
124
+ {
125
+ event.handled = true;
126
+ }
127
+ });
128
+ }
129
+ }
130
+
131
+ impl Drop for Form {
132
+ fn drop(&mut self) {
133
+ if Rc::strong_count(&self.shared) == 1 {
134
+ self.shared.deactivate();
135
+ }
136
+ }
137
+ }
138
+
139
+ impl FormShared {
140
+ fn deactivate(&self) {
141
+ self.cancel_armed_button();
142
+ let token = self.key_filter_token.replace(0);
143
+ if token != 0 {
144
+ event::remove_key_filter(token);
145
+ }
146
+ }
147
+
148
+ fn handle_global_key_event(&self, event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
149
+ if key == "Enter" && self.should_defer_enter_to_focused_button(modifiers) {
150
+ return false;
151
+ }
152
+ match event_type {
153
+ KeyEventType::Down => self.handle_key_down(key),
154
+ KeyEventType::Up => self.handle_key_up(key),
155
+ }
156
+ }
157
+
158
+ fn handle_key_down(&self, key: &str) -> bool {
159
+ let Some(button) = self.resolve_button_for_key(key) else {
160
+ return false;
161
+ };
162
+ if let Some(armed_key) = self.armed_key.borrow().as_ref() {
163
+ return armed_key == key;
164
+ }
165
+ self.armed_key.borrow_mut().replace(key.to_string());
166
+ self.armed_button.borrow_mut().replace(button.clone());
167
+ button.begin_press();
168
+ true
169
+ }
170
+
171
+ fn handle_key_up(&self, key: &str) -> bool {
172
+ let armed_matches = self
173
+ .armed_key
174
+ .borrow()
175
+ .as_ref()
176
+ .is_some_and(|armed_key| armed_key == key);
177
+ if !armed_matches {
178
+ return false;
179
+ }
180
+ let button = self.armed_button.borrow_mut().take();
181
+ self.armed_key.borrow_mut().take();
182
+ if let Some(button) = button {
183
+ button.end_press(true);
184
+ }
185
+ true
186
+ }
187
+
188
+ fn cancel_armed_button(&self) {
189
+ let button = self.armed_button.borrow_mut().take();
190
+ self.armed_key.borrow_mut().take();
191
+ if let Some(button) = button {
192
+ button.cancel_press();
193
+ }
194
+ }
195
+
196
+ fn resolve_button_for_key(&self, key: &str) -> Option<Button> {
197
+ match key {
198
+ "Enter" => self.default_button.borrow().clone(),
199
+ "Escape" => self.cancel_button.borrow().clone(),
200
+ _ => None,
201
+ }
202
+ }
203
+
204
+ fn should_defer_enter_to_focused_button(&self, modifiers: u32) -> bool {
205
+ if modifiers != 0 {
206
+ return false;
207
+ }
208
+ event::focused_node_is_enabled_button()
209
+ }
210
+ }
211
+
212
+ impl Node for Form {
213
+ fn retained_node_ref(&self) -> NodeRef {
214
+ self.shared.root.retained_node_ref()
215
+ }
216
+
217
+ fn retained_owner_attachment(&self) -> Option<Rc<dyn Any>> {
218
+ Some(self.shared.clone())
219
+ }
220
+
221
+ fn build_self(&self) {
222
+ self.shared.root.build_self();
223
+ }
224
+
225
+ fn dispose(&self) {
226
+ self.deactivate();
227
+ self.shared.root.dispose();
228
+ }
229
+ }
@@ -0,0 +1,142 @@
1
+ use crate::controls::ButtonColors;
2
+ use crate::ffi::{AlignItems, FlexDirection, JustifyContent};
3
+ use crate::node::{flex_box, FlexBox, TextCore};
4
+ use crate::theme::Theme;
5
+ use crate::{FontStyle, FontWeight};
6
+ use std::rc::Rc;
7
+
8
+ #[derive(Clone, Copy, Debug, Default)]
9
+ pub struct ButtonVisualState {
10
+ pub hovered: bool,
11
+ pub pressed: bool,
12
+ pub focused: bool,
13
+ pub enabled: bool,
14
+ }
15
+
16
+ pub trait ButtonPresenter {
17
+ fn content_root(&self) -> FlexBox;
18
+ fn label_node(&self) -> TextCore;
19
+ fn apply(
20
+ &self,
21
+ host: &FlexBox,
22
+ theme: Theme,
23
+ state: ButtonVisualState,
24
+ colors: Option<ButtonColors>,
25
+ );
26
+ }
27
+
28
+ pub trait ButtonTemplate {
29
+ fn create(&self) -> Rc<dyn ButtonPresenter>;
30
+ }
31
+
32
+ #[derive(Clone)]
33
+ pub struct DefaultButtonPresenter {
34
+ content_root: FlexBox,
35
+ label_node: TextCore,
36
+ }
37
+
38
+ impl DefaultButtonPresenter {
39
+ pub fn new() -> Self {
40
+ let label_node = TextCore::new("");
41
+ let content_root = flex_box();
42
+ content_root
43
+ .flex_direction(FlexDirection::Row)
44
+ .align_items(AlignItems::Center)
45
+ .justify_content(JustifyContent::Center)
46
+ .child(&label_node);
47
+ Self {
48
+ content_root,
49
+ label_node,
50
+ }
51
+ }
52
+ }
53
+
54
+ impl ButtonPresenter for DefaultButtonPresenter {
55
+ fn content_root(&self) -> FlexBox {
56
+ self.content_root.clone()
57
+ }
58
+
59
+ fn label_node(&self) -> TextCore {
60
+ self.label_node.clone()
61
+ }
62
+
63
+ fn apply(
64
+ &self,
65
+ host: &FlexBox,
66
+ theme: Theme,
67
+ state: ButtonVisualState,
68
+ colors: Option<ButtonColors>,
69
+ ) {
70
+ let background = if !state.enabled {
71
+ colors
72
+ .and_then(|colors| colors.background)
73
+ .unwrap_or(theme.colors.accent)
74
+ } else if state.pressed {
75
+ colors
76
+ .and_then(|colors| colors.background_pressed)
77
+ .or_else(|| colors.and_then(|colors| colors.background_hover))
78
+ .or_else(|| colors.and_then(|colors| colors.background))
79
+ .unwrap_or(theme.colors.accent_pressed)
80
+ } else if state.hovered {
81
+ colors
82
+ .and_then(|colors| colors.background_hover)
83
+ .or_else(|| colors.and_then(|colors| colors.background))
84
+ .unwrap_or(theme.colors.accent_hovered)
85
+ } else {
86
+ colors
87
+ .and_then(|colors| colors.background)
88
+ .unwrap_or(theme.colors.accent)
89
+ };
90
+ let border = colors
91
+ .and_then(|colors| colors.border)
92
+ .unwrap_or(theme.colors.border);
93
+ let text_color = if !state.enabled {
94
+ colors
95
+ .and_then(|colors| colors.text_muted)
96
+ .or_else(|| colors.and_then(|colors| colors.text_primary))
97
+ .unwrap_or(theme.colors.text_on_accent)
98
+ } else {
99
+ colors
100
+ .and_then(|colors| colors.text_primary)
101
+ .unwrap_or(theme.colors.text_on_accent)
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
+ self.content_root
117
+ .flex_direction(FlexDirection::Row)
118
+ .align_items(AlignItems::Center)
119
+ .justify_content(JustifyContent::Center);
120
+ self.label_node
121
+ .font_family(theme.fonts.body_family.clone())
122
+ .font_weight(FontWeight::Regular)
123
+ .font_style(FontStyle::Normal)
124
+ .font_size(theme.fonts.size_body)
125
+ .text_color(text_color);
126
+ }
127
+ }
128
+
129
+ #[derive(Clone, Copy, Debug, Default)]
130
+ pub struct DefaultButtonTemplate;
131
+
132
+ impl ButtonTemplate for DefaultButtonTemplate {
133
+ fn create(&self) -> Rc<dyn ButtonPresenter> {
134
+ Rc::new(DefaultButtonPresenter::new())
135
+ }
136
+ }
137
+
138
+ pub const DEFAULT_BUTTON_TEMPLATE: DefaultButtonTemplate = DefaultButtonTemplate;
139
+
140
+ pub fn create_default_button_presenter() -> Rc<dyn ButtonPresenter> {
141
+ Rc::new(DefaultButtonPresenter::new())
142
+ }
@@ -0,0 +1,210 @@
1
+ use super::pressable_indicator_presenter::{
2
+ PressableIndicatorMetrics, PressableIndicatorPresenter, PressableIndicatorVisualState,
3
+ };
4
+ use crate::controls::{LabeledControlColors, LabeledControlSizing};
5
+ use crate::ffi::{AlignItems, JustifyContent, SemanticCheckedState, Unit};
6
+ use crate::node::{flex_box, svg, FlexBox, SvgNode};
7
+ use crate::theme::Theme;
8
+ use std::rc::Rc;
9
+
10
+ const CHECKBOX_CHECK_SVG_URL: &str = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 14 14'><path d='M2.25 7.15 5.35 10.25 11.75 3.85' fill='none' stroke='%23000000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";
11
+
12
+ #[derive(Clone, Copy, Debug, PartialEq)]
13
+ pub struct CheckboxIndicatorMetrics {
14
+ pub indicator_size: f32,
15
+ pub corner_radius: f32,
16
+ pub check_mark_size: f32,
17
+ }
18
+
19
+ impl CheckboxIndicatorMetrics {
20
+ pub const fn new(indicator_size: f32, corner_radius: f32, check_mark_size: f32) -> Self {
21
+ Self {
22
+ indicator_size,
23
+ corner_radius,
24
+ check_mark_size,
25
+ }
26
+ }
27
+ }
28
+
29
+ const DEFAULT_CHECKBOX_METRICS: CheckboxIndicatorMetrics =
30
+ CheckboxIndicatorMetrics::new(20.0, 4.0, 16.0);
31
+
32
+ pub fn resolve_checkbox_metrics(sizing: Option<LabeledControlSizing>) -> CheckboxIndicatorMetrics {
33
+ let Some(sizing) = sizing else {
34
+ return DEFAULT_CHECKBOX_METRICS;
35
+ };
36
+ if !sizing.has_indicator_size() {
37
+ return DEFAULT_CHECKBOX_METRICS;
38
+ }
39
+ let indicator_size = sizing.indicator_size_px();
40
+ CheckboxIndicatorMetrics::new(indicator_size, indicator_size * 0.2, indicator_size * 0.8)
41
+ }
42
+
43
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
44
+ pub struct CheckboxIndicatorVisualState {
45
+ pub checked_state: SemanticCheckedState,
46
+ pub hovered: bool,
47
+ pub pressed: bool,
48
+ pub focused: bool,
49
+ pub enabled: bool,
50
+ }
51
+
52
+ impl CheckboxIndicatorVisualState {
53
+ pub fn new(
54
+ checked_state: SemanticCheckedState,
55
+ hovered: bool,
56
+ pressed: bool,
57
+ focused: bool,
58
+ enabled: bool,
59
+ ) -> Self {
60
+ Self {
61
+ checked_state,
62
+ hovered,
63
+ pressed,
64
+ focused,
65
+ enabled,
66
+ }
67
+ }
68
+
69
+ pub fn pressable_state(&self) -> PressableIndicatorVisualState {
70
+ PressableIndicatorVisualState {
71
+ hovered: self.hovered,
72
+ pressed: self.pressed,
73
+ focused: self.focused,
74
+ enabled: self.enabled,
75
+ }
76
+ }
77
+ }
78
+
79
+ pub trait CheckboxIndicatorPresenter: PressableIndicatorPresenter {
80
+ fn apply(
81
+ &self,
82
+ theme: Theme,
83
+ state: CheckboxIndicatorVisualState,
84
+ colors: Option<LabeledControlColors>,
85
+ );
86
+ }
87
+
88
+ pub trait CheckboxIndicatorTemplate {
89
+ fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter>;
90
+ }
91
+
92
+ #[derive(Clone)]
93
+ pub struct DefaultCheckboxIndicatorPresenter {
94
+ root: FlexBox,
95
+ metrics: CheckboxIndicatorMetrics,
96
+ mark_node: SvgNode,
97
+ }
98
+
99
+ impl DefaultCheckboxIndicatorPresenter {
100
+ pub fn new(metrics: CheckboxIndicatorMetrics) -> Self {
101
+ let root = flex_box();
102
+ root.width(metrics.indicator_size, Unit::Pixel)
103
+ .height(metrics.indicator_size, Unit::Pixel)
104
+ .align_items(AlignItems::Center)
105
+ .justify_content(JustifyContent::Center);
106
+ let mark_host = flex_box();
107
+ mark_host
108
+ .fill_size()
109
+ .align_items(AlignItems::Center)
110
+ .justify_content(JustifyContent::Center);
111
+ let mark_node = svg(0);
112
+ mark_node
113
+ .width(metrics.check_mark_size, Unit::Pixel)
114
+ .height(metrics.check_mark_size, Unit::Pixel);
115
+ mark_host.child(&mark_node);
116
+ root.child(&mark_host);
117
+ Self {
118
+ root,
119
+ metrics,
120
+ mark_node,
121
+ }
122
+ }
123
+ }
124
+
125
+ impl PressableIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
126
+ fn root(&self) -> FlexBox {
127
+ self.root.clone()
128
+ }
129
+
130
+ fn metrics(&self) -> PressableIndicatorMetrics {
131
+ PressableIndicatorMetrics::new(self.metrics.indicator_size, self.metrics.indicator_size)
132
+ }
133
+ }
134
+
135
+ impl CheckboxIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
136
+ fn apply(
137
+ &self,
138
+ theme: Theme,
139
+ state: CheckboxIndicatorVisualState,
140
+ colors: Option<LabeledControlColors>,
141
+ ) {
142
+ let metrics = self.metrics;
143
+ let accent = colors
144
+ .filter(|colors| colors.has_accent())
145
+ .map(|colors| colors.accent_color())
146
+ .unwrap_or_else(|| {
147
+ if state.pressed {
148
+ theme.colors.accent_pressed
149
+ } else if state.hovered {
150
+ theme.colors.accent_hovered
151
+ } else {
152
+ theme.colors.accent
153
+ }
154
+ });
155
+ let mut background = colors
156
+ .filter(|colors| colors.has_background())
157
+ .map(|colors| colors.background_color())
158
+ .unwrap_or(theme.colors.surface);
159
+ let mut border_color = colors
160
+ .filter(|colors| colors.has_border())
161
+ .map(|colors| colors.border_color())
162
+ .unwrap_or(theme.colors.border);
163
+ let mut mark_visible = false;
164
+ let mut mark_color = theme.colors.text_primary;
165
+ if state.checked_state == SemanticCheckedState::True
166
+ || state.checked_state == SemanticCheckedState::Mixed
167
+ {
168
+ background = accent;
169
+ border_color = background;
170
+ mark_visible = state.checked_state == SemanticCheckedState::True;
171
+ mark_color = theme.colors.text_on_accent;
172
+ } else if state.hovered && colors.is_none_or(|colors| !colors.has_background()) {
173
+ background = theme.colors.background;
174
+ }
175
+ self.root
176
+ .corner_radius(metrics.corner_radius)
177
+ .border(1.0, border_color)
178
+ .bg_color(background);
179
+ if mark_visible {
180
+ self.mark_node.source(CHECKBOX_CHECK_SVG_URL);
181
+ } else {
182
+ self.mark_node.clear_source();
183
+ }
184
+ self.mark_node
185
+ .width(metrics.check_mark_size, Unit::Pixel)
186
+ .height(metrics.check_mark_size, Unit::Pixel)
187
+ .opacity(if mark_visible { 1.0 } else { 0.0 })
188
+ .tint(mark_color);
189
+ }
190
+ }
191
+
192
+ #[derive(Clone, Copy, Debug, Default)]
193
+ pub struct DefaultCheckboxIndicatorTemplate;
194
+
195
+ impl CheckboxIndicatorTemplate for DefaultCheckboxIndicatorTemplate {
196
+ fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter> {
197
+ create_default_checkbox_indicator_presenter(sizing)
198
+ }
199
+ }
200
+
201
+ pub const DEFAULT_CHECKBOX_INDICATOR_TEMPLATE: DefaultCheckboxIndicatorTemplate =
202
+ DefaultCheckboxIndicatorTemplate;
203
+
204
+ pub fn create_default_checkbox_indicator_presenter(
205
+ sizing: Option<LabeledControlSizing>,
206
+ ) -> Rc<dyn CheckboxIndicatorPresenter> {
207
+ Rc::new(DefaultCheckboxIndicatorPresenter::new(
208
+ resolve_checkbox_metrics(sizing),
209
+ ))
210
+ }
@@ -0,0 +1,134 @@
1
+ use crate::controls::DropdownSizing;
2
+ use crate::ffi::{AlignItems, JustifyContent, Unit};
3
+ use crate::node::{flex_box, svg, FlexBox, SvgNode};
4
+ use crate::theme::Theme;
5
+ use std::rc::Rc;
6
+
7
+ const DROPDOWN_CHEVRON_COLLAPSED_SVG: &str = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%23000000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>";
8
+ const DROPDOWN_CHEVRON_EXPANDED_SVG: &str = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M3 7.5 6 4.5 9 7.5' fill='none' stroke='%23000000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>";
9
+ const DEFAULT_DROPDOWN_CHEVRON_ICON_SIZE: f32 = 12.0;
10
+
11
+ #[derive(Clone, Copy, Debug, PartialEq)]
12
+ pub struct DropdownChevronMetrics {
13
+ pub icon_size: f32,
14
+ }
15
+
16
+ impl DropdownChevronMetrics {
17
+ pub const fn new(icon_size: f32) -> Self {
18
+ Self { icon_size }
19
+ }
20
+ }
21
+
22
+ pub const DEFAULT_DROPDOWN_CHEVRON_METRICS: DropdownChevronMetrics =
23
+ DropdownChevronMetrics::new(DEFAULT_DROPDOWN_CHEVRON_ICON_SIZE);
24
+
25
+ fn resolve_chevron_metrics(sizing: Option<DropdownSizing>) -> DropdownChevronMetrics {
26
+ let Some(sizing) = sizing else {
27
+ return DEFAULT_DROPDOWN_CHEVRON_METRICS;
28
+ };
29
+ if !sizing.has_chevron_icon_size() {
30
+ return DEFAULT_DROPDOWN_CHEVRON_METRICS;
31
+ }
32
+ DropdownChevronMetrics::new(sizing.chevron_icon_size_px())
33
+ }
34
+
35
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36
+ pub struct DropdownChevronVisualState {
37
+ pub open: bool,
38
+ pub hovered: bool,
39
+ pub enabled: bool,
40
+ }
41
+
42
+ impl DropdownChevronVisualState {
43
+ pub const fn new(open: bool, hovered: bool, enabled: bool) -> Self {
44
+ Self {
45
+ open,
46
+ hovered,
47
+ enabled,
48
+ }
49
+ }
50
+ }
51
+
52
+ pub trait DropdownChevronPresenter {
53
+ fn root(&self) -> FlexBox;
54
+ fn apply(&self, theme: Theme, state: DropdownChevronVisualState);
55
+ }
56
+
57
+ pub trait DropdownChevronTemplate {
58
+ fn create(&self, sizing: Option<DropdownSizing>) -> Rc<dyn DropdownChevronPresenter>;
59
+ }
60
+
61
+ #[derive(Clone)]
62
+ pub struct DefaultDropdownChevronPresenter {
63
+ root: FlexBox,
64
+ metrics: DropdownChevronMetrics,
65
+ icon_node: SvgNode,
66
+ }
67
+
68
+ impl DefaultDropdownChevronPresenter {
69
+ pub fn new(metrics: DropdownChevronMetrics) -> Self {
70
+ let root = flex_box();
71
+ root.fill_size()
72
+ .align_items(AlignItems::Center)
73
+ .justify_content(JustifyContent::Center);
74
+ let icon_node = svg(0);
75
+ icon_node
76
+ .width(metrics.icon_size, Unit::Pixel)
77
+ .height(metrics.icon_size, Unit::Pixel);
78
+ root.child(&icon_node);
79
+ Self {
80
+ root,
81
+ metrics,
82
+ icon_node: icon_node.clone(),
83
+ }
84
+ }
85
+ }
86
+
87
+ impl DropdownChevronPresenter for DefaultDropdownChevronPresenter {
88
+ fn root(&self) -> FlexBox {
89
+ self.root.clone()
90
+ }
91
+
92
+ fn apply(&self, theme: Theme, state: DropdownChevronVisualState) {
93
+ let metrics = self.metrics;
94
+ self.root
95
+ .fill_size()
96
+ .align_items(AlignItems::Center)
97
+ .justify_content(JustifyContent::Center);
98
+ self.icon_node
99
+ .width(metrics.icon_size, Unit::Pixel)
100
+ .height(metrics.icon_size, Unit::Pixel)
101
+ .source(if state.open {
102
+ DROPDOWN_CHEVRON_EXPANDED_SVG
103
+ } else {
104
+ DROPDOWN_CHEVRON_COLLAPSED_SVG
105
+ })
106
+ .tint(if !state.enabled {
107
+ theme.colors.text_muted
108
+ } else if state.hovered {
109
+ theme.colors.text_primary
110
+ } else {
111
+ theme.colors.text_muted
112
+ });
113
+ }
114
+ }
115
+
116
+ #[derive(Clone, Copy, Debug, Default)]
117
+ pub struct DefaultDropdownChevronTemplate;
118
+
119
+ impl DropdownChevronTemplate for DefaultDropdownChevronTemplate {
120
+ fn create(&self, sizing: Option<DropdownSizing>) -> Rc<dyn DropdownChevronPresenter> {
121
+ create_default_dropdown_chevron_presenter(sizing)
122
+ }
123
+ }
124
+
125
+ pub const DEFAULT_DROPDOWN_CHEVRON_TEMPLATE: DefaultDropdownChevronTemplate =
126
+ DefaultDropdownChevronTemplate;
127
+
128
+ pub fn create_default_dropdown_chevron_presenter(
129
+ sizing: Option<DropdownSizing>,
130
+ ) -> Rc<dyn DropdownChevronPresenter> {
131
+ Rc::new(DefaultDropdownChevronPresenter::new(
132
+ resolve_chevron_metrics(sizing),
133
+ ))
134
+ }