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.
@@ -0,0 +1,414 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use eframe::egui::{self, Align2, Color32, CornerRadius, Rect, StrokeKind, Vec2};
5
+ #[cfg(target_os = "windows")]
6
+ use wry::{
7
+ Rect as WryRect,
8
+ WebView,
9
+ WebViewBuilder,
10
+ dpi::{LogicalPosition as WryLogicalPosition, LogicalSize as WryLogicalSize},
11
+ };
12
+
13
+ use super::app_models::{
14
+ DesktopEmbeddedSurfaceContent, DesktopEmbeddedSurfaceRequest,
15
+ DesktopEmbeddedSurfaceVisibility, DesktopSurfaceWindowKind,
16
+ };
17
+ #[cfg(target_os = "windows")]
18
+ use super::app_models::DesktopEmbeddedSurface;
19
+ use super::utilities::{is_external_destination, is_probably_inline_html, spawn_native_surface_window};
20
+ use super::{DesktopNativeApp, NativeElementNode};
21
+
22
+ impl DesktopNativeApp {
23
+ fn escape_html_text(text: &str) -> String {
24
+ text
25
+ .replace('&', "&")
26
+ .replace('<', "&lt;")
27
+ .replace('>', "&gt;")
28
+ }
29
+
30
+ fn escape_html_attribute(text: &str) -> String {
31
+ Self::escape_html_text(text).replace('"', "&quot;")
32
+ }
33
+
34
+ fn file_url_from_path(path: &Path) -> String {
35
+ let normalized = path.to_string_lossy().replace('\\', "/");
36
+ let encoded = normalized
37
+ .replace('%', "%25")
38
+ .replace(' ', "%20")
39
+ .replace('#', "%23")
40
+ .replace('?', "%3F");
41
+ if encoded.starts_with('/') {
42
+ format!("file://{encoded}")
43
+ } else {
44
+ format!("file:///{encoded}")
45
+ }
46
+ }
47
+
48
+ fn resolve_surface_window_url(&self, source: &str) -> Option<String> {
49
+ let trimmed = source.trim();
50
+ if trimmed.is_empty() {
51
+ return None;
52
+ }
53
+
54
+ if is_external_destination(trimmed) || trimmed.starts_with("data:") {
55
+ return Some(trimmed.to_string());
56
+ }
57
+
58
+ self.resolve_resource_path(trimmed)
59
+ .map(|path| Self::file_url_from_path(&path))
60
+ }
61
+
62
+ pub(super) fn build_media_surface_html(
63
+ kind: DesktopSurfaceWindowKind,
64
+ title: &str,
65
+ source_url: &str,
66
+ poster_url: Option<&str>,
67
+ autoplay: bool,
68
+ looping: bool,
69
+ muted: bool,
70
+ controls: bool,
71
+ plays_inline: bool,
72
+ ) -> String {
73
+ let title = Self::escape_html_text(title);
74
+ let source_url = Self::escape_html_attribute(source_url);
75
+ let poster_attr = poster_url
76
+ .map(Self::escape_html_attribute)
77
+ .map(|value| format!(" poster=\"{value}\""))
78
+ .unwrap_or_default();
79
+ let autoplay_attr = autoplay.then_some(" autoplay").unwrap_or_default();
80
+ let loop_attr = looping.then_some(" loop").unwrap_or_default();
81
+ let muted_attr = muted.then_some(" muted").unwrap_or_default();
82
+ let controls_attr = controls.then_some(" controls").unwrap_or_default();
83
+ let plays_inline_attr = plays_inline.then_some(" playsinline").unwrap_or_default();
84
+ let body = match kind {
85
+ DesktopSurfaceWindowKind::Audio => format!(
86
+ "<audio src=\"{source_url}\" preload=\"metadata\"{autoplay_attr}{loop_attr}{muted_attr}{controls_attr}></audio>"
87
+ ),
88
+ DesktopSurfaceWindowKind::Video => format!(
89
+ "<video src=\"{source_url}\" preload=\"metadata\"{poster_attr}{autoplay_attr}{loop_attr}{muted_attr}{controls_attr}{plays_inline_attr}></video>"
90
+ ),
91
+ DesktopSurfaceWindowKind::WebView => String::new(),
92
+ };
93
+
94
+ format!(
95
+ "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title><style>html,body{{margin:0;height:100%;background:#111;color:#f6f3ee;font-family:Segoe UI,Arial,sans-serif;}}body{{display:flex;align-items:center;justify-content:center;}}video,audio{{width:min(100%, 1200px);max-height:100%;background:#000;outline:none;}}audio{{padding:24px;box-sizing:border-box;background:#181818;border-radius:16px;}}</style></head><body>{body}</body></html>"
96
+ )
97
+ }
98
+
99
+ pub(super) fn open_native_surface_window(
100
+ &self,
101
+ source: &str,
102
+ title: &str,
103
+ kind: DesktopSurfaceWindowKind,
104
+ poster: Option<&str>,
105
+ autoplay: bool,
106
+ looping: bool,
107
+ muted: bool,
108
+ controls: bool,
109
+ plays_inline: bool,
110
+ ) -> Result<(), String> {
111
+ let trimmed_title = title.trim();
112
+ let title = if trimmed_title.is_empty() {
113
+ match kind {
114
+ DesktopSurfaceWindowKind::WebView => String::from("WebView"),
115
+ DesktopSurfaceWindowKind::Video => String::from("Video"),
116
+ DesktopSurfaceWindowKind::Audio => String::from("Audio"),
117
+ }
118
+ } else {
119
+ trimmed_title.to_string()
120
+ };
121
+
122
+ match kind {
123
+ DesktopSurfaceWindowKind::WebView => {
124
+ if is_probably_inline_html(source) {
125
+ spawn_native_surface_window(title, 1024.0, 720.0, None, Some(source.to_string()))
126
+ } else {
127
+ let url = self
128
+ .resolve_surface_window_url(source)
129
+ .ok_or_else(|| format!("unable to resolve surface source '{source}'"))?;
130
+ spawn_native_surface_window(title, 1024.0, 720.0, Some(url), None)
131
+ }
132
+ }
133
+ DesktopSurfaceWindowKind::Video | DesktopSurfaceWindowKind::Audio => {
134
+ let source_url = self
135
+ .resolve_surface_window_url(source)
136
+ .ok_or_else(|| format!("unable to resolve media source '{source}'"))?;
137
+ let poster_url = poster.and_then(|poster| self.resolve_surface_window_url(poster));
138
+ let html = Self::build_media_surface_html(
139
+ kind,
140
+ &title,
141
+ &source_url,
142
+ poster_url.as_deref(),
143
+ autoplay,
144
+ looping,
145
+ muted,
146
+ controls,
147
+ plays_inline,
148
+ );
149
+ spawn_native_surface_window(title, 1024.0, 720.0, None, Some(html))
150
+ }
151
+ }
152
+ }
153
+
154
+ pub(super) fn supports_embedded_surfaces() -> bool {
155
+ cfg!(target_os = "windows")
156
+ }
157
+
158
+ pub(super) fn resolve_embedded_surface_size(&self, node: &NativeElementNode, available_width: f32, fallback: Vec2) -> Vec2 {
159
+ let style = self.get_style_map(node);
160
+ let width = style
161
+ .and_then(|style| self.parse_css_size_against_basis_with_viewport(style.get("width"), available_width))
162
+ .or_else(|| self.resolve_prop_number(node, "width"));
163
+ let min_width = style.and_then(|style| self.parse_css_size_against_basis_with_viewport(style.get("minWidth"), available_width));
164
+ let max_width = style.and_then(|style| self.parse_css_size_against_basis_with_viewport(style.get("maxWidth"), available_width));
165
+ let height = style
166
+ .and_then(|style| self.parse_css_number_with_viewport(style.get("height")))
167
+ .or_else(|| self.resolve_prop_number(node, "height"));
168
+ let min_height = style.and_then(|style| self.parse_css_number_with_viewport(style.get("minHeight")));
169
+ let max_height = style.and_then(|style| self.parse_css_number_with_viewport(style.get("maxHeight")));
170
+
171
+ let aspect_ratio = if fallback.y.abs() > f32::EPSILON {
172
+ fallback.x / fallback.y
173
+ } else {
174
+ 16.0 / 9.0
175
+ };
176
+
177
+ let mut size = fallback;
178
+ match (width, height) {
179
+ (Some(width), Some(height)) => {
180
+ size = Vec2::new(width.max(1.0), height.max(1.0));
181
+ }
182
+ (Some(width), None) => {
183
+ size = Vec2::new(width.max(1.0), (width / aspect_ratio).max(1.0));
184
+ }
185
+ (None, Some(height)) => {
186
+ size = Vec2::new((height * aspect_ratio).max(1.0), height.max(1.0));
187
+ }
188
+ (None, None) => {
189
+ if size.x > available_width {
190
+ let scale = available_width / size.x.max(1.0);
191
+ size *= scale.max(0.0);
192
+ }
193
+ }
194
+ }
195
+
196
+ if width.is_none() && size.x > available_width {
197
+ let scale = available_width / size.x.max(1.0);
198
+ size *= scale.max(0.0);
199
+ }
200
+
201
+ if let Some(min_width) = min_width {
202
+ size.x = size.x.max(min_width);
203
+ }
204
+ if let Some(max_width) = max_width {
205
+ size.x = size.x.min(max_width);
206
+ }
207
+ if let Some(min_height) = min_height {
208
+ size.y = size.y.max(min_height);
209
+ }
210
+ if let Some(max_height) = max_height {
211
+ size.y = size.y.min(max_height);
212
+ }
213
+
214
+ Vec2::new(size.x.max(1.0), size.y.max(1.0))
215
+ }
216
+
217
+ pub(super) fn resolve_embedded_web_view_content(&self, source: &str) -> Option<DesktopEmbeddedSurfaceContent> {
218
+ if is_probably_inline_html(source) {
219
+ return Some(DesktopEmbeddedSurfaceContent::Html(source.to_string()));
220
+ }
221
+
222
+ self.resolve_surface_window_url(source)
223
+ .map(DesktopEmbeddedSurfaceContent::Url)
224
+ }
225
+
226
+ pub(super) fn resolve_embedded_media_content(
227
+ &self,
228
+ kind: DesktopSurfaceWindowKind,
229
+ title: &str,
230
+ source: &str,
231
+ poster: Option<&str>,
232
+ autoplay: bool,
233
+ looping: bool,
234
+ muted: bool,
235
+ controls: bool,
236
+ plays_inline: bool,
237
+ ) -> Option<DesktopEmbeddedSurfaceContent> {
238
+ let source_url = self.resolve_surface_window_url(source)?;
239
+ let poster_url = poster.and_then(|poster| self.resolve_surface_window_url(poster));
240
+ Some(DesktopEmbeddedSurfaceContent::Html(Self::build_media_surface_html(
241
+ kind,
242
+ title,
243
+ &source_url,
244
+ poster_url.as_deref(),
245
+ autoplay,
246
+ looping,
247
+ muted,
248
+ controls,
249
+ plays_inline,
250
+ )))
251
+ }
252
+
253
+ fn queue_embedded_surface_request(&mut self, key: String, request: DesktopEmbeddedSurfaceRequest) {
254
+ self.embedded_surface_requests.insert(key, request);
255
+ }
256
+
257
+ pub(super) fn resolve_embedded_surface_visibility(rect: Rect, clip_rect: Rect) -> DesktopEmbeddedSurfaceVisibility {
258
+ let intersects = rect.max.x > clip_rect.min.x
259
+ && rect.min.x < clip_rect.max.x
260
+ && rect.max.y > clip_rect.min.y
261
+ && rect.min.y < clip_rect.max.y;
262
+
263
+ if !intersects {
264
+ return DesktopEmbeddedSurfaceVisibility::Hidden;
265
+ }
266
+
267
+ if clip_rect.contains_rect(rect) {
268
+ DesktopEmbeddedSurfaceVisibility::Visible
269
+ } else {
270
+ DesktopEmbeddedSurfaceVisibility::Clipped
271
+ }
272
+ }
273
+
274
+ pub(super) fn render_embedded_surface_slot(
275
+ &mut self,
276
+ ui: &mut egui::Ui,
277
+ node: &NativeElementNode,
278
+ path: &str,
279
+ title: &str,
280
+ kind: DesktopSurfaceWindowKind,
281
+ content: DesktopEmbeddedSurfaceContent,
282
+ fallback_size: Vec2,
283
+ ) {
284
+ let desired_size = self.resolve_embedded_surface_size(node, ui.available_width().max(1.0), fallback_size);
285
+ let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
286
+ let visibility = Self::resolve_embedded_surface_visibility(rect, ui.clip_rect());
287
+ if visibility == DesktopEmbeddedSurfaceVisibility::Hidden {
288
+ return;
289
+ }
290
+
291
+ let style = self.get_style_map(node);
292
+ let fill = Self::resolve_effective_background_color_from_style(style).unwrap_or(Color32::from_rgb(17, 17, 17));
293
+ let stroke = Self::resolve_border_stroke_from_style(style).unwrap_or_else(|| egui::Stroke::new(1.0, Color32::from_gray(52)));
294
+ let corner_radius = Self::resolve_corner_radius_from_style(style).unwrap_or(CornerRadius::same(12));
295
+ ui.painter().rect_filled(rect, corner_radius, fill);
296
+ ui.painter().rect_stroke(rect, corner_radius, stroke, StrokeKind::Outside);
297
+
298
+ if visibility == DesktopEmbeddedSurfaceVisibility::Clipped {
299
+ ui.painter().text(
300
+ rect.center(),
301
+ Align2::CENTER_CENTER,
302
+ title,
303
+ egui::FontId::proportional(13.0),
304
+ Color32::from_gray(190),
305
+ );
306
+ return;
307
+ }
308
+
309
+ self.queue_embedded_surface_request(
310
+ path.to_string(),
311
+ DesktopEmbeddedSurfaceRequest {
312
+ kind,
313
+ title: title.to_string(),
314
+ rect,
315
+ content,
316
+ focus_requested: response.clicked(),
317
+ },
318
+ );
319
+ }
320
+
321
+ #[cfg(target_os = "windows")]
322
+ fn wry_rect_from_egui_rect(rect: Rect) -> WryRect {
323
+ WryRect {
324
+ position: WryLogicalPosition::new(rect.min.x as f64, rect.min.y as f64).into(),
325
+ size: WryLogicalSize::new(rect.width().max(1.0) as f64, rect.height().max(1.0) as f64).into(),
326
+ }
327
+ }
328
+
329
+ #[cfg(target_os = "windows")]
330
+ fn build_embedded_surface_webview(
331
+ frame: &eframe::Frame,
332
+ key: &str,
333
+ request: &DesktopEmbeddedSurfaceRequest,
334
+ ) -> Result<WebView, String> {
335
+ let mut builder = WebViewBuilder::new()
336
+ .with_id(key)
337
+ .with_bounds(Self::wry_rect_from_egui_rect(request.rect));
338
+
339
+ builder = match &request.content {
340
+ DesktopEmbeddedSurfaceContent::Url(url) => builder.with_url(url),
341
+ DesktopEmbeddedSurfaceContent::Html(html) => builder.with_html(html),
342
+ };
343
+
344
+ builder.build_as_child(frame).map_err(|error| error.to_string())
345
+ }
346
+
347
+ #[cfg(target_os = "windows")]
348
+ pub(super) fn reconcile_embedded_surfaces(&mut self, frame: &eframe::Frame) {
349
+ let requested_keys = self
350
+ .embedded_surface_requests
351
+ .keys()
352
+ .cloned()
353
+ .collect::<HashSet<_>>();
354
+
355
+ let existing_keys = self.embedded_surfaces.keys().cloned().collect::<Vec<_>>();
356
+ for key in existing_keys {
357
+ if !requested_keys.contains(&key) {
358
+ if let Some(surface) = self.embedded_surfaces.get(&key) {
359
+ let _ = surface.webview.set_visible(false);
360
+ }
361
+ }
362
+ }
363
+
364
+ let pending_requests = std::mem::take(&mut self.embedded_surface_requests);
365
+ for (key, request) in pending_requests {
366
+ let bounds = Self::wry_rect_from_egui_rect(request.rect);
367
+ if let Some(surface) = self.embedded_surfaces.get_mut(&key) {
368
+ if surface.kind != request.kind || surface.content != request.content {
369
+ match &request.content {
370
+ DesktopEmbeddedSurfaceContent::Url(url) => {
371
+ let _ = surface.webview.load_url(url);
372
+ }
373
+ DesktopEmbeddedSurfaceContent::Html(html) => {
374
+ let _ = surface.webview.load_html(html);
375
+ }
376
+ }
377
+ surface.kind = request.kind;
378
+ surface.content = request.content.clone();
379
+ }
380
+ let _ = surface.webview.set_bounds(bounds);
381
+ let _ = surface.webview.set_visible(true);
382
+ if request.focus_requested {
383
+ let _ = surface.webview.focus();
384
+ }
385
+ continue;
386
+ }
387
+
388
+ match Self::build_embedded_surface_webview(frame, &key, &request) {
389
+ Ok(webview) => {
390
+ let _ = webview.set_visible(true);
391
+ if request.focus_requested {
392
+ let _ = webview.focus();
393
+ }
394
+ self.embedded_surfaces.insert(
395
+ key,
396
+ DesktopEmbeddedSurface {
397
+ kind: request.kind,
398
+ content: request.content,
399
+ webview,
400
+ },
401
+ );
402
+ }
403
+ Err(error) => {
404
+ eprintln!("failed to create embedded desktop surface '{}': {error}", request.title);
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ #[cfg(not(target_os = "windows"))]
411
+ pub(super) fn reconcile_embedded_surfaces(&mut self, _frame: &eframe::Frame) {
412
+ self.embedded_surface_requests.clear();
413
+ }
414
+ }