elit 3.5.6 → 3.5.8

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 (128) 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/dist/build.d.ts +1 -1
  26. package/dist/cli.cjs +16 -2
  27. package/dist/cli.mjs +16 -2
  28. package/dist/config.d.ts +1 -1
  29. package/dist/coverage.d.ts +1 -1
  30. package/dist/desktop-auto-render.cjs +2370 -0
  31. package/dist/desktop-auto-render.d.ts +13 -0
  32. package/dist/desktop-auto-render.js +2341 -0
  33. package/dist/desktop-auto-render.mjs +2344 -0
  34. package/dist/render-context.cjs +118 -0
  35. package/dist/render-context.d.ts +39 -0
  36. package/dist/render-context.js +77 -0
  37. package/dist/render-context.mjs +87 -0
  38. package/dist/{server-CNgDUgSZ.d.ts → server-FCdUqabc.d.ts} +1 -1
  39. package/dist/server.d.ts +1 -1
  40. package/package.json +26 -3
  41. package/dist/build.d.mts +0 -20
  42. package/dist/chokidar.d.mts +0 -134
  43. package/dist/cli.d.mts +0 -81
  44. package/dist/config.d.mts +0 -254
  45. package/dist/coverage.d.mts +0 -85
  46. package/dist/database.d.mts +0 -52
  47. package/dist/desktop.d.mts +0 -68
  48. package/dist/dom.d.mts +0 -87
  49. package/dist/el.d.mts +0 -208
  50. package/dist/fs.d.mts +0 -255
  51. package/dist/hmr.d.mts +0 -38
  52. package/dist/http.d.mts +0 -169
  53. package/dist/https.d.mts +0 -108
  54. package/dist/index.d.mts +0 -13
  55. package/dist/mime-types.d.mts +0 -48
  56. package/dist/native.d.mts +0 -136
  57. package/dist/path.d.mts +0 -163
  58. package/dist/router.d.mts +0 -49
  59. package/dist/runtime.d.mts +0 -97
  60. package/dist/server-D0Dp4R5z.d.mts +0 -449
  61. package/dist/server.d.mts +0 -7
  62. package/dist/state.d.mts +0 -117
  63. package/dist/style.d.mts +0 -232
  64. package/dist/test-reporter.d.mts +0 -77
  65. package/dist/test-runtime.d.mts +0 -122
  66. package/dist/test.d.mts +0 -39
  67. package/dist/types.d.mts +0 -586
  68. package/dist/universal.d.mts +0 -21
  69. package/dist/ws.d.mts +0 -200
  70. package/dist/wss.d.mts +0 -108
  71. package/src/build.ts +0 -362
  72. package/src/chokidar.ts +0 -427
  73. package/src/cli.ts +0 -1162
  74. package/src/config.ts +0 -509
  75. package/src/coverage.ts +0 -1479
  76. package/src/database.ts +0 -1410
  77. package/src/desktop-auto-render.ts +0 -317
  78. package/src/desktop-cli.ts +0 -1533
  79. package/src/desktop.ts +0 -99
  80. package/src/dev-build.ts +0 -340
  81. package/src/dom.ts +0 -901
  82. package/src/el.ts +0 -183
  83. package/src/fs.ts +0 -609
  84. package/src/hmr.ts +0 -149
  85. package/src/http.ts +0 -856
  86. package/src/https.ts +0 -411
  87. package/src/index.ts +0 -16
  88. package/src/mime-types.ts +0 -222
  89. package/src/mobile-cli.ts +0 -2313
  90. package/src/native-background.ts +0 -444
  91. package/src/native-border.ts +0 -343
  92. package/src/native-canvas.ts +0 -260
  93. package/src/native-cli.ts +0 -414
  94. package/src/native-color.ts +0 -904
  95. package/src/native-estimation.ts +0 -194
  96. package/src/native-grid.ts +0 -590
  97. package/src/native-interaction.ts +0 -1289
  98. package/src/native-layout.ts +0 -568
  99. package/src/native-link.ts +0 -76
  100. package/src/native-render-support.ts +0 -361
  101. package/src/native-spacing.ts +0 -231
  102. package/src/native-state.ts +0 -318
  103. package/src/native-strings.ts +0 -46
  104. package/src/native-transform.ts +0 -120
  105. package/src/native-types.ts +0 -439
  106. package/src/native-typography.ts +0 -254
  107. package/src/native-units.ts +0 -441
  108. package/src/native-vector.ts +0 -910
  109. package/src/native.ts +0 -5606
  110. package/src/path.ts +0 -493
  111. package/src/pm-cli.ts +0 -2498
  112. package/src/preview-build.ts +0 -294
  113. package/src/render-context.ts +0 -138
  114. package/src/router.ts +0 -260
  115. package/src/runtime.ts +0 -97
  116. package/src/server.ts +0 -2294
  117. package/src/state.ts +0 -556
  118. package/src/style.ts +0 -1790
  119. package/src/test-globals.d.ts +0 -184
  120. package/src/test-reporter.ts +0 -609
  121. package/src/test-runtime.ts +0 -1359
  122. package/src/test.ts +0 -368
  123. package/src/types.ts +0 -381
  124. package/src/universal.ts +0 -81
  125. package/src/wapk-cli.ts +0 -3213
  126. package/src/workspace-package.ts +0 -102
  127. package/src/ws.ts +0 -648
  128. package/src/wss.ts +0 -241
@@ -0,0 +1,634 @@
1
+ use eframe::egui::{self, Align, Align2, Color32, CornerRadius, Direction, Layout, Pos2, Rect, RichText, Sense, StrokeKind, Vec2};
2
+ use serde_json::{Map, Value};
3
+
4
+ use super::app_models::DesktopSurfaceWindowKind;
5
+ use super::utilities::{is_external_destination, is_probably_inline_html, parse_native_bool, parse_view_box, resolve_surface_source};
6
+ use super::{DesktopNativeApp, NativeElementNode, NativeNode};
7
+
8
+ impl DesktopNativeApp {
9
+ fn render_local_or_placeholder_image(
10
+ &mut self,
11
+ ui: &mut egui::Ui,
12
+ ctx: &egui::Context,
13
+ node: &NativeElementNode,
14
+ source: &str,
15
+ fallback_label: Option<&str>,
16
+ ) {
17
+ if let Ok(texture) = self.ensure_texture_for_source(ctx, source) {
18
+ let natural_size = texture.size_vec2();
19
+ let desired_size = self.resolve_display_size(node, natural_size);
20
+ let style = self.get_style_map(node);
21
+ let object_fit = style.and_then(|style| style.get("objectFit")).and_then(Value::as_str).unwrap_or("fill");
22
+ let render_size = Self::resolve_object_fit_image_size(object_fit, desired_size, natural_size);
23
+ let image = egui::Image::from_texture(&texture).fit_to_exact_size(render_size);
24
+ if render_size != desired_size {
25
+ ui.allocate_ui_with_layout(
26
+ desired_size,
27
+ Layout::centered_and_justified(Direction::TopDown),
28
+ |ui| {
29
+ let clip = ui.clip_rect().intersect(ui.max_rect());
30
+ ui.set_clip_rect(clip);
31
+ ui.add(image);
32
+ },
33
+ );
34
+ } else {
35
+ ui.add(image);
36
+ }
37
+ return;
38
+ }
39
+
40
+ egui::Frame::group(ui.style()).show(ui, |ui| {
41
+ ui.label(fallback_label.unwrap_or("Image preview unavailable"));
42
+ ui.small(source);
43
+ if is_external_destination(source) && ui.button("Open source").clicked() {
44
+ let _ = webbrowser::open(source);
45
+ }
46
+ });
47
+ }
48
+
49
+ pub(super) fn render_text_element(&mut self, ui: &mut egui::Ui, node: &NativeElementNode) {
50
+ let text = self.collect_text_content(&NativeNode::Element(node.clone()));
51
+ if text.is_empty() {
52
+ return;
53
+ }
54
+
55
+ let style = self.get_style_map(node).cloned();
56
+ let margin = Self::resolve_box_edges(style.as_ref(), "margin").filter(|margin| !margin.is_zero());
57
+ let padding = Self::resolve_box_edges(style.as_ref(), "padding").filter(|padding| !padding.is_zero());
58
+ let opacity = Self::resolve_opacity_from_style(style.as_ref());
59
+ let backdrop_blur = Self::resolve_backdrop_blur_radius_from_style(style.as_ref());
60
+ let css_filter = Self::resolve_css_filter_from_style(style.as_ref());
61
+ let linear_gradient = Self::resolve_background_gradient_from_style(style.as_ref()).map(|gradient| Self::gradient_with_opacity(gradient, opacity));
62
+ let radial_gradient = if linear_gradient.is_none() {
63
+ Self::resolve_background_radial_gradient_from_style(style.as_ref()).map(|gradient| Self::radial_gradient_with_opacity(gradient, opacity))
64
+ } else {
65
+ None
66
+ };
67
+ let has_gradient = linear_gradient.is_some() || radial_gradient.is_some();
68
+ let fill = if has_gradient {
69
+ Color32::TRANSPARENT
70
+ } else {
71
+ let raw_fill = Self::resolve_effective_background_color_from_style(style.as_ref())
72
+ .map(|fill| fill.gamma_multiply(opacity))
73
+ .unwrap_or(Color32::TRANSPARENT);
74
+ if let Some(ref filter) = css_filter { filter.apply(raw_fill) } else { raw_fill }
75
+ };
76
+ let raw_stroke = Self::resolve_border_stroke_from_style(style.as_ref()).unwrap_or(egui::Stroke::NONE);
77
+ let stroke = if let Some(ref filter) = css_filter {
78
+ egui::Stroke::new(raw_stroke.width, filter.apply(raw_stroke.color))
79
+ } else {
80
+ raw_stroke
81
+ };
82
+ let corner_radius = Self::resolve_corner_radius_from_style(style.as_ref());
83
+ let shadow = Self::resolve_box_shadow_from_style(style.as_ref())
84
+ .or_else(|| backdrop_blur.filter(|_| corner_radius.is_some()).map(Self::synthesize_shadow_from_backdrop_blur));
85
+ let outline_stroke = Self::resolve_outline_from_style(style.as_ref());
86
+ let overflow_hidden = Self::resolve_overflow_hidden_from_style(style.as_ref());
87
+ let cursor_icon = Self::resolve_cursor_from_style(style.as_ref());
88
+ let text_shadow = Self::resolve_text_shadow_from_style(style.as_ref());
89
+ let do_wrap = Self::resolve_white_space_wraps(style.as_ref());
90
+ let do_truncate = Self::resolve_text_overflow_ellipsis(style.as_ref());
91
+
92
+ let resolved_font_size = style.as_ref()
93
+ .and_then(|style| self.parse_css_number_with_viewport(style.get("fontSize")))
94
+ .unwrap_or(14.0);
95
+ let measure_ctx = self.css_measure_context(Some(resolved_font_size));
96
+ let text_indent = Self::resolve_text_indent_from_style(style.as_ref(), resolved_font_size, measure_ctx);
97
+ let display_text = if let Some(indent) = text_indent {
98
+ let space_count = ((indent / (resolved_font_size * 0.55)).round() as usize).min(20);
99
+ format!("{}{}", " ".repeat(space_count), text)
100
+ } else {
101
+ text
102
+ };
103
+
104
+ let mut rich_text = self.resolve_text_style_from_style(ui, style.as_ref(), display_text.clone(), true);
105
+ if let Some(ref filter) = css_filter {
106
+ if let Some(color) = Self::resolve_text_color_from_style(style.as_ref()) {
107
+ rich_text = rich_text.color(filter.apply(color));
108
+ }
109
+ }
110
+
111
+ let has_frame = Self::style_has_widget_frame(style.as_ref())
112
+ || shadow.is_some()
113
+ || has_gradient
114
+ || opacity < 1.0
115
+ || margin.is_some()
116
+ || outline_stroke.is_some();
117
+
118
+ if has_frame {
119
+ let gradient_shape_idx = has_gradient.then(|| ui.painter().add(egui::Shape::Noop));
120
+ let mut frame = egui::Frame::new().fill(fill).stroke(stroke);
121
+ if let Some(padding) = padding {
122
+ frame = frame.inner_margin(padding.to_margin());
123
+ }
124
+ if let Some(margin) = margin {
125
+ frame = frame.outer_margin(margin.to_margin());
126
+ }
127
+ if let Some(corner_radius) = corner_radius {
128
+ frame = frame.corner_radius(corner_radius);
129
+ }
130
+ if let Some(shadow) = shadow {
131
+ frame = frame.shadow(shadow);
132
+ }
133
+ if opacity < 1.0 {
134
+ frame = frame.multiply_with_opacity(opacity);
135
+ }
136
+
137
+ let response = frame.show(ui, |ui| {
138
+ if overflow_hidden {
139
+ let clip = ui.clip_rect().intersect(ui.max_rect());
140
+ ui.set_clip_rect(clip);
141
+ }
142
+
143
+ if let Some(ref shadow) = text_shadow {
144
+ let shadow_rich = self.resolve_text_style_from_style(ui, style.as_ref(), display_text.clone(), false)
145
+ .color(shadow.color);
146
+ let shadow_response = ui.label(shadow_rich);
147
+ let text_rect = shadow_response.rect;
148
+ ui.painter().text(
149
+ Pos2::new(text_rect.min.x + shadow.offset_x, text_rect.min.y + shadow.offset_y),
150
+ Align2::LEFT_TOP,
151
+ &display_text,
152
+ egui::FontId::proportional(resolved_font_size),
153
+ shadow.color,
154
+ );
155
+ }
156
+
157
+ let mut label = egui::Label::new(rich_text);
158
+ if !do_wrap || do_truncate {
159
+ label = label.truncate();
160
+ } else {
161
+ label = label.wrap();
162
+ }
163
+ ui.add(label)
164
+ });
165
+
166
+ let widget_rect = if let Some(margin) = margin {
167
+ Rect::from_min_max(
168
+ Pos2::new(response.response.rect.left() + margin.left, response.response.rect.top() + margin.top),
169
+ Pos2::new(response.response.rect.right() - margin.right, response.response.rect.bottom() - margin.bottom),
170
+ )
171
+ } else {
172
+ response.response.rect
173
+ };
174
+
175
+ if let Some(shape_idx) = gradient_shape_idx {
176
+ let gradient_shape = if let Some(gradient) = linear_gradient.as_ref() {
177
+ Some(Self::gradient_shape_for_rect(widget_rect, corner_radius.unwrap_or(CornerRadius::ZERO), gradient))
178
+ } else if let Some(gradient) = radial_gradient.as_ref() {
179
+ Some(Self::radial_gradient_shape_for_rect(widget_rect, corner_radius.unwrap_or(CornerRadius::ZERO), gradient))
180
+ } else {
181
+ None
182
+ };
183
+ if let Some(shape) = gradient_shape {
184
+ ui.painter().set(shape_idx, shape);
185
+ }
186
+ }
187
+
188
+ if let Some(outline) = outline_stroke {
189
+ let expand = outline.width.max(1.0);
190
+ let outline_rect = widget_rect.expand(expand);
191
+ ui.painter().rect_stroke(outline_rect, corner_radius.unwrap_or(CornerRadius::ZERO), outline, StrokeKind::Outside);
192
+ }
193
+
194
+ if let Some(cursor) = cursor_icon {
195
+ let response = ui.interact(widget_rect, ui.id().with("cursor_text"), egui::Sense::hover());
196
+ if response.hovered() {
197
+ ui.ctx().set_cursor_icon(cursor);
198
+ }
199
+ }
200
+
201
+ return;
202
+ }
203
+
204
+ if let Some(ref shadow) = text_shadow {
205
+ let shadow_rich = self.resolve_text_style_from_style(ui, style.as_ref(), display_text.clone(), false)
206
+ .color(shadow.color);
207
+ let shadow_response = ui.label(shadow_rich);
208
+ let text_rect = shadow_response.rect;
209
+ ui.painter().text(
210
+ Pos2::new(text_rect.min.x + shadow.offset_x, text_rect.min.y + shadow.offset_y),
211
+ Align2::LEFT_TOP,
212
+ &display_text,
213
+ egui::FontId::proportional(resolved_font_size),
214
+ shadow.color,
215
+ );
216
+ }
217
+
218
+ let mut label = egui::Label::new(rich_text);
219
+ if !do_wrap || do_truncate {
220
+ label = label.truncate();
221
+ } else {
222
+ label = label.wrap();
223
+ }
224
+ let label_response = ui.add(label);
225
+
226
+ if let Some(cursor) = cursor_icon {
227
+ if label_response.hovered() {
228
+ ui.ctx().set_cursor_icon(cursor);
229
+ }
230
+ }
231
+ }
232
+
233
+ pub(super) fn render_divider(&mut self, ui: &mut egui::Ui) {
234
+ ui.separator();
235
+ }
236
+
237
+ pub(super) fn render_progress(&mut self, ui: &mut egui::Ui, node: &NativeElementNode) {
238
+ let value = node.props.get("value").and_then(Value::as_f64);
239
+ let max = node.props.get("max").and_then(Value::as_f64).filter(|value| *value > 0.0);
240
+
241
+ match (value, max) {
242
+ (Some(value), Some(max)) => {
243
+ let progress = (value / max).clamp(0.0, 1.0) as f32;
244
+ ui.add(egui::ProgressBar::new(progress).show_percentage());
245
+ }
246
+ _ => {
247
+ ui.add(egui::Spinner::new());
248
+ }
249
+ }
250
+ }
251
+
252
+ fn collect_table_rows(&self, node: &NativeElementNode) -> Vec<NativeElementNode> {
253
+ let mut rows = Vec::new();
254
+ for child in &node.children {
255
+ if let NativeNode::Element(element_node) = child {
256
+ if element_node.component == "Row" {
257
+ rows.push(element_node.clone());
258
+ } else if matches!(element_node.source_tag.as_str(), "tbody" | "thead" | "tfoot") {
259
+ for nested in &element_node.children {
260
+ if let NativeNode::Element(row_node) = nested {
261
+ if row_node.component == "Row" {
262
+ rows.push(row_node.clone());
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ rows
270
+ }
271
+
272
+ fn collect_row_cells(&self, row: &NativeElementNode) -> Vec<NativeElementNode> {
273
+ row.children
274
+ .iter()
275
+ .filter_map(|child| match child {
276
+ NativeNode::Element(element_node) if element_node.component == "Cell" => Some(element_node.clone()),
277
+ _ => None,
278
+ })
279
+ .collect()
280
+ }
281
+
282
+ pub(super) fn render_table(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
283
+ let rows = self.collect_table_rows(node);
284
+ if rows.is_empty() {
285
+ self.render_container(ui, ctx, node, path);
286
+ return;
287
+ }
288
+
289
+ let columns = rows.iter().map(|row| self.collect_row_cells(row).len()).max().unwrap_or(1);
290
+ let gap = self.resolve_gap(node);
291
+ egui::Grid::new(path)
292
+ .num_columns(columns)
293
+ .spacing(egui::vec2(gap, gap))
294
+ .show(ui, |ui| {
295
+ for (row_index, row) in rows.iter().enumerate() {
296
+ let cells = self.collect_row_cells(row);
297
+ for (column_index, cell) in cells.iter().enumerate() {
298
+ egui::Frame::group(ui.style()).show(ui, |ui| {
299
+ self.render_children(ui, ctx, cell, &format!("{path}-{row_index}-{column_index}"));
300
+ });
301
+ }
302
+
303
+ for _ in cells.len()..columns {
304
+ ui.label("");
305
+ }
306
+
307
+ ui.end_row();
308
+ }
309
+ });
310
+ }
311
+
312
+ pub(super) fn render_row(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
313
+ ui.horizontal_wrapped(|ui| {
314
+ for (index, child) in node.children.iter().enumerate() {
315
+ self.render_node(ui, ctx, child, &format!("{path}-{index}"));
316
+ }
317
+ });
318
+ }
319
+
320
+ pub(super) fn render_cell(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
321
+ egui::Frame::group(ui.style()).show(ui, |ui| {
322
+ self.render_children(ui, ctx, node, path);
323
+ });
324
+ }
325
+
326
+ pub(super) fn render_image(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode) {
327
+ if let Some(source) = resolve_surface_source(node) {
328
+ let fallback = self.resolve_label(node);
329
+ self.render_local_or_placeholder_image(ui, ctx, node, &source, fallback.as_deref());
330
+ return;
331
+ }
332
+
333
+ egui::Frame::group(ui.style()).show(ui, |ui| {
334
+ ui.label(self.resolve_label(node).unwrap_or_else(|| String::from("Image")));
335
+ ui.small("No image source provided");
336
+ });
337
+ }
338
+
339
+ fn open_source(&self, source: &str) {
340
+ if is_probably_inline_html(source) {
341
+ return;
342
+ }
343
+
344
+ if is_external_destination(source) {
345
+ let _ = webbrowser::open(source);
346
+ return;
347
+ }
348
+
349
+ if let Some(path) = self.resolve_resource_path(source) {
350
+ let _ = webbrowser::open(path.to_string_lossy().as_ref());
351
+ }
352
+ }
353
+
354
+ pub(super) fn render_web_view(&mut self, ui: &mut egui::Ui, node: &NativeElementNode, path: &str) {
355
+ let source = resolve_surface_source(node).unwrap_or_default();
356
+ let preview = self.read_source_preview(&source);
357
+ let title = self.resolve_label(node).unwrap_or_else(|| String::from("WebView"));
358
+
359
+ if !source.is_empty() && Self::supports_embedded_surfaces() {
360
+ if let Some(content) = self.resolve_embedded_web_view_content(&source) {
361
+ self.render_embedded_surface_slot(
362
+ ui,
363
+ node,
364
+ path,
365
+ &title,
366
+ DesktopSurfaceWindowKind::WebView,
367
+ content,
368
+ Vec2::new(720.0, 420.0),
369
+ );
370
+ return;
371
+ }
372
+ }
373
+
374
+ egui::Frame::group(ui.style()).show(ui, |ui| {
375
+ ui.label(&title);
376
+ if source.is_empty() {
377
+ ui.small("No source provided");
378
+ } else if let Some(preview) = preview {
379
+ ui.small(preview);
380
+ if ui.button("Open native surface").clicked() {
381
+ if self
382
+ .open_native_surface_window(&source, &title, DesktopSurfaceWindowKind::WebView, None, false, false, false, true, false)
383
+ .is_err()
384
+ {
385
+ self.open_source(&source);
386
+ }
387
+ }
388
+ if !is_probably_inline_html(&source) && ui.button("Open source").clicked() {
389
+ self.open_source(&source);
390
+ }
391
+ } else {
392
+ ui.small(&source);
393
+ if ui.button("Open native surface").clicked() {
394
+ if self
395
+ .open_native_surface_window(&source, &title, DesktopSurfaceWindowKind::WebView, None, false, false, false, true, false)
396
+ .is_err()
397
+ {
398
+ self.open_source(&source);
399
+ }
400
+ }
401
+ if ui.button("Open in browser").clicked() {
402
+ self.open_source(&source);
403
+ }
404
+ }
405
+ });
406
+ }
407
+
408
+ pub(super) fn render_media(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
409
+ let source = resolve_surface_source(node);
410
+ let poster = self.resolve_prop_string(node, "poster").map(str::to_string);
411
+ let media_label = if node.source_tag == "audio" { "Audio" } else { "Video" };
412
+ let title = self.resolve_label(node).unwrap_or_else(|| String::from(media_label));
413
+ let mut traits = Vec::new();
414
+
415
+ if parse_native_bool(node.props.get("controls")) {
416
+ traits.push("controls");
417
+ }
418
+ if parse_native_bool(node.props.get("autoplay")) {
419
+ traits.push("autoplay");
420
+ }
421
+ if parse_native_bool(node.props.get("loop")) {
422
+ traits.push("loop");
423
+ }
424
+ if parse_native_bool(node.props.get("muted")) {
425
+ traits.push("muted");
426
+ }
427
+ if parse_native_bool(node.props.get("playsInline")) {
428
+ traits.push("inline");
429
+ }
430
+
431
+ if let Some(source) = source.as_deref().filter(|_| Self::supports_embedded_surfaces()) {
432
+ let kind = if node.source_tag == "audio" {
433
+ DesktopSurfaceWindowKind::Audio
434
+ } else {
435
+ DesktopSurfaceWindowKind::Video
436
+ };
437
+ if let Some(content) = self.resolve_embedded_media_content(
438
+ kind,
439
+ &title,
440
+ source,
441
+ poster.as_deref(),
442
+ parse_native_bool(node.props.get("autoplay")),
443
+ parse_native_bool(node.props.get("loop")),
444
+ parse_native_bool(node.props.get("muted")),
445
+ parse_native_bool(node.props.get("controls")) || node.source_tag == "audio",
446
+ parse_native_bool(node.props.get("playsInline")),
447
+ ) {
448
+ self.render_embedded_surface_slot(
449
+ ui,
450
+ node,
451
+ path,
452
+ &title,
453
+ kind,
454
+ content,
455
+ if node.source_tag == "audio" {
456
+ Vec2::new(640.0, 108.0)
457
+ } else {
458
+ Vec2::new(720.0, 405.0)
459
+ },
460
+ );
461
+ return;
462
+ }
463
+ }
464
+
465
+ egui::Frame::group(ui.style()).show(ui, |ui| {
466
+ ui.label(&title);
467
+
468
+ if !traits.is_empty() {
469
+ ui.small(traits.join(" | "));
470
+ }
471
+
472
+ if let Some(poster) = poster.as_deref() {
473
+ self.render_local_or_placeholder_image(ui, ctx, node, poster, Some("Poster preview unavailable"));
474
+ }
475
+
476
+ if let Some(source) = source.as_deref() {
477
+ ui.small(source);
478
+ if ui.button("Open native surface").clicked() {
479
+ let kind = if node.source_tag == "audio" {
480
+ DesktopSurfaceWindowKind::Audio
481
+ } else {
482
+ DesktopSurfaceWindowKind::Video
483
+ };
484
+ if self
485
+ .open_native_surface_window(
486
+ source,
487
+ &title,
488
+ kind,
489
+ poster.as_deref(),
490
+ parse_native_bool(node.props.get("autoplay")),
491
+ parse_native_bool(node.props.get("loop")),
492
+ parse_native_bool(node.props.get("muted")),
493
+ parse_native_bool(node.props.get("controls")) || node.source_tag == "audio",
494
+ parse_native_bool(node.props.get("playsInline")),
495
+ )
496
+ .is_err()
497
+ {
498
+ self.open_source(source);
499
+ }
500
+ }
501
+ if ui.button("Open media").clicked() {
502
+ self.open_source(source);
503
+ }
504
+ } else {
505
+ ui.small("No media source provided");
506
+ }
507
+ });
508
+ }
509
+
510
+ pub(super) fn render_vector(&mut self, ui: &mut egui::Ui, node: &NativeElementNode) {
511
+ if let Some(spec) = self.get_vector_spec(node) {
512
+ self.render_vector_spec(ui, node, &spec);
513
+ return;
514
+ }
515
+
516
+ let view_box = parse_view_box(node.props.get("viewBox")).unwrap_or((0.0, 0.0, 100.0, 100.0));
517
+ let fallback = Vec2::new(view_box.2.max(24.0), view_box.3.max(24.0));
518
+ let size = self.resolve_display_size(node, fallback);
519
+ let (response, painter) = ui.allocate_painter(size, Sense::hover());
520
+ let rect = response.rect;
521
+ let mut unsupported = 0usize;
522
+
523
+ if let Some(fill) = self.resolve_background_color(node) {
524
+ painter.rect_filled(rect, 0.0, fill);
525
+ }
526
+
527
+ for child in &node.children {
528
+ if let NativeNode::Element(element_node) = child {
529
+ self.draw_vector_node(&painter, rect, element_node, view_box, &mut unsupported);
530
+ }
531
+ }
532
+
533
+ if unsupported > 0 {
534
+ ui.small(format!("{unsupported} vector node(s) still use fallback rendering"));
535
+ }
536
+ }
537
+
538
+ pub(super) fn render_canvas(&mut self, ui: &mut egui::Ui, node: &NativeElementNode) {
539
+ if let Some(spec) = self.get_canvas_spec(node) {
540
+ self.render_canvas_spec(ui, node, &spec);
541
+ return;
542
+ }
543
+
544
+ let intrinsic_width = self.resolve_prop_number(node, "width").unwrap_or(300.0);
545
+ let intrinsic_height = self.resolve_prop_number(node, "height").unwrap_or(150.0);
546
+ let size = self.resolve_display_size(node, Vec2::new(intrinsic_width, intrinsic_height));
547
+ let (response, painter) = ui.allocate_painter(size, Sense::hover());
548
+ let rect = response.rect;
549
+
550
+ painter.rect_stroke(rect, 0.0, egui::Stroke::new(1.0, Color32::from_gray(70)), StrokeKind::Inside);
551
+
552
+ if let Some(draw_ops) = node.props.get("drawOps").and_then(Value::as_array) {
553
+ for operation in draw_ops {
554
+ if let Some(operation) = operation.as_object() {
555
+ self.draw_canvas_operation(&painter, rect, Vec2::new(intrinsic_width, intrinsic_height), operation);
556
+ }
557
+ }
558
+ } else {
559
+ painter.text(
560
+ rect.center(),
561
+ Align2::CENTER_CENTER,
562
+ "Canvas",
563
+ egui::FontId::proportional(14.0),
564
+ Color32::from_gray(180),
565
+ );
566
+ }
567
+ }
568
+
569
+ pub(super) fn render_math(&mut self, ui: &mut egui::Ui, node: &NativeElementNode) {
570
+ let text = self.collect_text_content(&NativeNode::Element(node.clone()));
571
+ egui::Frame::group(ui.style()).show(ui, |ui| {
572
+ ui.label(RichText::new(if text.trim().is_empty() { String::from("Math") } else { text }).monospace().italics());
573
+ });
574
+ }
575
+
576
+ pub(super) fn render_surface_placeholder(&mut self, ui: &mut egui::Ui, title: &str, message: &str) {
577
+ egui::Frame::group(ui.style()).show(ui, |ui| {
578
+ ui.label(title);
579
+ ui.small(message);
580
+ });
581
+ }
582
+
583
+ pub(super) fn list_markers_visible_from_style(style: Option<&Map<String, Value>>) -> bool {
584
+ let Some(style) = style else {
585
+ return true;
586
+ };
587
+
588
+ let hides_markers = |value: Option<&Value>| {
589
+ value
590
+ .and_then(Value::as_str)
591
+ .map(|value| {
592
+ value
593
+ .to_ascii_lowercase()
594
+ .split_whitespace()
595
+ .any(|token| token == "none")
596
+ })
597
+ .unwrap_or(false)
598
+ };
599
+
600
+ !(hides_markers(style.get("listStyle")) || hides_markers(style.get("listStyleType")))
601
+ }
602
+
603
+ fn current_list_markers_visible(&self) -> bool {
604
+ self.list_marker_stack.last().copied().unwrap_or(true)
605
+ }
606
+
607
+ pub(super) fn render_list(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
608
+ let style = self.resolve_style_map_with_state(node, self.base_pseudo_state(node));
609
+ self.list_marker_stack.push(Self::list_markers_visible_from_style(style.as_ref()));
610
+ self.render_container(ui, ctx, node, path);
611
+ self.list_marker_stack.pop();
612
+ }
613
+
614
+ pub(super) fn render_list_item(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
615
+ if !self.current_list_markers_visible() {
616
+ self.render_container(ui, ctx, node, path);
617
+ return;
618
+ }
619
+
620
+ ui.horizontal_top(|ui| {
621
+ ui.label("•");
622
+ let available_width = ui.available_width().max(1.0);
623
+ ui.allocate_ui_with_layout(
624
+ Vec2::new(available_width, 0.0),
625
+ Layout::top_down(Align::Min).with_cross_justify(true),
626
+ |ui| {
627
+ ui.set_width(available_width);
628
+ ui.set_min_width(available_width);
629
+ self.render_container(ui, ctx, node, path);
630
+ },
631
+ );
632
+ });
633
+ }
634
+ }