elit 3.5.6 → 3.5.7

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 (113) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/package.json +6 -3
  26. package/dist/build.d.mts +0 -20
  27. package/dist/chokidar.d.mts +0 -134
  28. package/dist/cli.d.mts +0 -81
  29. package/dist/config.d.mts +0 -254
  30. package/dist/coverage.d.mts +0 -85
  31. package/dist/database.d.mts +0 -52
  32. package/dist/desktop.d.mts +0 -68
  33. package/dist/dom.d.mts +0 -87
  34. package/dist/el.d.mts +0 -208
  35. package/dist/fs.d.mts +0 -255
  36. package/dist/hmr.d.mts +0 -38
  37. package/dist/http.d.mts +0 -169
  38. package/dist/https.d.mts +0 -108
  39. package/dist/index.d.mts +0 -13
  40. package/dist/mime-types.d.mts +0 -48
  41. package/dist/native.d.mts +0 -136
  42. package/dist/path.d.mts +0 -163
  43. package/dist/router.d.mts +0 -49
  44. package/dist/runtime.d.mts +0 -97
  45. package/dist/server-D0Dp4R5z.d.mts +0 -449
  46. package/dist/server.d.mts +0 -7
  47. package/dist/state.d.mts +0 -117
  48. package/dist/style.d.mts +0 -232
  49. package/dist/test-reporter.d.mts +0 -77
  50. package/dist/test-runtime.d.mts +0 -122
  51. package/dist/test.d.mts +0 -39
  52. package/dist/types.d.mts +0 -586
  53. package/dist/universal.d.mts +0 -21
  54. package/dist/ws.d.mts +0 -200
  55. package/dist/wss.d.mts +0 -108
  56. package/src/build.ts +0 -362
  57. package/src/chokidar.ts +0 -427
  58. package/src/cli.ts +0 -1162
  59. package/src/config.ts +0 -509
  60. package/src/coverage.ts +0 -1479
  61. package/src/database.ts +0 -1410
  62. package/src/desktop-auto-render.ts +0 -317
  63. package/src/desktop-cli.ts +0 -1533
  64. package/src/desktop.ts +0 -99
  65. package/src/dev-build.ts +0 -340
  66. package/src/dom.ts +0 -901
  67. package/src/el.ts +0 -183
  68. package/src/fs.ts +0 -609
  69. package/src/hmr.ts +0 -149
  70. package/src/http.ts +0 -856
  71. package/src/https.ts +0 -411
  72. package/src/index.ts +0 -16
  73. package/src/mime-types.ts +0 -222
  74. package/src/mobile-cli.ts +0 -2313
  75. package/src/native-background.ts +0 -444
  76. package/src/native-border.ts +0 -343
  77. package/src/native-canvas.ts +0 -260
  78. package/src/native-cli.ts +0 -414
  79. package/src/native-color.ts +0 -904
  80. package/src/native-estimation.ts +0 -194
  81. package/src/native-grid.ts +0 -590
  82. package/src/native-interaction.ts +0 -1289
  83. package/src/native-layout.ts +0 -568
  84. package/src/native-link.ts +0 -76
  85. package/src/native-render-support.ts +0 -361
  86. package/src/native-spacing.ts +0 -231
  87. package/src/native-state.ts +0 -318
  88. package/src/native-strings.ts +0 -46
  89. package/src/native-transform.ts +0 -120
  90. package/src/native-types.ts +0 -439
  91. package/src/native-typography.ts +0 -254
  92. package/src/native-units.ts +0 -441
  93. package/src/native-vector.ts +0 -910
  94. package/src/native.ts +0 -5606
  95. package/src/path.ts +0 -493
  96. package/src/pm-cli.ts +0 -2498
  97. package/src/preview-build.ts +0 -294
  98. package/src/render-context.ts +0 -138
  99. package/src/router.ts +0 -260
  100. package/src/runtime.ts +0 -97
  101. package/src/server.ts +0 -2294
  102. package/src/state.ts +0 -556
  103. package/src/style.ts +0 -1790
  104. package/src/test-globals.d.ts +0 -184
  105. package/src/test-reporter.ts +0 -609
  106. package/src/test-runtime.ts +0 -1359
  107. package/src/test.ts +0 -368
  108. package/src/types.ts +0 -381
  109. package/src/universal.ts +0 -81
  110. package/src/wapk-cli.ts +0 -3213
  111. package/src/workspace-package.ts +0 -102
  112. package/src/ws.ts +0 -648
  113. package/src/wss.ts +0 -241
@@ -0,0 +1,135 @@
1
+ use std::path::{Path, PathBuf};
2
+
3
+ use eframe::egui::{self, Vec2};
4
+ use serde_json::{Map, Value};
5
+
6
+ use super::utilities::{
7
+ is_probably_inline_html, load_color_image, resolve_resource_path, strip_markup_tags,
8
+ truncate_preview,
9
+ };
10
+ use super::{
11
+ DesktopInteraction, DesktopNativeApp, NativeDesktopCanvasSpec, NativeDesktopVectorSpec,
12
+ NativeElementNode,
13
+ };
14
+
15
+ impl DesktopNativeApp {
16
+ fn resource_base_dir(&self) -> Option<&Path> {
17
+ self.resource_base_dir.as_deref()
18
+ }
19
+
20
+ pub(super) fn resolve_resource_path(&self, source: &str) -> Option<PathBuf> {
21
+ resolve_resource_path(self.resource_base_dir(), source)
22
+ }
23
+
24
+ pub(super) fn read_source_preview(&self, source: &str) -> Option<String> {
25
+ if is_probably_inline_html(source) {
26
+ return Some(truncate_preview(&strip_markup_tags(source), 360));
27
+ }
28
+
29
+ let path = self.resolve_resource_path(source)?;
30
+ let content = std::fs::read_to_string(path).ok()?;
31
+ Some(truncate_preview(&strip_markup_tags(&content), 360))
32
+ }
33
+
34
+ pub(super) fn get_vector_spec(&self, node: &NativeElementNode) -> Option<NativeDesktopVectorSpec> {
35
+ serde_json::from_value(node.props.get("desktopVectorSpec")?.clone()).ok()
36
+ }
37
+
38
+ pub(super) fn get_canvas_spec(&self, node: &NativeElementNode) -> Option<NativeDesktopCanvasSpec> {
39
+ serde_json::from_value(node.props.get("desktopCanvasSpec")?.clone()).ok()
40
+ }
41
+
42
+ fn resolved_window_title(&self) -> String {
43
+ self.navigation
44
+ .current_route()
45
+ .map(|route| format!("{} - {}", self.base_title, route))
46
+ .unwrap_or_else(|| self.base_title.clone())
47
+ }
48
+
49
+ pub(super) fn apply_navigation_title(&self, ctx: &egui::Context) {
50
+ ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.resolved_window_title()));
51
+ }
52
+
53
+ fn build_ready_interaction(&self) -> DesktopInteraction {
54
+ let mut payload = Map::new();
55
+ payload.insert(String::from("title"), Value::String(self.base_title.clone()));
56
+ payload.insert(String::from("autoClose"), Value::Bool(self.payload.window.auto_close));
57
+ if let Some(route) = self.navigation.current_route() {
58
+ payload.insert(String::from("route"), Value::String(route.to_string()));
59
+ }
60
+
61
+ DesktopInteraction {
62
+ action: Some(String::from("desktop:ready")),
63
+ route: self.navigation.current_route().map(str::to_string),
64
+ payload: Some(Value::Object(payload)),
65
+ }
66
+ }
67
+
68
+ pub(super) fn emit_ready_interaction(&mut self) {
69
+ if self.ready_emitted || !self.interaction_output.emit_ready {
70
+ return;
71
+ }
72
+
73
+ self.ready_emitted = true;
74
+ self.record_interaction(self.build_ready_interaction());
75
+ }
76
+
77
+ pub(super) fn ensure_texture_for_source(
78
+ &mut self,
79
+ ctx: &egui::Context,
80
+ source: &str,
81
+ ) -> Result<egui::TextureHandle, String> {
82
+ let resolved = self
83
+ .resolve_resource_path(source)
84
+ .ok_or_else(|| format!("unable to resolve image resource '{source}'"))?;
85
+ let key = resolved.to_string_lossy().into_owned();
86
+
87
+ if let Some(texture) = self.image_textures.get(&key) {
88
+ return Ok(texture.clone());
89
+ }
90
+
91
+ let image = load_color_image(&resolved)?;
92
+ let texture = ctx.load_texture(format!("elit-resource:{key}"), image, Default::default());
93
+ self.image_textures.insert(key, texture.clone());
94
+ Ok(texture)
95
+ }
96
+
97
+ pub(super) fn resolve_display_size(&self, node: &NativeElementNode, fallback: Vec2) -> Vec2 {
98
+ let style = self.get_style_map(node);
99
+ let width = style
100
+ .and_then(|style| self.parse_css_number_with_viewport(style.get("width")))
101
+ .or_else(|| self.resolve_prop_number(node, "width"));
102
+ let height = style
103
+ .and_then(|style| self.parse_css_number_with_viewport(style.get("height")))
104
+ .or_else(|| self.resolve_prop_number(node, "height"));
105
+ let max_width = style.and_then(|style| self.parse_css_number_with_viewport(style.get("maxWidth")));
106
+ let max_height = style.and_then(|style| self.parse_css_number_with_viewport(style.get("maxHeight")));
107
+
108
+ let mut size = fallback;
109
+ match (width, height) {
110
+ (Some(width), Some(height)) => {
111
+ size = Vec2::new(width.max(1.0), height.max(1.0));
112
+ }
113
+ (Some(width), None) => {
114
+ let scale = if fallback.x > 0.0 { width / fallback.x } else { 1.0 };
115
+ size = Vec2::new(width.max(1.0), (fallback.y * scale).max(1.0));
116
+ }
117
+ (None, Some(height)) => {
118
+ let scale = if fallback.y > 0.0 { height / fallback.y } else { 1.0 };
119
+ size = Vec2::new((fallback.x * scale).max(1.0), height.max(1.0));
120
+ }
121
+ (None, None) => {}
122
+ }
123
+
124
+ if let Some(max_width) = max_width {
125
+ let scale = if size.x > 0.0 { (max_width / size.x).min(1.0) } else { 1.0 };
126
+ size *= scale;
127
+ }
128
+ if let Some(max_height) = max_height {
129
+ let scale = if size.y > 0.0 { (max_height / size.y).min(1.0) } else { 1.0 };
130
+ size *= scale;
131
+ }
132
+
133
+ Vec2::new(size.x.max(1.0), size.y.max(1.0))
134
+ }
135
+ }
@@ -0,0 +1,495 @@
1
+ use std::fs::{OpenOptions, create_dir_all};
2
+ use std::io::Write;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ #[cfg(not(target_os = "macos"))]
6
+ use tao::dpi::LogicalSize;
7
+ #[cfg(not(target_os = "macos"))]
8
+ use tao::event::{Event, WindowEvent};
9
+ #[cfg(not(target_os = "macos"))]
10
+ use tao::event_loop::{ControlFlow, EventLoopBuilder};
11
+ #[cfg(not(target_os = "macos"))]
12
+ use tao::window::WindowBuilder as TaoWindowBuilder;
13
+ #[cfg(not(target_os = "macos"))]
14
+ use wry::WebViewBuilder;
15
+
16
+ use crate::icon::load_icon_bitmap;
17
+ use eframe::egui::{self, Color32, Pos2};
18
+ use image::GenericImageView;
19
+ use serde_json::{Map, Value};
20
+
21
+ use super::app_models::{DesktopControlEventData, DesktopInteraction};
22
+ use super::{NativeDesktopColor, NativeElementNode};
23
+
24
+ pub(crate) fn configure_native_context_rendering(ctx: &egui::Context) {
25
+ ctx.tessellation_options_mut(|options| {
26
+ options.round_text_to_pixels = true;
27
+ });
28
+ }
29
+
30
+ pub(crate) fn load_window_icon(path: Option<&str>) -> Option<egui::IconData> {
31
+ let path = Path::new(path?);
32
+ let bitmap = load_icon_bitmap(path)
33
+ .map_err(|error| {
34
+ eprintln!("failed to load native desktop icon '{}': {}", path.display(), error);
35
+ error
36
+ })
37
+ .ok()?;
38
+ let (rgba, width, height) = bitmap.into_rgba();
39
+ Some(egui::IconData { rgba, width, height })
40
+ }
41
+
42
+ pub(crate) fn format_number(value: f64) -> String {
43
+ if value.fract().abs() < f64::EPSILON {
44
+ format!("{value:.0}")
45
+ } else {
46
+ value.to_string()
47
+ }
48
+ }
49
+
50
+ pub(crate) fn color32_from_native_color(color: &NativeDesktopColor) -> Color32 {
51
+ Color32::from_rgba_unmultiplied(
52
+ color.red.clamp(0.0, 255.0).round() as u8,
53
+ color.green.clamp(0.0, 255.0).round() as u8,
54
+ color.blue.clamp(0.0, 255.0).round() as u8,
55
+ (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8,
56
+ )
57
+ }
58
+
59
+ pub(crate) fn visible_color(color: Option<&NativeDesktopColor>) -> Option<Color32> {
60
+ let resolved = color.map(color32_from_native_color)?;
61
+ (resolved.a() > 0).then_some(resolved)
62
+ }
63
+
64
+ pub(crate) fn value_as_string(value: &Value) -> Option<String> {
65
+ match value {
66
+ Value::String(text) => Some(text.clone()),
67
+ Value::Number(number) => Some(number.to_string()),
68
+ Value::Bool(boolean) => Some(boolean.to_string()),
69
+ _ => None,
70
+ }
71
+ }
72
+
73
+ pub(crate) fn parse_native_bool(value: Option<&Value>) -> bool {
74
+ match value {
75
+ Some(Value::Bool(value)) => *value,
76
+ Some(Value::Number(value)) => value.as_i64().map(|number| number != 0).unwrap_or(false),
77
+ Some(Value::String(text)) => matches!(
78
+ text.trim().to_ascii_lowercase().as_str(),
79
+ "true" | "1" | "yes" | "on" | "checked" | "selected" | "disabled" | "readonly" | "multiple"
80
+ ),
81
+ _ => false,
82
+ }
83
+ }
84
+
85
+ pub(crate) fn resolve_surface_source(node: &NativeElementNode) -> Option<String> {
86
+ ["source", "src", "data", "destination"]
87
+ .iter()
88
+ .find_map(|key| node.props.get(*key).and_then(value_as_string))
89
+ .map(|value| value.trim().to_string())
90
+ .filter(|value| !value.is_empty())
91
+ }
92
+
93
+ pub(crate) fn resolve_route_from_payload(payload: Option<&Value>) -> Option<String> {
94
+ match payload {
95
+ Some(Value::String(route)) if !route.trim().is_empty() => Some(route.trim().to_string()),
96
+ Some(Value::Object(payload)) => payload
97
+ .get("route")
98
+ .and_then(Value::as_str)
99
+ .map(str::trim)
100
+ .filter(|route| !route.is_empty())
101
+ .map(str::to_string),
102
+ _ => None,
103
+ }
104
+ }
105
+
106
+ pub(crate) fn strip_markup_tags(input: &str) -> String {
107
+ let mut output = String::new();
108
+ let mut inside_tag = false;
109
+
110
+ for character in input.chars() {
111
+ match character {
112
+ '<' => {
113
+ inside_tag = true;
114
+ if !output.ends_with(' ') {
115
+ output.push(' ');
116
+ }
117
+ }
118
+ '>' => {
119
+ inside_tag = false;
120
+ }
121
+ _ if !inside_tag => output.push(character),
122
+ _ => {}
123
+ }
124
+ }
125
+
126
+ output.split_whitespace().collect::<Vec<_>>().join(" ")
127
+ }
128
+
129
+ pub(crate) fn is_probably_inline_html(source: &str) -> bool {
130
+ let trimmed = source.trim_start();
131
+ trimmed.starts_with('<') || trimmed.contains("<html") || trimmed.contains("<body")
132
+ }
133
+
134
+ pub(crate) fn truncate_preview(text: &str, max_chars: usize) -> String {
135
+ let trimmed = text.trim();
136
+ if trimmed.chars().count() <= max_chars {
137
+ return trimmed.to_string();
138
+ }
139
+
140
+ let truncated = trimmed.chars().take(max_chars).collect::<String>();
141
+ format!("{truncated}...")
142
+ }
143
+
144
+ pub(crate) fn cubic_bezier_point(start: Pos2, control1: Pos2, control2: Pos2, end: Pos2, t: f32) -> Pos2 {
145
+ let inverse_t = 1.0 - t;
146
+ let inverse_t2 = inverse_t * inverse_t;
147
+ let inverse_t3 = inverse_t2 * inverse_t;
148
+ let t2 = t * t;
149
+ let t3 = t2 * t;
150
+
151
+ Pos2::new(
152
+ inverse_t3 * start.x
153
+ + 3.0 * inverse_t2 * t * control1.x
154
+ + 3.0 * inverse_t * t2 * control2.x
155
+ + t3 * end.x,
156
+ inverse_t3 * start.y
157
+ + 3.0 * inverse_t2 * t * control1.y
158
+ + 3.0 * inverse_t * t2 * control2.y
159
+ + t3 * end.y,
160
+ )
161
+ }
162
+
163
+ pub(crate) fn is_external_destination(destination: &str) -> bool {
164
+ let normalized = destination.trim().to_ascii_lowercase();
165
+ normalized.starts_with("http://")
166
+ || normalized.starts_with("https://")
167
+ || normalized.starts_with("mailto:")
168
+ || normalized.starts_with("tel:")
169
+ || normalized.starts_with("//")
170
+ }
171
+
172
+ pub(crate) fn resolve_resource_path(resource_base_dir: Option<&Path>, source: &str) -> Option<PathBuf> {
173
+ let trimmed = source.trim();
174
+ if trimmed.is_empty() || is_external_destination(trimmed) || trimmed.starts_with("data:") {
175
+ return None;
176
+ }
177
+
178
+ let candidate = PathBuf::from(trimmed);
179
+ if candidate.is_absolute() {
180
+ return candidate.exists().then_some(candidate);
181
+ }
182
+
183
+ if let Some(base_dir) = resource_base_dir {
184
+ let joined = base_dir.join(trimmed);
185
+ if joined.exists() {
186
+ return Some(joined);
187
+ }
188
+ }
189
+
190
+ if let Ok(current_dir) = std::env::current_dir() {
191
+ let joined = current_dir.join(trimmed);
192
+ if joined.exists() {
193
+ return Some(joined);
194
+ }
195
+ }
196
+
197
+ candidate.exists().then_some(candidate)
198
+ }
199
+
200
+ #[cfg(not(target_os = "macos"))]
201
+ pub(crate) fn spawn_native_surface_window(
202
+ title: String,
203
+ initial_width: f64,
204
+ initial_height: f64,
205
+ url: Option<String>,
206
+ html: Option<String>,
207
+ ) -> Result<(), String> {
208
+ std::thread::Builder::new()
209
+ .name(String::from("elit-native-surface"))
210
+ .spawn(move || {
211
+ let event_loop = EventLoopBuilder::<()>::with_user_event().build();
212
+ let window = TaoWindowBuilder::new()
213
+ .with_title(&title)
214
+ .with_inner_size(LogicalSize::new(initial_width, initial_height))
215
+ .build(&event_loop)
216
+ .expect("Failed to build native surface window");
217
+
218
+ let mut builder = WebViewBuilder::new();
219
+ builder = if let Some(url) = url.as_deref() {
220
+ builder.with_url(url)
221
+ } else if let Some(html) = html.as_deref() {
222
+ builder.with_html(html)
223
+ } else {
224
+ builder.with_html("<!DOCTYPE html><html><body></body></html>")
225
+ };
226
+
227
+ let _webview = builder.build(&window).expect("Failed to build native surface webview");
228
+ event_loop.run(move |event, _target, control_flow| {
229
+ *control_flow = ControlFlow::Wait;
230
+ if let Event::WindowEvent {
231
+ event: WindowEvent::CloseRequested,
232
+ ..
233
+ } = event
234
+ {
235
+ *control_flow = ControlFlow::Exit;
236
+ }
237
+ });
238
+ })
239
+ .map(|_| ())
240
+ .map_err(|error| error.to_string())
241
+ }
242
+
243
+ #[cfg(target_os = "macos")]
244
+ pub(crate) fn spawn_native_surface_window(
245
+ _title: String,
246
+ _initial_width: f64,
247
+ _initial_height: f64,
248
+ _url: Option<String>,
249
+ _html: Option<String>,
250
+ ) -> Result<(), String> {
251
+ Err(String::from("native surface windows are not available inside the macOS desktop renderer yet"))
252
+ }
253
+
254
+ pub(crate) fn resolve_output_path(path: &str) -> PathBuf {
255
+ let candidate = PathBuf::from(path.trim());
256
+ if candidate.is_absolute() {
257
+ return candidate;
258
+ }
259
+
260
+ if let Ok(current_dir) = std::env::current_dir() {
261
+ return current_dir.join(&candidate);
262
+ }
263
+
264
+ if let Ok(current_exe) = std::env::current_exe() {
265
+ if let Some(parent) = current_exe.parent() {
266
+ return parent.join(&candidate);
267
+ }
268
+ }
269
+
270
+ candidate
271
+ }
272
+
273
+ pub(crate) fn normalize_jsonish_value(value: Value) -> Value {
274
+ match value {
275
+ Value::String(text) => serde_json::from_str::<Value>(&text).unwrap_or(Value::String(text)),
276
+ other => other,
277
+ }
278
+ }
279
+
280
+ pub(crate) fn write_interaction_output(file_path: Option<&Path>, interaction_json: &str) -> Result<(), String> {
281
+ let Some(file_path) = file_path else {
282
+ return Ok(());
283
+ };
284
+
285
+ if let Some(parent) = file_path.parent() {
286
+ create_dir_all(parent)
287
+ .map_err(|error| format!("create interaction output dir failed for {}: {error}", parent.display()))?;
288
+ }
289
+
290
+ let mut file = OpenOptions::new()
291
+ .create(true)
292
+ .append(true)
293
+ .open(file_path)
294
+ .map_err(|error| format!("open interaction output failed for {}: {error}", file_path.display()))?;
295
+ writeln!(file, "{interaction_json}")
296
+ .map_err(|error| format!("write interaction output failed for {}: {error}", file_path.display()))
297
+ }
298
+
299
+ pub(crate) fn load_color_image(path: &Path) -> Result<egui::ColorImage, String> {
300
+ let extension = path
301
+ .extension()
302
+ .and_then(|value| value.to_str())
303
+ .map(|value| value.to_ascii_lowercase());
304
+
305
+ if extension.as_deref() == Some("svg") {
306
+ use resvg::{tiny_skia, usvg};
307
+
308
+ let svg = std::fs::read(path)
309
+ .map_err(|error| format!("read SVG failed for {}: {error}", path.display()))?;
310
+ let options = usvg::Options::default();
311
+ let tree = usvg::Tree::from_data(&svg, &options)
312
+ .map_err(|error| format!("parse SVG failed for {}: {error}", path.display()))?;
313
+ let size = tree.size();
314
+ let width = size.width().ceil().max(1.0) as u32;
315
+ let height = size.height().ceil().max(1.0) as u32;
316
+ let mut pixmap = tiny_skia::Pixmap::new(width, height)
317
+ .ok_or_else(|| format!("allocate SVG pixmap failed for {}", path.display()))?;
318
+ resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
319
+ let rgba = pixmap.take();
320
+ return Ok(egui::ColorImage::from_rgba_unmultiplied(
321
+ [width as usize, height as usize],
322
+ &rgba,
323
+ ));
324
+ }
325
+
326
+ let image = image::open(path)
327
+ .map_err(|error| format!("decode image failed for {}: {error}", path.display()))?;
328
+ let rgba = image.to_rgba8();
329
+ let (width, height) = image.dimensions();
330
+ Ok(egui::ColorImage::from_rgba_unmultiplied(
331
+ [width as usize, height as usize],
332
+ &rgba.into_raw(),
333
+ ))
334
+ }
335
+
336
+ pub(crate) fn build_event_payload(
337
+ node: &NativeElementNode,
338
+ event_name: &str,
339
+ input_type: Option<String>,
340
+ event_data: &DesktopControlEventData,
341
+ ) -> Value {
342
+ let mut payload = Map::new();
343
+ payload.insert(String::from("event"), Value::String(String::from(event_name)));
344
+ payload.insert(String::from("sourceTag"), Value::String(node.source_tag.clone()));
345
+
346
+ if let Some(input_type) = input_type {
347
+ payload.insert(String::from("inputType"), Value::String(input_type));
348
+ }
349
+ if let Some(value) = &event_data.value {
350
+ payload.insert(String::from("value"), Value::String(value.clone()));
351
+ }
352
+ if let Some(values) = &event_data.values {
353
+ payload.insert(
354
+ String::from("values"),
355
+ Value::Array(values.iter().cloned().map(Value::String).collect()),
356
+ );
357
+ }
358
+ if let Some(checked) = event_data.checked {
359
+ payload.insert(String::from("checked"), Value::Bool(checked));
360
+ }
361
+ if let Some(detail) = node.props.get("nativePayload") {
362
+ payload.insert(String::from("detail"), normalize_jsonish_value(detail.clone()));
363
+ }
364
+
365
+ Value::Object(payload)
366
+ }
367
+
368
+ pub(crate) fn resolve_native_route(node: &NativeElementNode) -> Option<String> {
369
+ if let Some(route) = node
370
+ .props
371
+ .get("nativeRoute")
372
+ .and_then(Value::as_str)
373
+ .map(str::trim)
374
+ .filter(|value| !value.is_empty())
375
+ {
376
+ return Some(route.to_string());
377
+ }
378
+
379
+ let destination = node
380
+ .props
381
+ .get("destination")
382
+ .and_then(Value::as_str)
383
+ .map(str::trim)
384
+ .filter(|value| !value.is_empty());
385
+
386
+ destination
387
+ .filter(|destination| !is_external_destination(destination))
388
+ .map(str::to_string)
389
+ }
390
+
391
+ pub(crate) fn resolve_interaction(
392
+ node: &NativeElementNode,
393
+ default_action: Option<String>,
394
+ payload_override: Option<Value>,
395
+ ) -> Option<DesktopInteraction> {
396
+ let action = node
397
+ .props
398
+ .get("nativeAction")
399
+ .and_then(Value::as_str)
400
+ .map(str::trim)
401
+ .filter(|value| !value.is_empty())
402
+ .map(str::to_string)
403
+ .or(default_action);
404
+ let route = resolve_native_route(node);
405
+ let payload = payload_override
406
+ .or_else(|| node.props.get("nativePayload").cloned())
407
+ .map(normalize_jsonish_value);
408
+
409
+ let interaction = DesktopInteraction { action, route, payload };
410
+ (!interaction.is_empty()).then_some(interaction)
411
+ }
412
+
413
+ pub(crate) fn resolve_control_event_input_type(node: &NativeElementNode) -> Option<String> {
414
+ match node.component.as_str() {
415
+ "Picker" => Some(if parse_native_bool(node.props.get("multiple")) {
416
+ String::from("select-multiple")
417
+ } else {
418
+ String::from("select-one")
419
+ }),
420
+ "Toggle" => node
421
+ .props
422
+ .get("type")
423
+ .and_then(Value::as_str)
424
+ .map(str::trim)
425
+ .filter(|value| !value.is_empty())
426
+ .map(|value| value.to_ascii_lowercase())
427
+ .or_else(|| Some(String::from("checkbox"))),
428
+ "Slider" => Some(String::from("range")),
429
+ _ => {
430
+ if node.source_tag == "textarea" {
431
+ Some(String::from("text"))
432
+ } else {
433
+ node.props
434
+ .get("type")
435
+ .and_then(Value::as_str)
436
+ .map(str::trim)
437
+ .filter(|value| !value.is_empty())
438
+ .map(|value| value.to_ascii_lowercase())
439
+ }
440
+ }
441
+ }
442
+ }
443
+
444
+ pub(crate) fn should_dispatch_control_event(node: &NativeElementNode, event_name: &str) -> bool {
445
+ if !node.events.iter().any(|candidate| candidate == event_name) {
446
+ return false;
447
+ }
448
+
449
+ !(event_name == "input"
450
+ && node.props.get("nativeBinding").is_some()
451
+ && node.events.iter().all(|candidate| candidate == "input"))
452
+ }
453
+
454
+ pub(crate) fn control_event_action(node: &NativeElementNode, event_name: &str) -> String {
455
+ node.props
456
+ .get("nativeAction")
457
+ .and_then(Value::as_str)
458
+ .map(str::trim)
459
+ .filter(|value| !value.is_empty())
460
+ .map(str::to_string)
461
+ .unwrap_or_else(|| format!("elit.event.{event_name}"))
462
+ }
463
+
464
+ pub(crate) fn parse_points_value(value: &Value) -> Vec<Pos2> {
465
+ match value {
466
+ Value::Array(points) => points
467
+ .iter()
468
+ .filter_map(|point| {
469
+ let object = point.as_object()?;
470
+ let x = object.get("x").and_then(Value::as_f64)? as f32;
471
+ let y = object.get("y").and_then(Value::as_f64)? as f32;
472
+ Some(Pos2::new(x, y))
473
+ })
474
+ .collect(),
475
+ Value::String(text) => text
476
+ .split_whitespace()
477
+ .filter_map(|entry| {
478
+ let mut parts = entry.split(',');
479
+ let x = parts.next()?.parse::<f32>().ok()?;
480
+ let y = parts.next()?.parse::<f32>().ok()?;
481
+ Some(Pos2::new(x, y))
482
+ })
483
+ .collect(),
484
+ _ => Vec::new(),
485
+ }
486
+ }
487
+
488
+ pub(crate) fn parse_view_box(value: Option<&Value>) -> Option<(f32, f32, f32, f32)> {
489
+ let view_box = value?.as_str()?;
490
+ let parts = view_box
491
+ .split_whitespace()
492
+ .filter_map(|part| part.parse::<f32>().ok())
493
+ .collect::<Vec<_>>();
494
+ (parts.len() == 4).then_some((parts[0], parts[1], parts[2], parts[3]))
495
+ }