elit 3.4.8 → 3.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/src/desktop/native_renderer/action_widgets.rs +184 -0
- package/src/desktop/native_renderer/app_models.rs +171 -0
- package/src/desktop/native_renderer/app_runtime.rs +140 -0
- package/src/desktop/native_renderer/container_rendering.rs +610 -0
- package/src/desktop/native_renderer/content_widgets.rs +634 -0
- package/src/desktop/native_renderer/css_models.rs +371 -0
- package/src/desktop/native_renderer/embedded_surfaces.rs +414 -0
- package/src/desktop/native_renderer/form_controls.rs +516 -0
- package/src/desktop/native_renderer/interaction_dispatch.rs +89 -0
- package/src/desktop/native_renderer/runtime_support.rs +135 -0
- package/src/desktop/native_renderer/utilities.rs +495 -0
- package/src/desktop/native_renderer/vector_drawing.rs +491 -0
- package/src/desktop/native_renderer.rs +2449 -6530
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ Elit now includes a practical native-generation foundation that keeps the existi
|
|
|
76
76
|
|
|
77
77
|
That same foundation also feeds native desktop mode: Elit resolves one shared native tree and style/layout model, then emits IR, Compose, SwiftUI, or native desktop output from it. Public `elit/native` APIs stay the same while parity fixes and native CSS-subset improvements can land across outputs together.
|
|
78
78
|
|
|
79
|
+
On the desktop-native backend, renderer responsibilities are now split internally by concern too: widget rendering, content and media surfaces, form controls, container layout, vector drawing, interaction dispatch, runtime support, and app orchestration no longer live in one monolithic renderer file. That does not change the public API, but it makes parity fixes for buttons, inputs, media surfaces, layout, and vector output safer to land across desktop native, IR, Compose, and SwiftUI outputs without forking the shared native tree contract.
|
|
80
|
+
|
|
79
81
|
```ts
|
|
80
82
|
import { a, button, div, h1, img, input } from 'elit/el';
|
|
81
83
|
import { renderNativeTree } from 'elit/native';
|
package/dist/cli.js
CHANGED
|
@@ -59399,7 +59399,7 @@ var require_package3 = __commonJS({
|
|
|
59399
59399
|
"package.json"(exports2, module2) {
|
|
59400
59400
|
module2.exports = {
|
|
59401
59401
|
name: "elit",
|
|
59402
|
-
version: "3.4.
|
|
59402
|
+
version: "3.4.9",
|
|
59403
59403
|
description: "Optimized lightweight library for creating DOM elements with reactive state",
|
|
59404
59404
|
main: "dist/index.js",
|
|
59405
59405
|
module: "dist/index.mjs",
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|