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,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
+ }