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}