1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
use crate::config::{RendererConfig, RendererKind as ConfigRendererKind, WindowConfig};
use crate::ipc::IpcHandler;
use crate::window::WindowHandle;
/// The kind of renderer being used.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RendererKind {
/// System WebView (WebView2/WebKit)
WebView,
/// Chrome Embedded (CDP)
Chrome,
}
impl From<&ConfigRendererKind> for RendererKind {
fn from(kind: &ConfigRendererKind) -> Self {
match kind {
ConfigRendererKind::WebView => Self::WebView,
ConfigRendererKind::Chrome => Self::Chrome,
}
}
}
impl From<&RendererConfig> for RendererKind {
fn from(config: &RendererConfig) -> Self {
Self::from(&config.kind)
}
}
/// Core trait that both WebView and Chrome backends must implement.
///
/// This provides a unified API for creating windows, loading content,
/// executing JavaScript, and handling IPC regardless of the underlying
/// rendering engine.
pub trait Renderer {
/// Initialize the renderer.
fn init(&mut self) -> crate::Result<()>;
/// Create a new window with the given configuration.
fn create_window(&mut self, config: &WindowConfig) -> crate::Result<WindowHandle>;
/// Load a URL in the specified window.
fn load_url(&self, window: WindowHandle, url: &str) -> crate::Result<()>;
/// Load HTML content directly.
fn load_html(&self, window: WindowHandle, html: &str) -> crate::Result<()>;
/// Load HTML content with a document base URL for relative assets.
///
/// WebView backends commonly implement `load_html` with an in-memory
/// document (`NavigateToString` on WebView2). Such documents do not have
/// a useful filesystem base, so relative Vite assets like
/// `./assets/index.js` fail to load. This helper injects a `<base>` tag
/// before delegating to the backend and keeps local-first renderers
/// portable across WebView2, WKWebView, and WebKitGTK.
fn load_html_with_base_url(
&self,
window: WindowHandle,
html: &str,
base_url: &str,
) -> crate::Result<()> {
let html_with_base = html_with_base_url(html, base_url);
self.load_html(window, &html_with_base)
}
/// Execute JavaScript in the specified window.
fn eval_script(&self, window: WindowHandle, script: &str) -> crate::Result<()>;
/// Set the IPC handler for messages from the frontend.
fn set_ipc_handler(&mut self, handler: Box<dyn IpcHandler>);
/// Send a message to the frontend JavaScript.
fn send_to_frontend(&self, window: WindowHandle, message: &str) -> crate::Result<()>;
/// Set the window title.
fn set_title(&self, window: WindowHandle, title: &str) -> crate::Result<()>;
/// Set the window size.
fn set_size(&self, window: WindowHandle, width: u32, height: u32) -> crate::Result<()>;
/// Set whether the window is resizable.
fn set_resizable(&self, window: WindowHandle, resizable: bool) -> crate::Result<()>;
/// Show or hide the window.
fn set_visible(&self, window: WindowHandle, visible: bool) -> crate::Result<()>;
/// Close a window.
fn close_window(&mut self, window: WindowHandle) -> crate::Result<()>;
// ── Frameless / Custom Title Bar ────────────────────────────────
/// Minimize the window.
fn minimize_window(&self, window: WindowHandle) -> crate::Result<()>;
/// Toggle maximize/restore.
fn maximize_window(&self, window: WindowHandle) -> crate::Result<()>;
/// Check whether the window is currently maximized.
fn is_maximized(&self, window: WindowHandle) -> crate::Result<bool>;
/// Toggle fullscreen mode.
fn set_fullscreen(&self, window: WindowHandle, fullscreen: bool) -> crate::Result<()>;
/// Check whether the window is currently fullscreen.
fn is_fullscreen(&self, window: WindowHandle) -> crate::Result<bool>;
/// Begin an interactive window drag.
///
/// Call this from a `mousedown` handler on a custom title bar element
/// to allow the user to drag the window from any region.
fn start_drag(&self, window: WindowHandle) -> crate::Result<()>;
/// Begin an interactive window resize.
///
/// `edge` specifies which edge/corner to resize from.
fn start_resize(&self, window: WindowHandle, edge: ResizeEdge) -> crate::Result<()>;
/// Set whether the window has OS decorations (title bar + borders).
fn set_decorations(&self, window: WindowHandle, decorations: bool) -> crate::Result<()>;
/// Set the window's always-on-top state.
fn set_always_on_top(&self, window: WindowHandle, always: bool) -> crate::Result<()>;
/// Enable or disable click-through: when enabled, pointer events fall
/// through the window to whatever is behind it (used by wallpaper and
/// click-through overlays). Applied at window creation by default; this
/// method allows toggling it at runtime where the platform supports it.
///
/// Default implementation is a no-op; backends override it to call the
/// platform-specific window API.
fn set_click_through(&self, _window: WindowHandle, _enabled: bool) -> crate::Result<()> {
Ok(())
}
// ── Lifecycle ───────────────────────────────────────────────────
/// Run the main event loop. This blocks until the application exits.
fn run(self: Box<Self>) -> crate::Result<()>;
/// Get the renderer kind.
fn kind(&self) -> RendererKind;
}
/// Add a document base URL while preserving a caller-provided base tag.
fn html_with_base_url(html: &str, base_url: &str) -> String {
let lower = html.to_ascii_lowercase();
if lower.contains("<base ") || lower.contains("<base>") {
return html.to_string();
}
let escaped_base = base_url
.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">");
let normalized_base = if escaped_base.ends_with('/') {
escaped_base
} else {
format!("{escaped_base}/")
};
let base_tag = format!(r#"<base href="{normalized_base}">"#);
if let Some(head_start) = lower.find("<head") {
if let Some(tag_end) = html[head_start..].find('>') {
let insert_at = head_start + tag_end + 1;
let mut output = String::with_capacity(html.len() + base_tag.len() + 1);
output.push_str(&html[..insert_at]);
output.push('\n');
output.push_str(&base_tag);
output.push_str(&html[insert_at..]);
return output;
}
}
format!("{base_tag}\n{html}")
}
#[cfg(test)]
mod tests {
use super::html_with_base_url;
#[test]
fn injects_base_into_head() {
let html = "<!doctype html><html><head><title>Test</title></head></html>";
let result = html_with_base_url(html, "file:///C:/app/frontend");
assert!(result.contains(r#"<base href="file:///C:/app/frontend/">"#));
assert!(result.contains("<head>\n<base"));
}
#[test]
fn preserves_existing_base() {
let html = r#"<head><base href="custom://app/"></head>"#;
assert_eq!(html_with_base_url(html, "file:///ignored"), html);
}
}
/// Edge or corner for interactive resize.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
Top,
Bottom,
Left,
Right,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}