Skip to main content

rdesktop_core/
renderer.rs

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