@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.
- package/COMMERCIAL.md +7 -0
- package/Cargo.toml +34 -0
- package/LICENSE.md +9 -0
- package/README.md +88 -0
- package/package.json +54 -0
- package/scripts/build.sh +227 -0
- package/scripts/check-runtime-dependency.sh +41 -0
- package/scripts/framework-host-services.ts +40 -0
- package/scripts/generate-host-events.ts +17 -0
- package/scripts/generate-host-services.ts +28 -0
- package/scripts/hostgen/common.ts +73 -0
- package/scripts/hostgen/registry.ts +159 -0
- package/scripts/hostgen/rust-host-events.ts +171 -0
- package/scripts/hostgen/rust-host-services.ts +257 -0
- package/src/animation.rs +625 -0
- package/src/app.rs +324 -0
- package/src/assets.rs +572 -0
- package/src/bindings/mod.rs +1 -0
- package/src/bindings/ui.rs +705 -0
- package/src/bitmap.rs +317 -0
- package/src/bridge_callbacks.rs +242 -0
- package/src/context_menu_manager.rs +332 -0
- package/src/controls/anti_selection_area.rs +54 -0
- package/src/controls/button.rs +542 -0
- package/src/controls/checkbox.rs +372 -0
- package/src/controls/combobox.rs +1574 -0
- package/src/controls/context_menu.rs +1367 -0
- package/src/controls/control_template_set.rs +46 -0
- package/src/controls/control_tokens.rs +596 -0
- package/src/controls/dialog.rs +590 -0
- package/src/controls/dropdown.rs +1010 -0
- package/src/controls/form.rs +229 -0
- package/src/controls/internal/button_presenter.rs +142 -0
- package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
- package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
- package/src/controls/internal/dropdown_field_presenter.rs +269 -0
- package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
- package/src/controls/internal/mod.rs +13 -0
- package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
- package/src/controls/internal/pressable_labeled_control.rs +492 -0
- package/src/controls/internal/radio_indicator_presenter.rs +208 -0
- package/src/controls/internal/selectable_popup_list.rs +597 -0
- package/src/controls/internal/slider_presenter.rs +282 -0
- package/src/controls/internal/switch_indicator_presenter.rs +235 -0
- package/src/controls/internal/text_input_core.rs +1074 -0
- package/src/controls/internal/text_input_presenter.rs +108 -0
- package/src/controls/mod.rs +235 -0
- package/src/controls/nav_link.rs +395 -0
- package/src/controls/popup.rs +191 -0
- package/src/controls/progress_bar.rs +287 -0
- package/src/controls/radio_button.rs +348 -0
- package/src/controls/radio_group.rs +283 -0
- package/src/controls/selection_area.rs +82 -0
- package/src/controls/shared.rs +68 -0
- package/src/controls/slider.rs +701 -0
- package/src/controls/switch.rs +293 -0
- package/src/controls/templating.rs +49 -0
- package/src/controls/tests.rs +3905 -0
- package/src/controls/text_area.rs +207 -0
- package/src/controls/text_input.rs +192 -0
- package/src/debug.rs +146 -0
- package/src/drag_drop.rs +424 -0
- package/src/drag_gesture.rs +258 -0
- package/src/drawing.rs +385 -0
- package/src/event.rs +1603 -0
- package/src/external_drop.rs +467 -0
- package/src/fetch.rs +500 -0
- package/src/ffi.rs +3542 -0
- package/src/file.rs +1677 -0
- package/src/focus_adorner.rs +307 -0
- package/src/focus_visibility.rs +121 -0
- package/src/frame_scheduler.rs +159 -0
- package/src/frame_signal.rs +64 -0
- package/src/generated/ffi.rs +798 -0
- package/src/generated/framework_host_services.rs +74 -0
- package/src/generated/mod.rs +2 -0
- package/src/host_services.rs +162 -0
- package/src/image_sampling.rs +78 -0
- package/src/keyboard_scroll.rs +137 -0
- package/src/keyboard_scroll_tracker.rs +385 -0
- package/src/lib.rs +547 -0
- package/src/logger.rs +56 -0
- package/src/mobile_text_selection_toolbar.rs +1034 -0
- package/src/navigation.rs +48 -0
- package/src/node/core.rs +2210 -0
- package/src/node/custom_drawable.rs +149 -0
- package/src/node/flex_box.rs +874 -0
- package/src/node/grid.rs +304 -0
- package/src/node/helpers.rs +442 -0
- package/src/node/image.rs +506 -0
- package/src/node/mod.rs +315 -0
- package/src/node/scroll_bar.rs +845 -0
- package/src/node/scroll_box.rs +514 -0
- package/src/node/scroll_state.rs +121 -0
- package/src/node/scroll_view.rs +557 -0
- package/src/node/svg_node.rs +474 -0
- package/src/node/text_node.rs +633 -0
- package/src/node/virtual_list.rs +678 -0
- package/src/panic_hook.rs +36 -0
- package/src/persisted.rs +274 -0
- package/src/platform.rs +556 -0
- package/src/popup_presenter.rs +344 -0
- package/src/selection_handle_adorner.rs +838 -0
- package/src/signal.rs +134 -0
- package/src/text.rs +1065 -0
- package/src/theme.rs +571 -0
- package/src/timers.rs +106 -0
- package/src/tool_tip.rs +167 -0
- package/src/tool_tip_manager.rs +691 -0
- package/src/transitions.rs +41 -0
- package/src/typography.rs +520 -0
- package/src/viewport.rs +108 -0
- package/src/worker.rs +308 -0
- package/src/worker_job.rs +82 -0
- 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
|
+
}
|