@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
package/src/theme.rs
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
use crate::generated::framework_host_services;
|
|
2
|
+
use crate::signal::{Callback, Signal, SubscriptionGuard};
|
|
3
|
+
use crate::typography::{FontFamily, FontStack};
|
|
4
|
+
use std::cell::RefCell;
|
|
5
|
+
use std::rc::Rc;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ACCENT_COLOR: u32 = 0x2563EBFF;
|
|
8
|
+
const WHITE: u32 = 0xFFFFFFFF;
|
|
9
|
+
const BLACK: u32 = 0x000000FF;
|
|
10
|
+
|
|
11
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
12
|
+
pub struct Colors {
|
|
13
|
+
pub background: u32,
|
|
14
|
+
pub surface: u32,
|
|
15
|
+
pub text_primary: u32,
|
|
16
|
+
pub text_muted: u32,
|
|
17
|
+
pub text_on_accent: u32,
|
|
18
|
+
pub accent: u32,
|
|
19
|
+
pub accent_pressed: u32,
|
|
20
|
+
pub accent_hovered: u32,
|
|
21
|
+
pub border: u32,
|
|
22
|
+
pub selection: u32,
|
|
23
|
+
pub scrollbar_track: u32,
|
|
24
|
+
pub scrollbar_thumb: u32,
|
|
25
|
+
pub dialog_backdrop: u32,
|
|
26
|
+
pub dialog_shadow: u32,
|
|
27
|
+
pub panel_shadow: u32,
|
|
28
|
+
pub focus_ring: u32,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
32
|
+
pub struct Spacing {
|
|
33
|
+
pub xs: f32,
|
|
34
|
+
pub sm: f32,
|
|
35
|
+
pub md: f32,
|
|
36
|
+
pub lg: f32,
|
|
37
|
+
pub xl: f32,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
41
|
+
pub struct Fonts {
|
|
42
|
+
pub body_stack: FontStack,
|
|
43
|
+
pub heading_stack: FontStack,
|
|
44
|
+
pub mono_stack: FontStack,
|
|
45
|
+
pub mono_bold_stack: FontStack,
|
|
46
|
+
pub body_family: FontFamily,
|
|
47
|
+
pub heading_family: FontFamily,
|
|
48
|
+
pub mono_family: FontFamily,
|
|
49
|
+
pub size_body: f32,
|
|
50
|
+
pub size_heading: f32,
|
|
51
|
+
pub size_mono: f32,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
55
|
+
pub struct ContextMenuItemTheme {
|
|
56
|
+
pub background: u32,
|
|
57
|
+
pub hover_background: u32,
|
|
58
|
+
pub text_color: u32,
|
|
59
|
+
pub corner_radius: f32,
|
|
60
|
+
pub font_family: FontFamily,
|
|
61
|
+
pub font_size: f32,
|
|
62
|
+
pub height: f32,
|
|
63
|
+
pub padding_left: f32,
|
|
64
|
+
pub padding_top: f32,
|
|
65
|
+
pub padding_right: f32,
|
|
66
|
+
pub padding_bottom: f32,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
70
|
+
pub struct ContextMenuTheme {
|
|
71
|
+
pub panel_background: u32,
|
|
72
|
+
pub panel_border_color: u32,
|
|
73
|
+
pub panel_shadow_color: u32,
|
|
74
|
+
pub panel_corner_radius: f32,
|
|
75
|
+
pub separator_color: u32,
|
|
76
|
+
pub shadow_offset_y: f32,
|
|
77
|
+
pub shadow_blur: f32,
|
|
78
|
+
pub shadow_spread: f32,
|
|
79
|
+
pub item: ContextMenuItemTheme,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
83
|
+
pub struct ToolTipTheme {
|
|
84
|
+
pub panel_background: u32,
|
|
85
|
+
pub panel_border_color: u32,
|
|
86
|
+
pub panel_shadow_color: u32,
|
|
87
|
+
pub panel_corner_radius: f32,
|
|
88
|
+
pub text_color: u32,
|
|
89
|
+
pub font_family: FontFamily,
|
|
90
|
+
pub font_size: f32,
|
|
91
|
+
pub max_width: f32,
|
|
92
|
+
pub padding_left: f32,
|
|
93
|
+
pub padding_top: f32,
|
|
94
|
+
pub padding_right: f32,
|
|
95
|
+
pub padding_bottom: f32,
|
|
96
|
+
pub shadow_offset_y: f32,
|
|
97
|
+
pub shadow_blur: f32,
|
|
98
|
+
pub shadow_spread: f32,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
102
|
+
pub struct Theme {
|
|
103
|
+
pub colors: Colors,
|
|
104
|
+
pub spacing: Spacing,
|
|
105
|
+
pub fonts: Fonts,
|
|
106
|
+
pub context_menu: ContextMenuTheme,
|
|
107
|
+
pub tool_tip: ToolTipTheme,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
111
|
+
enum ThemeSource {
|
|
112
|
+
System,
|
|
113
|
+
Custom,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
struct ThemeState {
|
|
117
|
+
signal: Signal<Theme>,
|
|
118
|
+
theme_source: ThemeSource,
|
|
119
|
+
system_dark_mode: bool,
|
|
120
|
+
system_accent_color: u32,
|
|
121
|
+
current_dark_mode: bool,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
thread_local! {
|
|
125
|
+
static THEME_STATE: RefCell<ThemeState> = RefCell::new(ThemeState {
|
|
126
|
+
signal: Signal::new(default_dark_theme()),
|
|
127
|
+
theme_source: ThemeSource::System,
|
|
128
|
+
system_dark_mode: true,
|
|
129
|
+
system_accent_color: DEFAULT_ACCENT_COLOR,
|
|
130
|
+
current_dark_mode: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const DEFAULT_SPACING: Spacing = Spacing {
|
|
135
|
+
xs: 4.0,
|
|
136
|
+
sm: 8.0,
|
|
137
|
+
md: 16.0,
|
|
138
|
+
lg: 24.0,
|
|
139
|
+
xl: 32.0,
|
|
140
|
+
};
|
|
141
|
+
fn default_fonts() -> Fonts {
|
|
142
|
+
let body_stack = FontStack::from_id(1).fallback_face(crate::typography::FontFace::new(3));
|
|
143
|
+
let heading_stack = FontStack::from_id(2).fallback_face(crate::typography::FontFace::new(3));
|
|
144
|
+
let mono_stack = FontStack::from_id(7)
|
|
145
|
+
.fallback_face(crate::typography::FontFace::new(4))
|
|
146
|
+
.fallback_face(crate::typography::FontFace::new(3));
|
|
147
|
+
let mono_bold_stack = FontStack::from_id(8)
|
|
148
|
+
.fallback_face(crate::typography::FontFace::new(4))
|
|
149
|
+
.fallback_face(crate::typography::FontFace::new(3));
|
|
150
|
+
Fonts {
|
|
151
|
+
body_stack: body_stack.clone(),
|
|
152
|
+
heading_stack: heading_stack.clone(),
|
|
153
|
+
mono_stack: mono_stack.clone(),
|
|
154
|
+
mono_bold_stack: mono_bold_stack.clone(),
|
|
155
|
+
body_family: FontFamily::regular_bold_stacks(body_stack.clone(), heading_stack.clone())
|
|
156
|
+
.italic_stack(FontStack::from_id(5))
|
|
157
|
+
.bold_italic_stack(FontStack::from_id(6)),
|
|
158
|
+
heading_family: FontFamily::regular_bold_stacks(
|
|
159
|
+
heading_stack.clone(),
|
|
160
|
+
heading_stack.clone(),
|
|
161
|
+
),
|
|
162
|
+
mono_family: FontFamily::regular_bold_stacks(mono_stack.clone(), mono_bold_stack.clone()),
|
|
163
|
+
size_body: 16.0,
|
|
164
|
+
size_heading: 24.0,
|
|
165
|
+
size_mono: 15.0,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fn rgba(r: u32, g: u32, b: u32, a: u32) -> u32 {
|
|
170
|
+
(r << 24) | (g << 16) | (b << 8) | a
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn color_red(color: u32) -> u32 {
|
|
174
|
+
(color >> 24) & 0xff
|
|
175
|
+
}
|
|
176
|
+
fn color_green(color: u32) -> u32 {
|
|
177
|
+
(color >> 16) & 0xff
|
|
178
|
+
}
|
|
179
|
+
fn color_blue(color: u32) -> u32 {
|
|
180
|
+
(color >> 8) & 0xff
|
|
181
|
+
}
|
|
182
|
+
fn color_alpha(color: u32) -> u32 {
|
|
183
|
+
color & 0xff
|
|
184
|
+
}
|
|
185
|
+
fn clamp_unit(value: f32) -> f32 {
|
|
186
|
+
value.clamp(0.0, 1.0)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn mix_channel(from: u32, to: u32, amount: f32) -> u32 {
|
|
190
|
+
let weight = clamp_unit(amount);
|
|
191
|
+
(from as f32 + ((to as f32 - from as f32) * weight)).round() as u32
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn mix_color(from: u32, to: u32, amount: f32) -> u32 {
|
|
195
|
+
rgba(
|
|
196
|
+
mix_channel(color_red(from), color_red(to), amount),
|
|
197
|
+
mix_channel(color_green(from), color_green(to), amount),
|
|
198
|
+
mix_channel(color_blue(from), color_blue(to), amount),
|
|
199
|
+
mix_channel(color_alpha(from), color_alpha(to), amount),
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn with_alpha(color: u32, alpha: u32) -> u32 {
|
|
204
|
+
rgba(
|
|
205
|
+
color_red(color),
|
|
206
|
+
color_green(color),
|
|
207
|
+
color_blue(color),
|
|
208
|
+
alpha,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn normalize_accent_color(color: u32) -> u32 {
|
|
213
|
+
if color == 0 {
|
|
214
|
+
return DEFAULT_ACCENT_COLOR;
|
|
215
|
+
}
|
|
216
|
+
let alpha = color_alpha(color);
|
|
217
|
+
if alpha == 0 {
|
|
218
|
+
return with_alpha(color, 0xff);
|
|
219
|
+
}
|
|
220
|
+
color
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn pick_accent_foreground(accent: u32) -> u32 {
|
|
224
|
+
let brightness = color_red(accent) as f32 * 0.2126
|
|
225
|
+
+ color_green(accent) as f32 * 0.7152
|
|
226
|
+
+ color_blue(accent) as f32 * 0.0722;
|
|
227
|
+
if brightness < 160.0 {
|
|
228
|
+
WHITE
|
|
229
|
+
} else {
|
|
230
|
+
BLACK
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn estimate_theme_dark(theme: &Theme) -> bool {
|
|
235
|
+
let background = theme.colors.background;
|
|
236
|
+
let luminance = color_red(background) as f32 * 0.2126
|
|
237
|
+
+ color_green(background) as f32 * 0.7152
|
|
238
|
+
+ color_blue(background) as f32 * 0.0722;
|
|
239
|
+
luminance < 128.0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pub fn generate_theme(is_dark: bool, accent_color: u32) -> Theme {
|
|
243
|
+
let fonts = default_fonts();
|
|
244
|
+
let accent = normalize_accent_color(accent_color);
|
|
245
|
+
let background = if is_dark {
|
|
246
|
+
rgba(0x04, 0x0a, 0x14, 0xff)
|
|
247
|
+
} else {
|
|
248
|
+
rgba(0xf8, 0xfa, 0xfc, 0xff)
|
|
249
|
+
};
|
|
250
|
+
let surface = if is_dark {
|
|
251
|
+
rgba(0x0f, 0x17, 0x28, 0xff)
|
|
252
|
+
} else {
|
|
253
|
+
WHITE
|
|
254
|
+
};
|
|
255
|
+
let text_primary = if is_dark {
|
|
256
|
+
rgba(0xf8, 0xfa, 0xfc, 0xff)
|
|
257
|
+
} else {
|
|
258
|
+
rgba(0x0f, 0x17, 0x2a, 0xff)
|
|
259
|
+
};
|
|
260
|
+
let text_muted = if is_dark {
|
|
261
|
+
rgba(0x94, 0xa3, 0xb8, 0xff)
|
|
262
|
+
} else {
|
|
263
|
+
rgba(0x47, 0x55, 0x69, 0xff)
|
|
264
|
+
};
|
|
265
|
+
let text_on_accent = pick_accent_foreground(accent);
|
|
266
|
+
let border = if is_dark {
|
|
267
|
+
rgba(0x24, 0x3b, 0x53, 0xff)
|
|
268
|
+
} else {
|
|
269
|
+
rgba(0xcb, 0xd5, 0xe1, 0xff)
|
|
270
|
+
};
|
|
271
|
+
let accent_hovered = if is_dark {
|
|
272
|
+
mix_color(accent, WHITE, 0.14)
|
|
273
|
+
} else {
|
|
274
|
+
mix_color(accent, WHITE, 0.10)
|
|
275
|
+
};
|
|
276
|
+
let accent_pressed = if is_dark {
|
|
277
|
+
mix_color(accent, BLACK, 0.24)
|
|
278
|
+
} else {
|
|
279
|
+
mix_color(accent, BLACK, 0.16)
|
|
280
|
+
};
|
|
281
|
+
let selection = with_alpha(accent, if is_dark { 0x40 } else { 0x33 });
|
|
282
|
+
let scrollbar_track = if is_dark {
|
|
283
|
+
rgba(0x12, 0x21, 0x33, 0xff)
|
|
284
|
+
} else {
|
|
285
|
+
rgba(0xe2, 0xe8, 0xf0, 0xff)
|
|
286
|
+
};
|
|
287
|
+
let scrollbar_thumb = if is_dark {
|
|
288
|
+
mix_color(accent, surface, 0.55)
|
|
289
|
+
} else {
|
|
290
|
+
mix_color(accent, surface, 0.40)
|
|
291
|
+
};
|
|
292
|
+
let dialog_backdrop = if is_dark {
|
|
293
|
+
rgba(0x00, 0x00, 0x00, 0x24)
|
|
294
|
+
} else {
|
|
295
|
+
rgba(0x00, 0x00, 0x00, 0x18)
|
|
296
|
+
};
|
|
297
|
+
let dialog_shadow = if is_dark {
|
|
298
|
+
rgba(0x00, 0x00, 0x00, 0xd8)
|
|
299
|
+
} else {
|
|
300
|
+
rgba(0x00, 0x00, 0x00, 0x88)
|
|
301
|
+
};
|
|
302
|
+
let panel_shadow = with_alpha(
|
|
303
|
+
dialog_shadow,
|
|
304
|
+
(color_alpha(dialog_shadow) as f32 * 0.30).round() as u32,
|
|
305
|
+
);
|
|
306
|
+
let context_menu_panel_background = if is_dark {
|
|
307
|
+
rgba(0x18, 0x1d, 0x26, 0xd8)
|
|
308
|
+
} else {
|
|
309
|
+
rgba(0xff, 0xff, 0xff, 0xdc)
|
|
310
|
+
};
|
|
311
|
+
let context_menu_panel_border_color = if is_dark {
|
|
312
|
+
rgba(0xff, 0xff, 0xff, 0x10)
|
|
313
|
+
} else {
|
|
314
|
+
rgba(0x0f, 0x17, 0x2a, 0x14)
|
|
315
|
+
};
|
|
316
|
+
let context_menu_item_hover = if is_dark {
|
|
317
|
+
rgba(0xff, 0xff, 0xff, 0x0c)
|
|
318
|
+
} else {
|
|
319
|
+
rgba(0x0f, 0x17, 0x2a, 0x08)
|
|
320
|
+
};
|
|
321
|
+
let context_menu_separator_color = if is_dark {
|
|
322
|
+
rgba(0xff, 0xff, 0xff, 0x10)
|
|
323
|
+
} else {
|
|
324
|
+
rgba(0x0f, 0x17, 0x2a, 0x12)
|
|
325
|
+
};
|
|
326
|
+
let tool_tip_panel_background = if is_dark {
|
|
327
|
+
rgba(0x11, 0x17, 0x20, 0xf0)
|
|
328
|
+
} else {
|
|
329
|
+
rgba(0xff, 0xff, 0xff, 0xf8)
|
|
330
|
+
};
|
|
331
|
+
let tool_tip_panel_border_color = if is_dark {
|
|
332
|
+
rgba(0xff, 0xff, 0xff, 0x12)
|
|
333
|
+
} else {
|
|
334
|
+
rgba(0x0f, 0x17, 0x2a, 0x12)
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
Theme {
|
|
338
|
+
colors: Colors {
|
|
339
|
+
background,
|
|
340
|
+
surface,
|
|
341
|
+
text_primary,
|
|
342
|
+
text_muted,
|
|
343
|
+
text_on_accent,
|
|
344
|
+
accent,
|
|
345
|
+
accent_pressed,
|
|
346
|
+
accent_hovered,
|
|
347
|
+
border,
|
|
348
|
+
selection,
|
|
349
|
+
scrollbar_track,
|
|
350
|
+
scrollbar_thumb,
|
|
351
|
+
dialog_backdrop,
|
|
352
|
+
dialog_shadow,
|
|
353
|
+
panel_shadow,
|
|
354
|
+
focus_ring: accent,
|
|
355
|
+
},
|
|
356
|
+
spacing: DEFAULT_SPACING,
|
|
357
|
+
fonts: fonts.clone(),
|
|
358
|
+
context_menu: ContextMenuTheme {
|
|
359
|
+
panel_background: context_menu_panel_background,
|
|
360
|
+
panel_border_color: context_menu_panel_border_color,
|
|
361
|
+
panel_shadow_color: panel_shadow,
|
|
362
|
+
panel_corner_radius: if is_dark { 16.0 } else { 14.0 },
|
|
363
|
+
separator_color: context_menu_separator_color,
|
|
364
|
+
shadow_offset_y: 12.0,
|
|
365
|
+
shadow_blur: 28.0,
|
|
366
|
+
shadow_spread: 0.0,
|
|
367
|
+
item: ContextMenuItemTheme {
|
|
368
|
+
background: rgba(0, 0, 0, 0),
|
|
369
|
+
hover_background: context_menu_item_hover,
|
|
370
|
+
text_color: text_primary,
|
|
371
|
+
corner_radius: if is_dark { 10.0 } else { 9.0 },
|
|
372
|
+
font_family: fonts.body_family.clone(),
|
|
373
|
+
font_size: 13.0,
|
|
374
|
+
height: 30.0,
|
|
375
|
+
padding_left: 12.0,
|
|
376
|
+
padding_top: 6.0,
|
|
377
|
+
padding_right: 12.0,
|
|
378
|
+
padding_bottom: 6.0,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
tool_tip: ToolTipTheme {
|
|
382
|
+
panel_background: tool_tip_panel_background,
|
|
383
|
+
panel_border_color: tool_tip_panel_border_color,
|
|
384
|
+
panel_shadow_color: panel_shadow,
|
|
385
|
+
panel_corner_radius: if is_dark { 12.0 } else { 10.0 },
|
|
386
|
+
text_color: text_primary,
|
|
387
|
+
font_family: fonts.body_family.clone(),
|
|
388
|
+
font_size: 13.0,
|
|
389
|
+
max_width: 280.0,
|
|
390
|
+
padding_left: 10.0,
|
|
391
|
+
padding_top: 7.0,
|
|
392
|
+
padding_right: 10.0,
|
|
393
|
+
padding_bottom: 7.0,
|
|
394
|
+
shadow_offset_y: 10.0,
|
|
395
|
+
shadow_blur: 24.0,
|
|
396
|
+
shadow_spread: 0.0,
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
pub fn default_dark_theme() -> Theme {
|
|
402
|
+
generate_theme(true, DEFAULT_ACCENT_COLOR)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
pub fn default_light_theme() -> Theme {
|
|
406
|
+
generate_theme(false, DEFAULT_ACCENT_COLOR)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
pub fn current_theme() -> Theme {
|
|
410
|
+
THEME_STATE.with(|slot| slot.borrow().signal.get())
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
pub fn subscribe(handler: impl Fn(Theme) + 'static) -> SubscriptionGuard {
|
|
414
|
+
handler(current_theme());
|
|
415
|
+
let guard = THEME_STATE.with(|slot| {
|
|
416
|
+
let callback: Callback = Rc::new(move || handler(current_theme()));
|
|
417
|
+
slot.borrow_mut().signal.subscribe(callback)
|
|
418
|
+
});
|
|
419
|
+
guard
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
pub fn bind_theme(handler: impl Fn(Theme) + 'static) -> SubscriptionGuard {
|
|
423
|
+
subscribe(handler)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn apply_theme(theme: Theme, source: ThemeSource, is_dark: bool) -> Theme {
|
|
427
|
+
let callbacks = THEME_STATE.with(|slot| {
|
|
428
|
+
let mut state = slot.borrow_mut();
|
|
429
|
+
state.theme_source = source;
|
|
430
|
+
state.current_dark_mode = is_dark;
|
|
431
|
+
state.signal.set(theme.clone())
|
|
432
|
+
});
|
|
433
|
+
if let Some(callbacks) = callbacks {
|
|
434
|
+
for callback in callbacks {
|
|
435
|
+
callback();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
theme
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
fn apply_system_theme() -> Theme {
|
|
442
|
+
THEME_STATE
|
|
443
|
+
.with(|slot| {
|
|
444
|
+
let state = slot.borrow();
|
|
445
|
+
generate_theme(state.system_dark_mode, state.system_accent_color)
|
|
446
|
+
})
|
|
447
|
+
.pipe(|theme| {
|
|
448
|
+
let is_dark = estimate_theme_dark(&theme);
|
|
449
|
+
apply_theme(theme, ThemeSource::System, is_dark)
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pub fn use_system_theme() -> Theme {
|
|
454
|
+
THEME_STATE.with(|slot| {
|
|
455
|
+
let mut state = slot.borrow_mut();
|
|
456
|
+
state.system_dark_mode = framework_host_services::fui_is_dark_mode();
|
|
457
|
+
state.system_accent_color =
|
|
458
|
+
normalize_accent_color(framework_host_services::fui_get_accent_color());
|
|
459
|
+
});
|
|
460
|
+
apply_system_theme()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
pub fn use_custom_theme(theme: Theme) -> Theme {
|
|
464
|
+
apply_theme(
|
|
465
|
+
theme.clone(),
|
|
466
|
+
ThemeSource::Custom,
|
|
467
|
+
estimate_theme_dark(&theme),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
pub fn set_accent_color(color: u32) -> Theme {
|
|
472
|
+
use_custom_theme(generate_theme(is_dark_mode(), color))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
pub fn is_dark_mode() -> bool {
|
|
476
|
+
THEME_STATE.with(|slot| slot.borrow().current_dark_mode)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
pub fn is_using_system_theme() -> bool {
|
|
480
|
+
THEME_STATE.with(|slot| slot.borrow().theme_source == ThemeSource::System)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pub fn handle_system_dark_mode_changed(is_dark: bool) -> Theme {
|
|
484
|
+
THEME_STATE.with(|slot| {
|
|
485
|
+
let mut state = slot.borrow_mut();
|
|
486
|
+
state.system_dark_mode = is_dark;
|
|
487
|
+
if state.theme_source != ThemeSource::System {
|
|
488
|
+
return state.signal.get();
|
|
489
|
+
}
|
|
490
|
+
state.system_accent_color =
|
|
491
|
+
normalize_accent_color(framework_host_services::fui_get_accent_color());
|
|
492
|
+
let theme = generate_theme(state.system_dark_mode, state.system_accent_color);
|
|
493
|
+
drop(state);
|
|
494
|
+
apply_theme(theme, ThemeSource::System, is_dark)
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
pub fn handle_system_accent_color_changed(color: u32) -> Theme {
|
|
499
|
+
THEME_STATE.with(|slot| {
|
|
500
|
+
let mut state = slot.borrow_mut();
|
|
501
|
+
state.system_accent_color = normalize_accent_color(color);
|
|
502
|
+
if state.theme_source != ThemeSource::System {
|
|
503
|
+
return state.signal.get();
|
|
504
|
+
}
|
|
505
|
+
let theme = generate_theme(state.system_dark_mode, state.system_accent_color);
|
|
506
|
+
let is_dark = estimate_theme_dark(&theme);
|
|
507
|
+
drop(state);
|
|
508
|
+
apply_theme(theme, ThemeSource::System, is_dark)
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
trait Pipe: Sized {
|
|
513
|
+
fn pipe<T>(self, callback: impl FnOnce(Self) -> T) -> T {
|
|
514
|
+
callback(self)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
impl<T> Pipe for T {}
|
|
519
|
+
|
|
520
|
+
#[no_mangle]
|
|
521
|
+
pub extern "C" fn __fui_on_system_dark_mode_changed(is_dark: bool) {
|
|
522
|
+
handle_system_dark_mode_changed(is_dark);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[no_mangle]
|
|
526
|
+
pub extern "C" fn __fui_on_system_accent_color_changed(color: u32) {
|
|
527
|
+
handle_system_accent_color_changed(color);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#[cfg(test)]
|
|
531
|
+
mod tests {
|
|
532
|
+
use super::{
|
|
533
|
+
current_theme, handle_system_accent_color_changed, handle_system_dark_mode_changed,
|
|
534
|
+
is_dark_mode, subscribe, use_system_theme,
|
|
535
|
+
};
|
|
536
|
+
use crate::ffi;
|
|
537
|
+
use std::cell::Cell;
|
|
538
|
+
use std::rc::Rc;
|
|
539
|
+
|
|
540
|
+
#[test]
|
|
541
|
+
fn uses_host_system_theme_values() {
|
|
542
|
+
ffi::test::reset();
|
|
543
|
+
ffi::test::set_system_dark_mode(false);
|
|
544
|
+
ffi::test::set_system_accent_color(0xFF0000FF);
|
|
545
|
+
let theme = use_system_theme();
|
|
546
|
+
assert!(!is_dark_mode());
|
|
547
|
+
assert_eq!(theme.colors.accent, 0xFF0000FF);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn system_callbacks_update_active_theme() {
|
|
552
|
+
ffi::test::reset();
|
|
553
|
+
ffi::test::set_system_accent_color(0x00FF00FF);
|
|
554
|
+
handle_system_dark_mode_changed(false);
|
|
555
|
+
let theme = handle_system_accent_color_changed(0x112233FF);
|
|
556
|
+
assert!(!is_dark_mode());
|
|
557
|
+
assert_eq!(theme.colors.accent, 0x112233FF);
|
|
558
|
+
assert_eq!(current_theme().colors.accent, 0x112233FF);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[test]
|
|
562
|
+
fn subscribe_invokes_immediately() {
|
|
563
|
+
ffi::test::reset();
|
|
564
|
+
let count = Rc::new(Cell::new(0));
|
|
565
|
+
let counter = count.clone();
|
|
566
|
+
let _guard = subscribe(move |_theme| {
|
|
567
|
+
counter.set(counter.get() + 1);
|
|
568
|
+
});
|
|
569
|
+
assert_eq!(count.get(), 1);
|
|
570
|
+
}
|
|
571
|
+
}
|
package/src/timers.rs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
use crate::ffi;
|
|
2
|
+
use std::cell::{Cell, RefCell};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
5
|
+
type TimerCallback = Box<dyn Fn()>;
|
|
6
|
+
|
|
7
|
+
thread_local! {
|
|
8
|
+
static NEXT_TIMER_ID: Cell<u32> = const { Cell::new(1) };
|
|
9
|
+
static ACTIVE_TIMERS: RefCell<HashMap<u32, TimerCallback>> = RefCell::new(HashMap::new());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
13
|
+
pub struct TimerHandle(u32);
|
|
14
|
+
|
|
15
|
+
impl TimerHandle {
|
|
16
|
+
pub fn raw(self) -> u32 {
|
|
17
|
+
self.0
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn set_timeout(delay_ms: i32, callback: impl Fn() + 'static) -> TimerHandle {
|
|
22
|
+
let timer_id = NEXT_TIMER_ID.with(|next| {
|
|
23
|
+
let timer_id = next.get();
|
|
24
|
+
next.set(timer_id.saturating_add(1));
|
|
25
|
+
timer_id
|
|
26
|
+
});
|
|
27
|
+
ACTIVE_TIMERS.with(|timers| {
|
|
28
|
+
timers.borrow_mut().insert(timer_id, Box::new(callback));
|
|
29
|
+
});
|
|
30
|
+
unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
|
|
31
|
+
TimerHandle(timer_id)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(crate) fn schedule_internal_timer(timer_id: u32, delay_ms: i32, callback: impl Fn() + 'static) {
|
|
35
|
+
ACTIVE_TIMERS.with(|timers| {
|
|
36
|
+
timers.borrow_mut().insert(timer_id, Box::new(callback));
|
|
37
|
+
});
|
|
38
|
+
unsafe { ffi::fui_start_timer(timer_id, delay_ms) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub(crate) fn cancel_internal_timer(timer_id: u32) -> bool {
|
|
42
|
+
let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id).is_some());
|
|
43
|
+
if removed {
|
|
44
|
+
unsafe { ffi::fui_cancel_timer(timer_id) };
|
|
45
|
+
}
|
|
46
|
+
removed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn cancel_timeout(handle: TimerHandle) -> bool {
|
|
50
|
+
let removed = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&handle.0).is_some());
|
|
51
|
+
if removed {
|
|
52
|
+
unsafe { ffi::fui_cancel_timer(handle.0) };
|
|
53
|
+
}
|
|
54
|
+
removed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn cancel_all_timers() {
|
|
58
|
+
ACTIVE_TIMERS.with(|timers| timers.borrow_mut().clear());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[no_mangle]
|
|
62
|
+
pub extern "C" fn __fui_on_timer(timer_id: u32) {
|
|
63
|
+
let callback = ACTIVE_TIMERS.with(|timers| timers.borrow_mut().remove(&timer_id));
|
|
64
|
+
if let Some(callback) = callback {
|
|
65
|
+
callback();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
use crate::ffi::{self, Call};
|
|
72
|
+
use std::cell::Cell;
|
|
73
|
+
use std::rc::Rc;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn timer_callback_fires_once() {
|
|
77
|
+
ffi::test::reset();
|
|
78
|
+
let fired = Rc::new(Cell::new(0));
|
|
79
|
+
let handle = super::set_timeout(25, || {});
|
|
80
|
+
let timer_id = handle.raw();
|
|
81
|
+
let calls = ffi::test::take_calls();
|
|
82
|
+
assert!(calls.iter().any(|call| matches!(
|
|
83
|
+
call,
|
|
84
|
+
Call::StartTimer { timer_id: captured, delay_ms } if *captured == timer_id && *delay_ms == 25
|
|
85
|
+
)));
|
|
86
|
+
|
|
87
|
+
let fired_clone = fired.clone();
|
|
88
|
+
let handle = super::set_timeout(10, move || fired_clone.set(fired_clone.get() + 1));
|
|
89
|
+
super::__fui_on_timer(handle.raw());
|
|
90
|
+
super::__fui_on_timer(handle.raw());
|
|
91
|
+
assert_eq!(fired.get(), 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn cancel_timeout_emits_host_cancel() {
|
|
96
|
+
ffi::test::reset();
|
|
97
|
+
let handle = super::set_timeout(100, || {});
|
|
98
|
+
ffi::test::take_calls();
|
|
99
|
+
assert!(super::cancel_timeout(handle));
|
|
100
|
+
let calls = ffi::test::take_calls();
|
|
101
|
+
assert!(calls.iter().any(|call| matches!(
|
|
102
|
+
call,
|
|
103
|
+
Call::CancelTimer { timer_id } if *timer_id == handle.raw()
|
|
104
|
+
)));
|
|
105
|
+
}
|
|
106
|
+
}
|