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,184 @@
1
+ use eframe::egui::{self, Vec2};
2
+ use serde_json::Value;
3
+
4
+ use super::utilities::{is_external_destination, resolve_interaction};
5
+ use super::{DesktopNativeApp, NativeElementNode, NativeNode};
6
+
7
+ impl DesktopNativeApp {
8
+ pub(super) fn render_button(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode) {
9
+ let label = self.collect_text_content(&NativeNode::Element(node.clone()));
10
+ let disabled = self.is_disabled(node);
11
+ let base_state = self.base_pseudo_state(node);
12
+ let inactive_style = self.resolve_style_map_with_state(node, base_state);
13
+
14
+ let mut hovered_state = base_state;
15
+ hovered_state.hovered = true;
16
+ let hovered_style = self.resolve_style_map_with_state(node, hovered_state);
17
+
18
+ let mut active_state = hovered_state;
19
+ active_state.active = true;
20
+ let active_style = self.resolve_style_map_with_state(node, active_state);
21
+
22
+ let mut focus_state = base_state;
23
+ focus_state.focused = true;
24
+ focus_state.focus_within = true;
25
+ let focus_style = self.resolve_style_map_with_state(node, focus_state);
26
+
27
+ let mut disabled_state = base_state;
28
+ disabled_state.enabled = false;
29
+ disabled_state.disabled = true;
30
+ let disabled_style = self.resolve_style_map_with_state(node, disabled_state);
31
+ let inactive_gradient = Self::resolve_background_gradient_from_style(inactive_style.as_ref());
32
+ let hovered_gradient = Self::resolve_background_gradient_from_style(hovered_style.as_ref());
33
+ let active_gradient = Self::resolve_background_gradient_from_style(active_style.as_ref());
34
+ let focus_gradient = Self::resolve_background_gradient_from_style(focus_style.as_ref());
35
+ let disabled_gradient = Self::resolve_background_gradient_from_style(disabled_style.as_ref());
36
+ let gradient_shape_idx = if inactive_gradient.is_some() || hovered_gradient.is_some() || active_gradient.is_some() || focus_gradient.is_some() || disabled_gradient.is_some() {
37
+ Some(ui.painter().add(egui::Shape::Noop))
38
+ } else {
39
+ None
40
+ };
41
+ let button_corner_radius = Self::resolve_corner_radius_from_style(inactive_style.as_ref()).unwrap_or(ui.style().visuals.widgets.inactive.corner_radius);
42
+
43
+ let response = ui
44
+ .scope(|ui| {
45
+ self.apply_widget_state_visuals(
46
+ ui,
47
+ inactive_style.as_ref(),
48
+ hovered_style.as_ref(),
49
+ active_style.as_ref(),
50
+ focus_style.as_ref(),
51
+ disabled_style.as_ref(),
52
+ );
53
+
54
+ if let Some(padding) = Self::resolve_box_edges(inactive_style.as_ref(), "padding") {
55
+ ui.spacing_mut().button_padding = egui::vec2(padding.average_horizontal(), padding.average_vertical());
56
+ }
57
+
58
+ let min_size = self.resolve_widget_min_size(inactive_style.as_ref());
59
+ let mut button = egui::Button::new(self.resolve_text_style_from_style(ui, inactive_style.as_ref(), label.clone(), false)).frame(true);
60
+ if min_size != Vec2::ZERO {
61
+ button = button.min_size(min_size);
62
+ }
63
+
64
+ ui.add_enabled(!disabled, button)
65
+ })
66
+ .inner;
67
+
68
+ if let Some(shape_idx) = gradient_shape_idx {
69
+ let active_gradient = if disabled {
70
+ disabled_gradient.or(inactive_gradient)
71
+ } else if response.is_pointer_button_down_on() {
72
+ active_gradient.or(hovered_gradient).or(focus_gradient).or(inactive_gradient)
73
+ } else if response.hovered() {
74
+ hovered_gradient.or(focus_gradient).or(inactive_gradient)
75
+ } else if response.has_focus() {
76
+ focus_gradient.or(inactive_gradient)
77
+ } else {
78
+ inactive_gradient
79
+ };
80
+
81
+ if let Some(gradient) = active_gradient {
82
+ ui.painter().set(
83
+ shape_idx,
84
+ Self::gradient_shape_for_rect(response.rect, button_corner_radius, &gradient),
85
+ );
86
+ }
87
+ }
88
+
89
+ if response.clicked() {
90
+ if let Some(interaction) = resolve_interaction(node, None, None) {
91
+ self.dispatch_interaction(ctx, interaction);
92
+ } else if node.events.iter().any(|event| event == "press") {
93
+ self.dispatch_press_event(ctx, node);
94
+ }
95
+ }
96
+ }
97
+
98
+ pub(super) fn render_link(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeElementNode) {
99
+ let label = self.collect_text_content(&NativeNode::Element(node.clone()));
100
+ let base_state = self.base_pseudo_state(node);
101
+ let inactive_style = self.resolve_style_map_with_state(node, base_state);
102
+
103
+ let mut hovered_state = base_state;
104
+ hovered_state.hovered = true;
105
+ let hovered_style = self.resolve_style_map_with_state(node, hovered_state);
106
+
107
+ let mut active_state = hovered_state;
108
+ active_state.active = true;
109
+ let active_style = self.resolve_style_map_with_state(node, active_state);
110
+
111
+ let mut focus_state = base_state;
112
+ focus_state.focused = true;
113
+ focus_state.focus_within = true;
114
+ let focus_style = self.resolve_style_map_with_state(node, focus_state);
115
+
116
+ let has_frame = Self::style_has_widget_frame(inactive_style.as_ref());
117
+ let inactive_gradient = Self::resolve_background_gradient_from_style(inactive_style.as_ref());
118
+ let hovered_gradient = Self::resolve_background_gradient_from_style(hovered_style.as_ref());
119
+ let active_gradient = Self::resolve_background_gradient_from_style(active_style.as_ref());
120
+ let focus_gradient = Self::resolve_background_gradient_from_style(focus_style.as_ref());
121
+ let gradient_shape_idx = if inactive_gradient.is_some() || hovered_gradient.is_some() || active_gradient.is_some() || focus_gradient.is_some() {
122
+ Some(ui.painter().add(egui::Shape::Noop))
123
+ } else {
124
+ None
125
+ };
126
+ let link_corner_radius = Self::resolve_corner_radius_from_style(inactive_style.as_ref()).unwrap_or(ui.style().visuals.widgets.inactive.corner_radius);
127
+ let destination = node.props.get("destination").and_then(Value::as_str).map(str::trim).unwrap_or("");
128
+ let response = ui
129
+ .scope(|ui| {
130
+ self.apply_widget_state_visuals(
131
+ ui,
132
+ inactive_style.as_ref(),
133
+ hovered_style.as_ref(),
134
+ active_style.as_ref(),
135
+ focus_style.as_ref(),
136
+ None,
137
+ );
138
+
139
+ if has_frame {
140
+ if let Some(padding) = Self::resolve_box_edges(inactive_style.as_ref(), "padding") {
141
+ ui.spacing_mut().button_padding = egui::vec2(padding.average_horizontal(), padding.average_vertical());
142
+ }
143
+ }
144
+
145
+ let min_size = self.resolve_widget_min_size(inactive_style.as_ref());
146
+ let mut link = egui::Button::new(self.resolve_text_style_from_style(ui, inactive_style.as_ref(), label.clone(), false)).frame(has_frame);
147
+ if min_size != Vec2::ZERO {
148
+ link = link.min_size(min_size);
149
+ }
150
+
151
+ ui.add(link)
152
+ })
153
+ .inner;
154
+
155
+ if let Some(shape_idx) = gradient_shape_idx {
156
+ let active_gradient = if response.is_pointer_button_down_on() {
157
+ active_gradient.or(hovered_gradient).or(focus_gradient).or(inactive_gradient)
158
+ } else if response.hovered() {
159
+ hovered_gradient.or(focus_gradient).or(inactive_gradient)
160
+ } else if response.has_focus() {
161
+ focus_gradient.or(inactive_gradient)
162
+ } else {
163
+ inactive_gradient
164
+ };
165
+
166
+ if let Some(gradient) = active_gradient {
167
+ ui.painter().set(
168
+ shape_idx,
169
+ Self::gradient_shape_for_rect(response.rect, link_corner_radius, &gradient),
170
+ );
171
+ }
172
+ }
173
+
174
+ if response.clicked() {
175
+ if !destination.is_empty() && is_external_destination(destination) {
176
+ let _ = webbrowser::open(destination);
177
+ } else if let Some(interaction) = resolve_interaction(node, None, None) {
178
+ self.dispatch_interaction(ctx, interaction);
179
+ } else if node.events.iter().any(|event| event == "press") {
180
+ self.dispatch_press_event(ctx, node);
181
+ }
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,171 @@
1
+ use std::path::PathBuf;
2
+
3
+ use eframe::egui::Rect;
4
+ use serde::Serialize;
5
+ use serde_json::Value;
6
+ #[cfg(target_os = "windows")]
7
+ use wry::WebView;
8
+
9
+ #[derive(Debug, Clone, PartialEq, Serialize)]
10
+ pub(crate) struct DesktopInteraction {
11
+ pub(crate) action: Option<String>,
12
+ pub(crate) route: Option<String>,
13
+ pub(crate) payload: Option<Value>,
14
+ }
15
+
16
+ impl DesktopInteraction {
17
+ pub(crate) fn is_empty(&self) -> bool {
18
+ self.action.is_none() && self.route.is_none() && self.payload.is_none()
19
+ }
20
+
21
+ pub(crate) fn summary(&self) -> String {
22
+ let mut parts = Vec::new();
23
+ if let Some(action) = &self.action {
24
+ parts.push(format!("action={action}"));
25
+ }
26
+ if let Some(route) = &self.route {
27
+ parts.push(format!("route={route}"));
28
+ }
29
+ if let Some(payload) = &self.payload {
30
+ parts.push(format!("payload={payload}"));
31
+ }
32
+
33
+ if parts.is_empty() {
34
+ String::from("idle")
35
+ } else {
36
+ parts.join(" | ")
37
+ }
38
+ }
39
+ }
40
+
41
+ #[derive(Debug, Clone, Default)]
42
+ pub(crate) struct DesktopControlEventData {
43
+ pub(crate) value: Option<String>,
44
+ pub(crate) values: Option<Vec<String>>,
45
+ pub(crate) checked: Option<bool>,
46
+ }
47
+
48
+ #[derive(Debug, Clone, PartialEq)]
49
+ pub(crate) struct PickerOptionData {
50
+ pub(crate) label: String,
51
+ pub(crate) value: String,
52
+ pub(crate) disabled: bool,
53
+ pub(crate) selected: bool,
54
+ }
55
+
56
+ #[derive(Debug, Clone, Default)]
57
+ pub(crate) struct ResolvedDesktopInteractionOutput {
58
+ pub(crate) file: Option<PathBuf>,
59
+ pub(crate) stdout: bool,
60
+ pub(crate) emit_ready: bool,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Default)]
64
+ pub(crate) struct DesktopNavigationState {
65
+ pub(crate) history: Vec<String>,
66
+ pub(crate) index: Option<usize>,
67
+ }
68
+
69
+ impl DesktopNavigationState {
70
+ pub(crate) fn current_route(&self) -> Option<&str> {
71
+ self.index.and_then(|index| self.history.get(index)).map(String::as_str)
72
+ }
73
+
74
+ pub(crate) fn can_go_back(&self) -> bool {
75
+ self.index.is_some()
76
+ }
77
+
78
+ pub(crate) fn can_go_forward(&self) -> bool {
79
+ match self.index {
80
+ Some(index) => index + 1 < self.history.len(),
81
+ None => !self.history.is_empty(),
82
+ }
83
+ }
84
+
85
+ pub(crate) fn navigate_to(&mut self, route: impl Into<String>) -> bool {
86
+ let route = route.into().trim().to_string();
87
+ if route.is_empty() {
88
+ return false;
89
+ }
90
+
91
+ if let Some(index) = self.index {
92
+ if index + 1 < self.history.len() {
93
+ self.history.truncate(index + 1);
94
+ }
95
+ } else {
96
+ self.history.clear();
97
+ }
98
+
99
+ self.history.push(route);
100
+ self.index = Some(self.history.len() - 1);
101
+ true
102
+ }
103
+
104
+ pub(crate) fn go_back(&mut self) -> bool {
105
+ match self.index {
106
+ Some(index) if index > 0 => {
107
+ self.index = Some(index - 1);
108
+ true
109
+ }
110
+ Some(_) => {
111
+ self.index = None;
112
+ true
113
+ }
114
+ None => false,
115
+ }
116
+ }
117
+
118
+ pub(crate) fn go_forward(&mut self) -> bool {
119
+ match self.index {
120
+ Some(index) if index + 1 < self.history.len() => {
121
+ self.index = Some(index + 1);
122
+ true
123
+ }
124
+ None if !self.history.is_empty() => {
125
+ self.index = Some(0);
126
+ true
127
+ }
128
+ _ => false,
129
+ }
130
+ }
131
+
132
+ pub(crate) fn clear(&mut self) {
133
+ self.index = None;
134
+ }
135
+ }
136
+
137
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
138
+ pub(crate) enum DesktopSurfaceWindowKind {
139
+ WebView,
140
+ Video,
141
+ Audio,
142
+ }
143
+
144
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
145
+ pub(crate) enum DesktopEmbeddedSurfaceVisibility {
146
+ Hidden,
147
+ Clipped,
148
+ Visible,
149
+ }
150
+
151
+ #[derive(Clone, Debug, PartialEq, Eq)]
152
+ pub(crate) enum DesktopEmbeddedSurfaceContent {
153
+ Url(String),
154
+ Html(String),
155
+ }
156
+
157
+ #[derive(Clone, Debug)]
158
+ pub(crate) struct DesktopEmbeddedSurfaceRequest {
159
+ pub(crate) kind: DesktopSurfaceWindowKind,
160
+ pub(crate) title: String,
161
+ pub(crate) rect: Rect,
162
+ pub(crate) content: DesktopEmbeddedSurfaceContent,
163
+ pub(crate) focus_requested: bool,
164
+ }
165
+
166
+ #[cfg(target_os = "windows")]
167
+ pub(crate) struct DesktopEmbeddedSurface {
168
+ pub(crate) kind: DesktopSurfaceWindowKind,
169
+ pub(crate) content: DesktopEmbeddedSurfaceContent,
170
+ pub(crate) webview: WebView,
171
+ }
@@ -0,0 +1,140 @@
1
+ use eframe::egui;
2
+ use serde_json::Value;
3
+
4
+ use super::{DesktopNativeApp, NativeElementNode, NativeNode};
5
+
6
+ impl DesktopNativeApp {
7
+ pub(super) fn resolve_prop_number(&self, node: &NativeElementNode, key: &str) -> Option<f32> {
8
+ Self::parse_css_number(node.props.get(key))
9
+ }
10
+
11
+ pub(super) fn resolve_prop_string<'a>(&self, node: &'a NativeElementNode, key: &str) -> Option<&'a str> {
12
+ node.props.get(key).and_then(Value::as_str).map(str::trim).filter(|value| !value.is_empty())
13
+ }
14
+
15
+ pub(super) fn resolve_label(&self, node: &NativeElementNode) -> Option<String> {
16
+ ["aria-label", "label", "title", "name", "alt"]
17
+ .iter()
18
+ .find_map(|key| self.resolve_prop_string(node, key).map(str::to_string))
19
+ .or_else(|| {
20
+ let text = self.collect_text_content(&NativeNode::Element(node.clone()));
21
+ let trimmed = text.trim();
22
+ (!trimmed.is_empty()).then_some(trimmed.to_string())
23
+ })
24
+ }
25
+
26
+ pub(super) fn is_disabled(&self, node: &NativeElementNode) -> bool {
27
+ super::parse_native_bool(node.props.get("disabled")) || super::parse_native_bool(node.props.get("aria-disabled"))
28
+ }
29
+
30
+ pub(super) fn is_read_only(&self, node: &NativeElementNode) -> bool {
31
+ super::parse_native_bool(node.props.get("readOnly")) || super::parse_native_bool(node.props.get("readonly"))
32
+ }
33
+
34
+ pub(super) fn has_focusable_tab_index(&self, node: &NativeElementNode) -> bool {
35
+ node.props
36
+ .get("tabIndex")
37
+ .or_else(|| node.props.get("tabindex"))
38
+ .and_then(|value| match value {
39
+ Value::Number(number) => number.as_i64(),
40
+ Value::String(text) => text.trim().parse::<i64>().ok(),
41
+ _ => None,
42
+ })
43
+ .map(|value| value >= 0)
44
+ .unwrap_or(false)
45
+ }
46
+
47
+ pub(super) fn render_node(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, node: &NativeNode, path: &str) {
48
+ match node {
49
+ NativeNode::Text(text_node) => {
50
+ ui.label(self.resolve_text_node_value(text_node));
51
+ }
52
+ NativeNode::Element(element_node) => match element_node.component.as_str() {
53
+ "Text" => self.render_text_element(ui, element_node),
54
+ "Button" => self.render_button(ui, ctx, element_node),
55
+ "Link" => self.render_link(ui, ctx, element_node),
56
+ "TextInput" => self.render_text_input(ui, ctx, element_node, path),
57
+ "Toggle" => self.render_toggle(ui, ctx, element_node, path),
58
+ "Slider" => self.render_slider(ui, ctx, element_node, path),
59
+ "Picker" => self.render_picker(ui, ctx, element_node, path),
60
+ "Option" => self.render_text_element(ui, element_node),
61
+ "Divider" => self.render_divider(ui),
62
+ "Progress" => self.render_progress(ui, element_node),
63
+ "Image" => self.render_image(ui, ctx, element_node),
64
+ "WebView" => self.render_web_view(ui, element_node, path),
65
+ "Media" => self.render_media(ui, ctx, element_node, path),
66
+ "Canvas" => self.render_canvas(ui, element_node),
67
+ "Vector" => self.render_vector(ui, element_node),
68
+ "Math" => self.render_math(ui, element_node),
69
+ "Table" => self.render_table(ui, ctx, element_node, path),
70
+ "Row" => self.render_row(ui, ctx, element_node, path),
71
+ "Cell" => self.render_cell(ui, ctx, element_node, path),
72
+ "ListItem" => self.render_list_item(ui, ctx, element_node, path),
73
+ "List" => self.render_list(ui, ctx, element_node, path),
74
+ "Screen" | "View" => self.render_container(ui, ctx, element_node, path),
75
+ _ => self.render_surface_placeholder(ui, &element_node.component, "Desktop native fallback surface"),
76
+ },
77
+ }
78
+ }
79
+
80
+ fn render_navigation_bar(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
81
+ if self.navigation.current_route().is_none() && !self.navigation.can_go_forward() {
82
+ return;
83
+ }
84
+
85
+ ui.horizontal(|ui| {
86
+ if ui.add_enabled(self.navigation.can_go_back(), egui::Button::new("Back")).clicked() {
87
+ self.navigation.go_back();
88
+ self.apply_navigation_title(ctx);
89
+ }
90
+
91
+ if ui.add_enabled(self.navigation.can_go_forward(), egui::Button::new("Forward")).clicked() {
92
+ self.navigation.go_forward();
93
+ self.apply_navigation_title(ctx);
94
+ }
95
+
96
+ if ui.add_enabled(self.navigation.current_route().is_some(), egui::Button::new("Reset")).clicked() {
97
+ self.navigation.clear();
98
+ self.apply_navigation_title(ctx);
99
+ }
100
+
101
+ ui.small(self.navigation.current_route().unwrap_or("/"));
102
+ });
103
+ ui.separator();
104
+ }
105
+
106
+ fn render_last_interaction_summary(&self, ui: &mut egui::Ui) {
107
+ if let Some(interaction) = &self.last_interaction {
108
+ ui.separator();
109
+ ui.small(format!("Last interaction: {}", interaction.summary()));
110
+ }
111
+ }
112
+ }
113
+
114
+ impl eframe::App for DesktopNativeApp {
115
+ fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
116
+ let roots = self.payload.tree.roots.clone();
117
+ self.viewport_size = ctx.screen_rect().size();
118
+ self.ensure_fonts_loaded(ctx);
119
+ self.emit_ready_interaction();
120
+
121
+ egui::CentralPanel::default().show(ctx, |ui| {
122
+ egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
123
+ self.render_navigation_bar(ui, ctx);
124
+
125
+ for (index, root) in roots.iter().enumerate() {
126
+ self.render_node(ui, ctx, root, &format!("root-{index}"));
127
+ }
128
+
129
+ self.render_last_interaction_summary(ui);
130
+ });
131
+ });
132
+
133
+ self.reconcile_embedded_surfaces(frame);
134
+
135
+ if self.pending_auto_close {
136
+ self.pending_auto_close = false;
137
+ ctx.send_viewport_cmd(egui::ViewportCommand::Close);
138
+ }
139
+ }
140
+ }