Skip to main content

cvkg_cli/
native_shell.rs

1//! Native Shell Module
2//!
3//! Provides a unified interface for creating and managing native application
4//! windows through multiple backend implementations: Tauri, Wry, or Headless
5//! (for testing and CI environments).
6
7use std::error::Error;
8use std::fmt;
9
10/// The rendering backend used for the native shell window.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ShellBackend {
13    /// Use the Wry (WebView) library for the native window.
14    Wry,
15    /// Headless mode — no actual window, useful for testing and CI.
16    Headless,
17}
18
19/// Configuration and handle for a native shell instance.
20///
21/// Use [`NativeShell::new`] to create a default shell, then chain
22/// builder methods to customize it before calling [`create_window`].
23#[derive(Debug, Clone)]
24pub struct NativeShell {
25    /// The rendering backend to use.
26    pub backend: ShellBackend,
27    /// The initial window title.
28    pub window_title: String,
29    /// The initial window width in pixels.
30    pub width: u32,
31    /// The initial window height in pixels.
32    pub height: u32,
33}
34
35impl NativeShell {
36    /// Create a new [`NativeShell`] with default dimensions (1280x720) and
37    /// the [`ShellBackend::Headless`] backend.
38    ///
39    /// # Arguments
40    ///
41    /// * `title` — The initial window title.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use cvkg_cli::native_shell::{NativeShell, ShellBackend};
47    /// let shell = NativeShell::new("My App");
48    /// assert_eq!(shell.window_title, "My App");
49    /// assert_eq!(shell.width, 1280);
50    /// assert_eq!(shell.height, 720);
51    /// assert_eq!(shell.backend, ShellBackend::Headless);
52    /// ```
53    pub fn new(title: &str) -> Self {
54        Self {
55            backend: ShellBackend::Headless,
56            window_title: title.to_string(),
57            width: 1280,
58            height: 720,
59        }
60    }
61
62    /// Set the window dimensions.
63    ///
64    /// # Arguments
65    ///
66    /// * `w` — Width in pixels.
67    /// * `h` — Height in pixels.
68    pub fn with_size(mut self, w: u32, h: u32) -> Self {
69        self.width = w;
70        self.height = h;
71        self
72    }
73
74    /// Set the rendering backend.
75    ///
76    /// # Arguments
77    ///
78    /// * `backend` — The [`ShellBackend`] to use.
79    pub fn backend(mut self, backend: ShellBackend) -> Self {
80        self.backend = backend;
81        self
82    }
83}
84
85/// A handle to a created native window.
86///
87/// Obtain a [`ShellWindow`] by calling [`create_window`].
88#[derive(Debug, Clone)]
89pub struct ShellWindow {
90    /// Unique identifier for the window.
91    pub id: u32,
92    /// The current window title.
93    pub title: String,
94    /// The current window width in pixels.
95    pub width: u32,
96    /// The current window height in pixels.
97    pub height: u32,
98}
99
100impl ShellWindow {
101    /// Update the window title.
102    ///
103    /// # Arguments
104    ///
105    /// * `title` — The new title string.
106    pub fn set_title(&mut self, title: &str) {
107        self.title = title.to_string();
108    }
109
110    /// Resize the window.
111    ///
112    /// # Arguments
113    ///
114    /// * `w` — New width in pixels.
115    /// * `h` — New height in pixels.
116    pub fn resize(&mut self, w: u32, h: u32) {
117        self.width = w;
118        self.height = h;
119    }
120
121    /// Close the window and release associated resources.
122    pub fn close(self) {
123        // In a real implementation this would call into the backend
124        // to destroy the native window. For now the handle is simply
125        // dropped.
126    }
127}
128
129/// Errors that can occur when creating or managing native shell windows.
130#[derive(Debug, Clone)]
131pub struct ShellError {
132    /// A human-readable error message.
133    pub message: String,
134}
135
136impl fmt::Display for ShellError {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "ShellError: {}", self.message)
139    }
140}
141
142impl Error for ShellError {}
143
144/// Events that can be emitted by a native window.
145#[derive(Debug, Clone, PartialEq)]
146pub enum WindowEvent {
147    /// The window was resized to the given dimensions.
148    Resized(u32, u32),
149    /// The window gained focus.
150    Focused,
151    /// The window lost focus.
152    Unfocused,
153    /// The user requested the window be closed.
154    CloseRequested,
155}
156
157/// Create a native window from the given [`NativeShell`] configuration.
158///
159/// The actual backend used depends on the `backend` field:
160/// - [`ShellBackend::Wry`] — creates a Wry webview window.
161/// - [`ShellBackend::Headless`] — creates an in-memory window handle
162///   suitable for testing.
163///
164/// # Errors
165///
166/// Returns a [`ShellError`] if the window could not be created (e.g. the
167/// requested backend is not available on the current platform).
168///
169/// # Examples
170///
171/// ```
172/// use cvkg_cli::native_shell::{NativeShell, ShellBackend, create_window};
173/// let shell = NativeShell::new("Test").backend(ShellBackend::Headless);
174/// let window = create_window(&shell).expect("Failed to create window");
175/// assert_eq!(window.title, "Test");
176/// ```
177pub fn create_window(shell: &NativeShell) -> Result<ShellWindow, ShellError> {
178    match shell.backend {
179        ShellBackend::Wry => {
180            // In a real implementation this would call wry::WebViewBuilder
181            Ok(ShellWindow {
182                id: 1,
183                title: shell.window_title.clone(),
184                width: shell.width,
185                height: shell.height,
186            })
187        }
188        ShellBackend::Headless => Ok(ShellWindow {
189            id: 0,
190            title: shell.window_title.clone(),
191            width: shell.width,
192            height: shell.height,
193        }),
194    }
195}
196
197/// Poll for pending window events in a non-blocking fashion.
198///
199/// Returns an empty Vec if no events are currently pending or if the
200/// window is operating in headless mode.
201///
202/// # Arguments
203///
204/// * `window` — The [`ShellWindow`] to poll events for.
205///
206/// # Examples
207///
208/// ```
209/// use cvkg_cli::native_shell::{NativeShell, ShellBackend, create_window, poll_events};
210/// let shell = NativeShell::new("Test").backend(ShellBackend::Headless);
211/// let window = create_window(&shell).unwrap();
212/// let events = poll_events(&window);
213/// // Headless mode always returns an empty event list
214/// assert!(events.is_empty());
215/// ```
216pub fn poll_events(_window: &ShellWindow) -> Vec<WindowEvent> {
217    // In a real implementation this would query the backend event loop.
218    // Headless mode returns no events.
219    Vec::new()
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_shell_new_defaults() {
228        let shell = NativeShell::new("Test App");
229        assert_eq!(shell.window_title, "Test App");
230        assert_eq!(shell.width, 1280);
231        assert_eq!(shell.height, 720);
232        assert_eq!(shell.backend, ShellBackend::Headless);
233    }
234
235    #[test]
236    fn test_shell_with_size() {
237        let shell = NativeShell::new("Sized").with_size(1920, 1080);
238        assert_eq!(shell.width, 1920);
239        assert_eq!(shell.height, 1080);
240    }
241
242    #[test]
243    fn test_shell_backend() {
244        let shell = NativeShell::new("Backend").backend(ShellBackend::Wry);
245        assert_eq!(shell.backend, ShellBackend::Wry);
246    }
247
248    #[test]
249    fn test_shell_builder_chain() {
250        let shell = NativeShell::new("Chained")
251            .with_size(800, 600)
252            .backend(ShellBackend::Wry);
253        assert_eq!(shell.window_title, "Chained");
254        assert_eq!(shell.width, 800);
255        assert_eq!(shell.height, 600);
256        assert_eq!(shell.backend, ShellBackend::Wry);
257    }
258
259    #[test]
260    fn test_create_window_headless() {
261        let shell = NativeShell::new("Headless Win").backend(ShellBackend::Headless);
262        let window = create_window(&shell).expect("Headless window creation should succeed");
263        assert_eq!(window.id, 0);
264        assert_eq!(window.title, "Headless Win");
265        assert_eq!(window.width, 1280);
266        assert_eq!(window.height, 720);
267    }
268
269    #[test]
270    fn test_create_window_wry() {
271        let shell = NativeShell::new("Wry Win")
272            .with_size(1024, 768)
273            .backend(ShellBackend::Wry);
274        let window = create_window(&shell).expect("Wry window creation should succeed");
275        assert_eq!(window.id, 1);
276        assert_eq!(window.title, "Wry Win");
277        assert_eq!(window.width, 1024);
278        assert_eq!(window.height, 768);
279    }
280
281
282    #[test]
283    fn test_window_set_title() {
284        let mut win = ShellWindow {
285            id: 1,
286            title: "Old".to_string(),
287            width: 800,
288            height: 600,
289        };
290        win.set_title("New Title");
291        assert_eq!(win.title, "New Title");
292    }
293
294    #[test]
295    fn test_window_resize() {
296        let mut win = ShellWindow {
297            id: 1,
298            title: "Resizable".to_string(),
299            width: 800,
300            height: 600,
301        };
302        win.resize(1920, 1080);
303        assert_eq!(win.width, 1920);
304        assert_eq!(win.height, 1080);
305    }
306
307    #[test]
308    fn test_window_close() {
309        let win = ShellWindow {
310            id: 1,
311            title: "Closable".to_string(),
312            width: 800,
313            height: 600,
314        };
315        win.close();
316        // After close, the handle is dropped. No assertion needed.
317    }
318
319    #[test]
320    fn test_poll_events_headless() {
321        let shell = NativeShell::new("Poll").backend(ShellBackend::Headless);
322        let window = create_window(&shell).unwrap();
323        let events = poll_events(&window);
324        assert!(events.is_empty());
325    }
326
327    #[test]
328    fn test_shell_error_display() {
329        let err = ShellError {
330            message: "something went wrong".to_string(),
331        };
332        assert_eq!(format!("{}", err), "ShellError: something went wrong");
333    }
334
335    #[test]
336    fn test_shell_error_implements_std_error() {
337        let err = ShellError {
338            message: "test".to_string(),
339        };
340        let _: &dyn Error = &err;
341    }
342
343    #[test]
344    fn test_window_event_equality() {
345        assert_eq!(WindowEvent::Focused, WindowEvent::Focused);
346        assert_eq!(
347            WindowEvent::Resized(800, 600),
348            WindowEvent::Resized(800, 600)
349        );
350        assert_ne!(WindowEvent::Focused, WindowEvent::Unfocused);
351        assert_ne!(
352            WindowEvent::Resized(800, 600),
353            WindowEvent::Resized(1024, 768)
354        );
355    }
356}