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