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}