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 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.8",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elit",
3
- "version": "3.4.8",
3
+ "version": "3.4.9",
4
4
  "description": "Optimized lightweight library for creating DOM elements with reactive state",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -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
+ }