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,516 @@
1
+ use eframe::egui::{self, Ui};
2
+ use serde_json::Value;
3
+
4
+ use super::app_models::{DesktopControlEventData, PickerOptionData};
5
+ use super::utilities::{
6
+ build_event_payload, control_event_action, format_number, parse_native_bool,
7
+ resolve_control_event_input_type, resolve_interaction, should_dispatch_control_event,
8
+ value_as_string,
9
+ };
10
+ use super::{DesktopNativeApp, DesktopStateValue, NativeElementNode, NativeNode};
11
+
12
+ impl DesktopNativeApp {
13
+ fn dispatch_control_events(
14
+ &mut self,
15
+ ctx: &egui::Context,
16
+ node: &NativeElementNode,
17
+ event_data: DesktopControlEventData,
18
+ ) {
19
+ let input_type = resolve_control_event_input_type(node);
20
+
21
+ for event_name in ["input", "change"] {
22
+ if !should_dispatch_control_event(node, event_name) {
23
+ continue;
24
+ }
25
+
26
+ let payload = build_event_payload(node, event_name, input_type.clone(), &event_data);
27
+ if let Some(interaction) = resolve_interaction(node, Some(control_event_action(node, event_name)), Some(payload)) {
28
+ self.dispatch_interaction(ctx, interaction);
29
+ }
30
+ }
31
+ }
32
+
33
+ pub(super) fn render_text_input(&mut self, ui: &mut Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
34
+ let binding = self.get_binding_reference(node);
35
+ let disabled = self.is_disabled(node);
36
+ let read_only = self.is_read_only(node);
37
+ let input_type = resolve_control_event_input_type(node).unwrap_or_else(|| String::from("text"));
38
+ let placeholder = self.resolve_prop_string(node, "placeholder").unwrap_or_default();
39
+ let mut value = if let Some(binding) = &binding {
40
+ self.state_values
41
+ .get(&binding.id)
42
+ .map(DesktopStateValue::as_text_input_value)
43
+ .unwrap_or_else(|| node.props.get("value").and_then(value_as_string).unwrap_or_default())
44
+ } else {
45
+ self.local_text_inputs
46
+ .get(path)
47
+ .cloned()
48
+ .or_else(|| node.props.get("value").and_then(value_as_string))
49
+ .unwrap_or_default()
50
+ };
51
+
52
+ let available_width = ui.available_width().max(1.0);
53
+ let mut base_state = self.base_pseudo_state(node);
54
+ base_state.placeholder_shown = !placeholder.is_empty() && value.trim().is_empty();
55
+ let state_styles = self.resolve_widget_state_styles(node, base_state);
56
+ let inactive_style = state_styles.inactive.as_ref();
57
+ let padding = Self::resolve_box_edges(inactive_style, "padding").filter(|padding| !padding.is_zero());
58
+ let desired_width = self.resolve_widget_width_from_style(inactive_style, available_width);
59
+ let (font_id, _, _) = self.resolve_text_font_id(ui, inactive_style);
60
+ let resolved_font_size = font_id
61
+ .as_ref()
62
+ .map(|font_id| font_id.size)
63
+ .unwrap_or_else(|| egui::TextStyle::Body.resolve(ui.style().as_ref()).size);
64
+ let desired_rows = if node.source_tag == "textarea" {
65
+ inactive_style
66
+ .and_then(|style| {
67
+ let line_height = Self::resolve_text_line_height(
68
+ Some(style),
69
+ resolved_font_size,
70
+ self.css_measure_context(Some(resolved_font_size)),
71
+ )
72
+ .unwrap_or(resolved_font_size * 1.35);
73
+ let target_height = self
74
+ .parse_css_number_with_viewport(style.get("height"))
75
+ .or_else(|| self.parse_css_number_with_viewport(style.get("minHeight")))?;
76
+ let inner_vertical = padding.map(|padding| padding.top + padding.bottom).unwrap_or(0.0);
77
+ Some(((target_height - inner_vertical).max(line_height) / line_height).round().max(1.0) as usize)
78
+ })
79
+ .unwrap_or(4)
80
+ } else {
81
+ 1
82
+ };
83
+ let text_color = Self::resolve_text_color_from_style(if disabled {
84
+ state_styles.disabled.as_ref().or(inactive_style)
85
+ } else {
86
+ inactive_style
87
+ });
88
+
89
+ let response = ui
90
+ .scope(|ui| {
91
+ self.apply_widget_state_visuals(
92
+ ui,
93
+ inactive_style,
94
+ state_styles.hovered.as_ref(),
95
+ state_styles.active.as_ref(),
96
+ state_styles.focus.as_ref(),
97
+ state_styles.disabled.as_ref(),
98
+ );
99
+
100
+ let mut text_edit = if node.source_tag == "textarea" {
101
+ egui::TextEdit::multiline(&mut value).desired_rows(desired_rows)
102
+ } else {
103
+ egui::TextEdit::singleline(&mut value)
104
+ };
105
+
106
+ if let Some(padding) = padding {
107
+ text_edit = text_edit.margin(padding.to_margin());
108
+ }
109
+ if let Some(font_id) = font_id.clone() {
110
+ text_edit = text_edit.font(font_id);
111
+ }
112
+ if let Some(text_color) = text_color {
113
+ text_edit = text_edit.text_color_opt(Some(text_color));
114
+ }
115
+ if let Some(desired_width) = desired_width {
116
+ text_edit = text_edit.desired_width(desired_width);
117
+ }
118
+ if !placeholder.is_empty() {
119
+ text_edit = text_edit.hint_text(placeholder);
120
+ }
121
+ if input_type == "password" {
122
+ text_edit = text_edit.password(true);
123
+ }
124
+ if read_only {
125
+ text_edit = text_edit.interactive(false);
126
+ }
127
+
128
+ if disabled {
129
+ ui.add_enabled(false, text_edit)
130
+ } else {
131
+ ui.add(text_edit)
132
+ }
133
+ })
134
+ .inner;
135
+
136
+ if response.changed() {
137
+ if let Some(binding) = binding {
138
+ let next_value = match binding.value_type.as_str() {
139
+ "number" => value
140
+ .parse::<f64>()
141
+ .map(DesktopStateValue::Number)
142
+ .unwrap_or_else(|_| DesktopStateValue::Number(0.0)),
143
+ "boolean" => DesktopStateValue::Boolean(parse_native_bool(Some(&Value::String(value.clone())))),
144
+ "string-array" => DesktopStateValue::StringArray(
145
+ value
146
+ .split(',')
147
+ .map(str::trim)
148
+ .filter(|entry| !entry.is_empty())
149
+ .map(str::to_string)
150
+ .collect(),
151
+ ),
152
+ _ => DesktopStateValue::String(value.clone()),
153
+ };
154
+ self.state_values.insert(binding.id, next_value);
155
+ } else {
156
+ self.local_text_inputs.insert(path.to_string(), value.clone());
157
+ }
158
+
159
+ self.dispatch_control_events(
160
+ ctx,
161
+ node,
162
+ DesktopControlEventData {
163
+ value: Some(value),
164
+ ..Default::default()
165
+ },
166
+ );
167
+ }
168
+ }
169
+
170
+ pub(super) fn render_toggle(&mut self, ui: &mut Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
171
+ let binding = self.get_binding_reference(node);
172
+ let disabled = self.is_disabled(node);
173
+ let mut checked = if let Some(binding) = &binding {
174
+ self.state_values
175
+ .get(&binding.id)
176
+ .map(DesktopStateValue::as_bool)
177
+ .unwrap_or_else(|| parse_native_bool(node.props.get("checked")))
178
+ } else {
179
+ self.local_toggles
180
+ .get(path)
181
+ .copied()
182
+ .unwrap_or_else(|| parse_native_bool(node.props.get("checked")))
183
+ };
184
+ let label = self.resolve_label(node).unwrap_or_default();
185
+ let mut base_state = self.base_pseudo_state(node);
186
+ base_state.checked = checked;
187
+ let state_styles = self.resolve_widget_state_styles(node, base_state);
188
+ let inactive_style = state_styles.inactive.as_ref();
189
+ let min_size = self.resolve_widget_min_size(inactive_style);
190
+ let response = ui
191
+ .scope(|ui| {
192
+ self.apply_widget_state_visuals(
193
+ ui,
194
+ inactive_style,
195
+ state_styles.hovered.as_ref(),
196
+ state_styles.active.as_ref(),
197
+ state_styles.focus.as_ref(),
198
+ state_styles.disabled.as_ref(),
199
+ );
200
+
201
+ if min_size.x > 0.0 {
202
+ ui.spacing_mut().icon_width = min_size.x;
203
+ ui.spacing_mut().icon_width_inner = (min_size.x * (8.0 / 14.0)).clamp(4.0, min_size.x.max(4.0));
204
+ ui.spacing_mut().interact_size.x = ui.spacing().interact_size.x.max(min_size.x);
205
+ }
206
+ if min_size.y > 0.0 {
207
+ ui.spacing_mut().interact_size.y = ui.spacing().interact_size.y.max(min_size.y);
208
+ }
209
+
210
+ ui.add_enabled(!disabled, egui::Checkbox::new(&mut checked, label.clone()))
211
+ })
212
+ .inner;
213
+
214
+ if response.changed() {
215
+ if let Some(binding) = binding {
216
+ self.state_values.insert(binding.id, DesktopStateValue::Boolean(checked));
217
+ } else {
218
+ self.local_toggles.insert(path.to_string(), checked);
219
+ }
220
+
221
+ self.dispatch_control_events(
222
+ ctx,
223
+ node,
224
+ DesktopControlEventData {
225
+ checked: Some(checked),
226
+ ..Default::default()
227
+ },
228
+ );
229
+ }
230
+ }
231
+
232
+ pub(super) fn render_slider(&mut self, ui: &mut Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
233
+ let binding = self.get_binding_reference(node);
234
+ let disabled = self.is_disabled(node);
235
+ let min = node.props.get("min").and_then(Value::as_f64).unwrap_or(0.0);
236
+ let max = node.props.get("max").and_then(Value::as_f64).unwrap_or(100.0);
237
+ let step = node.props.get("step").and_then(Value::as_f64).filter(|value| *value > 0.0);
238
+ let mut value = if let Some(binding) = &binding {
239
+ match self.state_values.get(&binding.id) {
240
+ Some(DesktopStateValue::Number(number)) => *number,
241
+ Some(DesktopStateValue::String(text)) => text.parse::<f64>().unwrap_or(min),
242
+ _ => node.props.get("value").and_then(Value::as_f64).unwrap_or(min),
243
+ }
244
+ } else {
245
+ self.local_slider_values
246
+ .get(path)
247
+ .copied()
248
+ .or_else(|| node.props.get("value").and_then(Value::as_f64))
249
+ .unwrap_or(min)
250
+ };
251
+ let state_styles = self.resolve_widget_state_styles(node, self.base_pseudo_state(node));
252
+ let inactive_style = state_styles.inactive.as_ref();
253
+ let desired_width = self.resolve_widget_width_from_style(inactive_style, ui.available_width().max(1.0));
254
+ let min_size = self.resolve_widget_min_size(inactive_style);
255
+ let response = ui
256
+ .scope(|ui| {
257
+ self.apply_widget_state_visuals(
258
+ ui,
259
+ inactive_style,
260
+ state_styles.hovered.as_ref(),
261
+ state_styles.active.as_ref(),
262
+ state_styles.focus.as_ref(),
263
+ state_styles.disabled.as_ref(),
264
+ );
265
+
266
+ if let Some(desired_width) = desired_width {
267
+ ui.spacing_mut().slider_width = desired_width;
268
+ }
269
+ if min_size.y > 0.0 {
270
+ ui.spacing_mut().interact_size.y = ui.spacing().interact_size.y.max(min_size.y);
271
+ }
272
+
273
+ let mut slider = egui::Slider::new(&mut value, min..=max);
274
+ if let Some(step) = step {
275
+ slider = slider.step_by(step);
276
+ }
277
+ if let Some(label) = self.resolve_label(node) {
278
+ slider = slider.text(label);
279
+ }
280
+
281
+ ui.add_enabled(!disabled, slider)
282
+ })
283
+ .inner;
284
+
285
+ if response.changed() {
286
+ if let Some(binding) = binding {
287
+ let next_value = match binding.value_type.as_str() {
288
+ "string" => DesktopStateValue::String(format_number(value)),
289
+ _ => DesktopStateValue::Number(value),
290
+ };
291
+ self.state_values.insert(binding.id, next_value);
292
+ } else {
293
+ self.local_slider_values.insert(path.to_string(), value);
294
+ }
295
+
296
+ self.dispatch_control_events(
297
+ ctx,
298
+ node,
299
+ DesktopControlEventData {
300
+ value: Some(format_number(value)),
301
+ ..Default::default()
302
+ },
303
+ );
304
+ }
305
+ }
306
+
307
+ pub(super) fn collect_picker_options(&self, node: &NativeElementNode) -> Vec<PickerOptionData> {
308
+ node.children
309
+ .iter()
310
+ .filter_map(|child| match child {
311
+ NativeNode::Element(element_node) if element_node.component == "Option" => Some(PickerOptionData {
312
+ label: self.collect_text_content(child).trim().to_string(),
313
+ value: element_node
314
+ .props
315
+ .get("value")
316
+ .and_then(value_as_string)
317
+ .unwrap_or_else(|| self.collect_text_content(child).trim().to_string()),
318
+ disabled: parse_native_bool(element_node.props.get("disabled")),
319
+ selected: parse_native_bool(element_node.props.get("selected")),
320
+ }),
321
+ _ => None,
322
+ })
323
+ .collect()
324
+ }
325
+
326
+ pub(super) fn render_picker(&mut self, ui: &mut Ui, ctx: &egui::Context, node: &NativeElementNode, path: &str) {
327
+ let binding = self.get_binding_reference(node);
328
+ let disabled = self.is_disabled(node);
329
+ let options = self.collect_picker_options(node);
330
+ let multiple = parse_native_bool(node.props.get("multiple"));
331
+
332
+ if multiple {
333
+ let mut values = if let Some(binding) = &binding {
334
+ match self.state_values.get(&binding.id) {
335
+ Some(DesktopStateValue::StringArray(values)) => values.clone(),
336
+ Some(DesktopStateValue::String(value)) => vec![value.clone()],
337
+ _ => Vec::new(),
338
+ }
339
+ } else {
340
+ self.local_multi_picker_values
341
+ .get(path)
342
+ .cloned()
343
+ .unwrap_or_else(|| {
344
+ options
345
+ .iter()
346
+ .filter(|option| option.selected)
347
+ .map(|option| option.value.clone())
348
+ .collect()
349
+ })
350
+ };
351
+
352
+ let mut changed = false;
353
+ let mut base_state = self.base_pseudo_state(node);
354
+ base_state.selected = !values.is_empty();
355
+ let state_styles = self.resolve_widget_state_styles(node, base_state);
356
+ let inactive_style = state_styles.inactive.as_ref();
357
+ let min_size = self.resolve_widget_min_size(inactive_style);
358
+ ui.scope(|ui| {
359
+ self.apply_widget_state_visuals(
360
+ ui,
361
+ inactive_style,
362
+ state_styles.hovered.as_ref(),
363
+ state_styles.active.as_ref(),
364
+ state_styles.focus.as_ref(),
365
+ state_styles.disabled.as_ref(),
366
+ );
367
+
368
+ if min_size.x > 0.0 {
369
+ ui.spacing_mut().icon_width = min_size.x;
370
+ ui.spacing_mut().icon_width_inner = (min_size.x * (8.0 / 14.0)).clamp(4.0, min_size.x.max(4.0));
371
+ ui.spacing_mut().interact_size.x = ui.spacing().interact_size.x.max(min_size.x);
372
+ }
373
+ if min_size.y > 0.0 {
374
+ ui.spacing_mut().interact_size.y = ui.spacing().interact_size.y.max(min_size.y);
375
+ }
376
+
377
+ ui.vertical(|ui| {
378
+ for option in &options {
379
+ let mut checked = values.iter().any(|value| value == &option.value);
380
+ let response = ui.add_enabled(!disabled && !option.disabled, egui::Checkbox::new(&mut checked, &option.label));
381
+ if response.changed() {
382
+ changed = true;
383
+ if checked {
384
+ if !values.iter().any(|value| value == &option.value) {
385
+ values.push(option.value.clone());
386
+ }
387
+ } else {
388
+ values.retain(|value| value != &option.value);
389
+ }
390
+ }
391
+ }
392
+ });
393
+ });
394
+
395
+ if changed {
396
+ values.sort();
397
+ if let Some(binding) = binding {
398
+ self.state_values.insert(binding.id, DesktopStateValue::StringArray(values.clone()));
399
+ } else {
400
+ self.local_multi_picker_values.insert(path.to_string(), values.clone());
401
+ }
402
+
403
+ self.dispatch_control_events(
404
+ ctx,
405
+ node,
406
+ DesktopControlEventData {
407
+ values: Some(values),
408
+ ..Default::default()
409
+ },
410
+ );
411
+ }
412
+
413
+ return;
414
+ }
415
+
416
+ let mut selected_value = if let Some(binding) = &binding {
417
+ match self.state_values.get(&binding.id) {
418
+ Some(DesktopStateValue::String(value)) => value.clone(),
419
+ Some(DesktopStateValue::Number(value)) => format_number(*value),
420
+ _ => String::new(),
421
+ }
422
+ } else {
423
+ self.local_picker_values
424
+ .get(path)
425
+ .cloned()
426
+ .or_else(|| node.props.get("value").and_then(value_as_string))
427
+ .or_else(|| options.iter().find(|option| option.selected).map(|option| option.value.clone()))
428
+ .unwrap_or_default()
429
+ };
430
+
431
+ let selected_label = options
432
+ .iter()
433
+ .find(|option| option.value == selected_value)
434
+ .map(|option| option.label.clone())
435
+ .unwrap_or_else(|| {
436
+ if selected_value.is_empty() {
437
+ String::from("Select")
438
+ } else {
439
+ selected_value.clone()
440
+ }
441
+ });
442
+ let previous_value = selected_value.clone();
443
+ let mut base_state = self.base_pseudo_state(node);
444
+ base_state.selected = !selected_value.is_empty();
445
+ let state_styles = self.resolve_widget_state_styles(node, base_state);
446
+ let inactive_style = state_styles.inactive.as_ref();
447
+ let desired_width = self.resolve_widget_width_from_style(inactive_style, ui.available_width().max(1.0));
448
+ let min_size = self.resolve_widget_min_size(inactive_style);
449
+
450
+ ui.scope(|ui| {
451
+ self.apply_widget_state_visuals(
452
+ ui,
453
+ inactive_style,
454
+ state_styles.hovered.as_ref(),
455
+ state_styles.active.as_ref(),
456
+ state_styles.focus.as_ref(),
457
+ state_styles.disabled.as_ref(),
458
+ );
459
+
460
+ if let Some(desired_width) = desired_width {
461
+ ui.spacing_mut().interact_size.x = ui.spacing().interact_size.x.max(desired_width);
462
+ }
463
+ if min_size.y > 0.0 {
464
+ ui.spacing_mut().interact_size.y = ui.spacing().interact_size.y.max(min_size.y);
465
+ }
466
+
467
+ ui.add_enabled_ui(!disabled, |ui| {
468
+ let mut combo_box = egui::ComboBox::from_id_salt(path).selected_text(
469
+ self.resolve_text_style_from_style(
470
+ ui,
471
+ if disabled {
472
+ state_styles.disabled.as_ref().or(inactive_style)
473
+ } else {
474
+ inactive_style
475
+ },
476
+ selected_label.clone(),
477
+ false,
478
+ ),
479
+ );
480
+
481
+ if let Some(desired_width) = desired_width {
482
+ combo_box = combo_box.width(desired_width);
483
+ }
484
+
485
+ combo_box.show_ui(ui, |ui| {
486
+ for option in &options {
487
+ ui.add_enabled_ui(!option.disabled, |ui| {
488
+ ui.selectable_value(&mut selected_value, option.value.clone(), &option.label);
489
+ });
490
+ }
491
+ });
492
+ });
493
+ });
494
+
495
+ if selected_value != previous_value {
496
+ if let Some(binding) = binding {
497
+ let next_value = match binding.value_type.as_str() {
498
+ "number" => DesktopStateValue::Number(selected_value.parse::<f64>().unwrap_or_default()),
499
+ _ => DesktopStateValue::String(selected_value.clone()),
500
+ };
501
+ self.state_values.insert(binding.id, next_value);
502
+ } else {
503
+ self.local_picker_values.insert(path.to_string(), selected_value.clone());
504
+ }
505
+
506
+ self.dispatch_control_events(
507
+ ctx,
508
+ node,
509
+ DesktopControlEventData {
510
+ value: Some(selected_value),
511
+ ..Default::default()
512
+ },
513
+ );
514
+ }
515
+ }
516
+ }
@@ -0,0 +1,89 @@
1
+ use eframe::egui;
2
+ use serde_json::{Map, Value};
3
+
4
+ use super::utilities::{is_external_destination, normalize_jsonish_value, resolve_interaction, resolve_route_from_payload, write_interaction_output};
5
+ use super::{DesktopInteraction, DesktopNativeApp, NativeElementNode};
6
+
7
+ impl DesktopNativeApp {
8
+ pub(super) fn record_interaction(&mut self, interaction: DesktopInteraction) {
9
+ let interaction_json = serde_json::to_string(&interaction).unwrap_or_else(|_| String::from("{}"));
10
+
11
+ if self.interaction_output.stdout {
12
+ println!("ELIT_NATIVE_INTERACTION {interaction_json}");
13
+ }
14
+
15
+ if let Err(error) = write_interaction_output(self.interaction_output.file.as_deref(), &interaction_json) {
16
+ eprintln!("failed to write desktop native interaction output: {error}");
17
+ }
18
+
19
+ self.last_interaction = Some(interaction);
20
+ }
21
+
22
+ pub(super) fn dispatch_interaction(&mut self, ctx: &egui::Context, interaction: DesktopInteraction) {
23
+ self.record_interaction(interaction.clone());
24
+
25
+ match interaction.action.as_deref() {
26
+ Some("desktop:quit") => {
27
+ ctx.send_viewport_cmd(egui::ViewportCommand::Close);
28
+ return;
29
+ }
30
+ Some("desktop:ping") => {
31
+ ctx.send_viewport_cmd(egui::ViewportCommand::Title(format!("{} - IPC OK", self.base_title)));
32
+ return;
33
+ }
34
+ Some("desktop:back") => {
35
+ self.navigation.go_back();
36
+ self.apply_navigation_title(ctx);
37
+ return;
38
+ }
39
+ Some("desktop:forward") => {
40
+ self.navigation.go_forward();
41
+ self.apply_navigation_title(ctx);
42
+ return;
43
+ }
44
+ Some("desktop:clear-route") => {
45
+ self.navigation.clear();
46
+ self.apply_navigation_title(ctx);
47
+ return;
48
+ }
49
+ _ => {}
50
+ }
51
+
52
+ let internal_route = interaction
53
+ .route
54
+ .as_deref()
55
+ .filter(|route| !is_external_destination(route))
56
+ .map(str::to_string)
57
+ .or_else(|| {
58
+ (interaction.action.as_deref() == Some("desktop:navigate"))
59
+ .then(|| resolve_route_from_payload(interaction.payload.as_ref()))
60
+ .flatten()
61
+ });
62
+
63
+ if let Some(route) = internal_route {
64
+ if self.navigation.navigate_to(route) {
65
+ self.apply_navigation_title(ctx);
66
+ }
67
+ return;
68
+ }
69
+
70
+ if let Some(route) = interaction.route.as_deref() {
71
+ if is_external_destination(route) {
72
+ let _ = webbrowser::open(route);
73
+ }
74
+ }
75
+ }
76
+
77
+ pub(super) fn dispatch_press_event(&mut self, ctx: &egui::Context, node: &NativeElementNode) {
78
+ let mut payload = Map::new();
79
+ payload.insert(String::from("event"), Value::String(String::from("press")));
80
+ payload.insert(String::from("sourceTag"), Value::String(node.source_tag.clone()));
81
+ if let Some(detail) = node.props.get("nativePayload") {
82
+ payload.insert(String::from("detail"), normalize_jsonish_value(detail.clone()));
83
+ }
84
+
85
+ if let Some(interaction) = resolve_interaction(node, Some(String::from("elit.event.press")), Some(Value::Object(payload))) {
86
+ self.dispatch_interaction(ctx, interaction);
87
+ }
88
+ }
89
+ }