Skip to main content

fovea_display/
debug_window.rs

1//! Debug window system for quick image visualization.
2//!
3//! This module provides an OpenCV-like `imshow` experience for Rust:
4//! display any [`ImageView`](fovea::image::ImageView) in a window with a single
5//! function call. It is gated behind the `debug-window` feature flag and
6//! is **not intended for production use**.
7//!
8//! # Architecture
9//!
10//! The debug window system uses a two-thread model:
11//!
12//! - **Main thread**: Runs the winit event loop (`DebugDisplay::run()`).
13//!   This is required by winit (and macOS) — the event loop must live on
14//!   the main thread.
15//! - **Background thread**: Runs user code. The user receives a
16//!   [`DisplayContext`] handle to send images and wait for key presses.
17//!
18//! Communication between threads uses `std::sync::mpsc` channels and a
19//! winit [`EventLoopProxy`] for wakeup.
20//!
21//! # Examples
22//!
23//! ## One-shot display
24//!
25//! ```no_run
26//! use fovea_display::{show, Identity};
27//! use fovea::image::Image;
28//! use fovea::pixel::Srgb8;
29//!
30//! let img = Image::fill(100, 100, Srgb8::new(128, 64, 200));
31//! show("Preview", &img, Identity);
32//! ```
33//!
34//! ## Multi-window with `DebugDisplay::run()`
35//!
36//! ```no_run
37//! use fovea_display::{DebugDisplay, DisplayContext, AutoContrast, Identity};
38//! use fovea::image::Image;
39//! use fovea::pixel::{Mono16, Srgba8};
40//!
41//! DebugDisplay::run(|ctx| {
42//!     let mono = Image::<Mono16>::zero(640, 480);
43//!     let strategy = AutoContrast::scan_with(&mono, |p| p.value() as f64);
44//!     ctx.show("Mono Preview", &mono, strategy);
45//!
46//!     let color = Image::fill(320, 240, Srgba8::new(255, 0, 0, 255));
47//!     ctx.show("Color Preview", &color, Identity);
48//!
49//!     ctx.wait_key();
50//! });
51//! ```
52//!
53//! # Platform considerations
54//!
55//! - **macOS**: The event loop **must** run on the main thread. Both
56//!   [`DebugDisplay::run()`] and [`show()`] take over the main thread
57//!   automatically. Calling either from a non-main thread will panic.
58//! - **Linux (Wayland/X11)**: No special requirements. The event loop
59//!   runs on whichever thread calls `run()`.
60//! - **Windows**: No special requirements.
61//! - **Minimized windows**: When a window is minimized its inner size
62//!   becomes zero. The blit is skipped until the window is restored.
63//! - **Window resizing**: The framebuffer is scaled to the window size
64//!   using nearest-neighbor interpolation in the `u32` domain. This
65//!   avoids re-running the display strategy on every resize.
66//!
67//! # Logging
68//!
69//! This module emits log messages via the [`log`] facade:
70//!
71//! | Level   | Events                                                    |
72//! |---------|-----------------------------------------------------------|
73//! | `debug` | Window created, updated, closed; exit command received     |
74//! | `warn`  | Proxy send failed, surface/window creation errors          |
75//! | `trace` | Surface resize dimensions                                 |
76//!
77//! Attach a logger (e.g. `env_logger`) to see these messages.
78
79use std::collections::HashMap;
80use std::num::NonZeroU32;
81use std::sync::Arc;
82use std::sync::mpsc;
83use std::time::Duration;
84
85use winit::application::ApplicationHandler;
86use winit::event::{ElementState, WindowEvent};
87use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
88use winit::keyboard::{KeyCode, PhysicalKey};
89use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
90use winit::window::{Window, WindowAttributes, WindowId};
91
92use fovea::image::ImageView;
93
94use crate::strategy::{DisplayStrategy, Framebuffer};
95
96// ═══════════════════════════════════════════════════════════════════════════════
97// 3.1  Channel types and message protocol
98// ═══════════════════════════════════════════════════════════════════════════════
99
100/// Commands sent from the background thread to the event loop.
101enum WindowCommand {
102    /// Create or update a window with the given title and framebuffer.
103    Show {
104        title: String,
105        framebuffer: Framebuffer,
106    },
107    /// Close all windows and exit the event loop.
108    Exit,
109}
110
111/// Events sent from the event loop back to the background thread.
112#[derive(Debug)]
113#[allow(dead_code)]
114enum WindowEvent_ {
115    /// A key was pressed in any window.
116    KeyPressed { key: KeyCode, window_title: String },
117    /// A window was closed by the user.
118    WindowClosed { title: String },
119    /// All windows have been closed.
120    AllClosed,
121}
122
123/// Custom user event for waking the event loop when commands are available.
124#[derive(Debug)]
125enum UserEvent {
126    /// Signals that a [`WindowCommand`] is available in the channel.
127    CommandAvailable,
128}
129
130// ═══════════════════════════════════════════════════════════════════════════════
131// 3.1a  Notifier — abstraction over EventLoopProxy for testability
132// ═══════════════════════════════════════════════════════════════════════════════
133
134/// Abstraction for waking the event loop when a command is available.
135///
136/// In production, wraps an [`EventLoopProxy`]. In tests, can be replaced
137/// with a no-op or a flag-setting closure. This enables unit testing
138/// [`DisplayContext`] without a real winit event loop.
139struct Notifier(Box<dyn Fn() -> bool + Send + Sync>);
140
141impl Notifier {
142    /// Create a notifier backed by an [`EventLoopProxy`].
143    fn from_proxy(proxy: EventLoopProxy<UserEvent>) -> Self {
144        Notifier(Box::new(move || {
145            proxy.send_event(UserEvent::CommandAvailable).is_ok()
146        }))
147    }
148
149    /// Wake the event loop. Returns `true` if the notification was delivered,
150    /// `false` if the event loop has exited.
151    fn notify(&self) -> bool {
152        (self.0)()
153    }
154}
155
156// ═══════════════════════════════════════════════════════════════════════════════
157// 3.2  DisplayContext — background thread API
158// ═══════════════════════════════════════════════════════════════════════════════
159
160/// Handle for displaying images from a background thread.
161///
162/// Obtained inside the closure passed to [`DebugDisplay::run()`]. All
163/// methods are safe to call from the background thread.
164///
165/// # Examples
166///
167/// ```no_run
168/// use fovea_display::{DebugDisplay, Identity};
169/// use fovea::image::Image;
170/// use fovea::pixel::Srgba8;
171///
172/// DebugDisplay::run(|ctx| {
173///     let img = Image::fill(100, 100, Srgba8::new(255, 0, 0, 255));
174///     ctx.show("Red", &img, Identity);
175///     ctx.wait_key();
176/// });
177/// ```
178pub struct DisplayContext {
179    cmd_tx: mpsc::Sender<WindowCommand>,
180    event_rx: mpsc::Receiver<WindowEvent_>,
181    notifier: Notifier,
182}
183
184impl DisplayContext {
185    /// Display an image in a window with the given title.
186    ///
187    /// Converts the image to a framebuffer using the given strategy on
188    /// the calling thread, then sends the framebuffer to the event loop
189    /// for display. If a window with this title already exists, its
190    /// contents are updated; otherwise a new window is created.
191    ///
192    /// This method is **non-blocking** — it returns immediately after
193    /// sending the command.
194    ///
195    /// # Type parameters
196    ///
197    /// - `V`: Any [`ImageView`] — owned images, ROIs, tiled views, etc.
198    /// - `S`: A [`DisplayStrategy`] that can convert `V`'s pixel type.
199    pub fn show<V, S>(&self, title: &str, image: &V, strategy: S)
200    where
201        V: ImageView,
202        V::Pixel: Copy,
203        S: DisplayStrategy<V::Pixel>,
204    {
205        let fb = Framebuffer::from_image(image, strategy);
206        self.show_framebuffer(title, fb);
207    }
208
209    /// Send a pre-built framebuffer for display.
210    ///
211    /// This is used internally by the [`show()`] convenience function to
212    /// avoid `Send` bounds on the image and strategy types.
213    pub(crate) fn show_framebuffer(&self, title: &str, fb: Framebuffer) {
214        let w = fb.width;
215        let h = fb.height;
216        let title_owned = title.to_string();
217
218        if self
219            .cmd_tx
220            .send(WindowCommand::Show {
221                title: title_owned.clone(),
222                framebuffer: fb,
223            })
224            .is_err()
225        {
226            log::warn!("command channel closed — event loop has exited");
227            return;
228        }
229
230        log::debug!("show: sent framebuffer for \"{title_owned}\" ({w}×{h})");
231
232        if !self.notifier.notify() {
233            log::warn!("event loop proxy send failed (event loop has exited)");
234        }
235    }
236
237    /// Block until a key is pressed in any window.
238    ///
239    /// Returns the [`KeyCode`] of the pressed key, or `None` if all
240    /// windows were closed or the event loop exited.
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// use fovea_display::{DebugDisplay, Identity};
246    /// use fovea::image::Image;
247    /// use fovea::pixel::Srgba8;
248    ///
249    /// DebugDisplay::run(|ctx| {
250    ///     let img = Image::fill(100, 100, Srgba8::new(0, 255, 0, 255));
251    ///     ctx.show("Green", &img, Identity);
252    ///     match ctx.wait_key() {
253    ///         Some(key) => println!("Key pressed: {:?}", key),
254    ///         None => println!("All windows closed"),
255    ///     }
256    /// });
257    /// ```
258    #[must_use]
259    pub fn wait_key(&self) -> Option<KeyCode> {
260        loop {
261            match self.event_rx.recv() {
262                Ok(WindowEvent_::KeyPressed { key, .. }) => return Some(key),
263                Ok(WindowEvent_::AllClosed) => return None,
264                Ok(WindowEvent_::WindowClosed { .. }) => {
265                    // A single window closed — keep waiting for a key
266                    // or AllClosed.
267                    continue;
268                }
269                Err(_) => {
270                    // Channel disconnected — event loop has exited.
271                    return None;
272                }
273            }
274        }
275    }
276
277    /// Block until a key is pressed, with a timeout.
278    ///
279    /// Returns `Some(key)` if a key was pressed within the timeout,
280    /// `None` if the timeout elapsed or all windows were closed.
281    #[must_use]
282    pub fn wait_key_timeout(&self, timeout: Duration) -> Option<KeyCode> {
283        let deadline = std::time::Instant::now() + timeout;
284        loop {
285            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
286            if remaining.is_zero() {
287                return None;
288            }
289            match self.event_rx.recv_timeout(remaining) {
290                Ok(WindowEvent_::KeyPressed { key, .. }) => return Some(key),
291                Ok(WindowEvent_::AllClosed) => return None,
292                Ok(WindowEvent_::WindowClosed { .. }) => {
293                    // A single window closed — keep waiting.
294                    continue;
295                }
296                Err(mpsc::RecvTimeoutError::Timeout) => return None,
297                Err(mpsc::RecvTimeoutError::Disconnected) => return None,
298            }
299        }
300    }
301
302    /// Request the event loop to close all windows and exit.
303    ///
304    /// This is called automatically when the user closure returns, but
305    /// can be called explicitly if needed.
306    pub fn exit(&self) {
307        let _ = self.cmd_tx.send(WindowCommand::Exit);
308        if !self.notifier.notify() {
309            log::warn!("event loop proxy send failed (event loop has exited)");
310        }
311    }
312
313    /// Create a `DisplayContext` for unit tests with a no-op notifier.
314    #[cfg(test)]
315    fn new_for_test(
316        cmd_tx: mpsc::Sender<WindowCommand>,
317        event_rx: mpsc::Receiver<WindowEvent_>,
318    ) -> Self {
319        DisplayContext {
320            cmd_tx,
321            event_rx,
322            notifier: Notifier(Box::new(|| true)),
323        }
324    }
325}
326
327// ═══════════════════════════════════════════════════════════════════════════════
328// 3.3  DebugDisplay::run() — event loop setup
329// ═══════════════════════════════════════════════════════════════════════════════
330
331/// Entry point for the debug display system.
332///
333/// This struct provides the [`run()`](DebugDisplay::run) method, which takes
334/// over the main thread for the winit event loop and runs user code on a
335/// background thread.
336pub struct DebugDisplay;
337
338impl DebugDisplay {
339    /// Run the debug display system.
340    ///
341    /// Takes over the **main thread** for the winit event loop. The user's
342    /// code runs in the provided closure on a **background thread**, which
343    /// receives a [`DisplayContext`] handle for displaying images.
344    ///
345    /// The event loop exits when:
346    /// - All windows are closed by the user, OR
347    /// - The user closure returns (an `Exit` command is sent automatically).
348    ///
349    /// # Examples
350    ///
351    /// ```no_run
352    /// use fovea_display::{DebugDisplay, AutoContrast, Identity};
353    /// use fovea::image::Image;
354    /// use fovea::pixel::Srgba8;
355    ///
356    /// DebugDisplay::run(|ctx| {
357    ///     let img = Image::fill(640, 480, Srgba8::new(128, 64, 200, 255));
358    ///     ctx.show("Preview", &img, Identity);
359    ///     ctx.wait_key();
360    /// });
361    /// ```
362    ///
363    /// # Panics
364    ///
365    /// Panics if the event loop cannot be created (e.g. no display server
366    /// available). On macOS, panics if called from a non-main thread.
367    pub fn run<F>(user_fn: F)
368    where
369        F: FnOnce(&DisplayContext) + Send + 'static,
370    {
371        // Build the event loop with custom user events.
372        let event_loop = EventLoop::<UserEvent>::with_user_event()
373            .build()
374            .expect("failed to create event loop");
375
376        let proxy = event_loop.create_proxy();
377
378        // Command channel: background thread → event loop.
379        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
380
381        // Event channel: event loop → background thread.
382        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
383
384        let ctx = DisplayContext {
385            cmd_tx: cmd_tx.clone(),
386            event_rx,
387            notifier: Notifier::from_proxy(proxy.clone()),
388        };
389
390        // Spawn the user's code on a background thread.
391        let bg_cmd_tx = cmd_tx;
392        let bg_notifier = Notifier::from_proxy(proxy);
393        std::thread::spawn(move || {
394            user_fn(&ctx);
395
396            // When the user closure returns, signal the event loop to exit.
397            let _ = bg_cmd_tx.send(WindowCommand::Exit);
398            if !bg_notifier.notify() {
399                log::warn!("event loop proxy send failed (event loop has exited)");
400            }
401        });
402
403        // Create the application handler and run the event loop.
404        let mut app = App {
405            cmd_rx,
406            event_tx,
407            context: None,
408            windows: HashMap::new(),
409        };
410
411        event_loop
412            .run_app(&mut app)
413            .expect("event loop terminated with error");
414    }
415}
416
417// ═══════════════════════════════════════════════════════════════════════════════
418// 3.4  App struct — ApplicationHandler implementation
419// ═══════════════════════════════════════════════════════════════════════════════
420
421/// The winit application handler that manages windows and processes commands.
422struct App {
423    cmd_rx: mpsc::Receiver<WindowCommand>,
424    event_tx: mpsc::Sender<WindowEvent_>,
425    /// Lazily initialized softbuffer context (needs a display handle from
426    /// the first window).
427    context: Option<softbuffer::Context<Arc<Window>>>,
428    /// Open windows, keyed by title.
429    windows: HashMap<String, WindowState>,
430}
431
432impl ApplicationHandler<UserEvent> for App {
433    fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
434        // Windows are created on demand when Show commands arrive.
435        // No action needed here.
436    }
437
438    fn user_event(&mut self, event_loop: &ActiveEventLoop, _event: UserEvent) {
439        // Drain all pending commands from the channel.
440        while let Ok(cmd) = self.cmd_rx.try_recv() {
441            match cmd {
442                WindowCommand::Show { title, framebuffer } => {
443                    self.handle_show(event_loop, title, framebuffer);
444                }
445                WindowCommand::Exit => {
446                    self.handle_exit(event_loop);
447                    return;
448                }
449            }
450        }
451    }
452
453    fn window_event(
454        &mut self,
455        event_loop: &ActiveEventLoop,
456        window_id: WindowId,
457        event: WindowEvent,
458    ) {
459        match event {
460            WindowEvent::CloseRequested => {
461                self.handle_close(event_loop, window_id);
462            }
463            WindowEvent::RedrawRequested => {
464                self.handle_redraw(window_id);
465            }
466            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
467                if let PhysicalKey::Code(key) = event.physical_key {
468                    // Find the title for this window.
469                    let title = self.title_for_window(window_id);
470                    if let Some(title) = title {
471                        let _ = self.event_tx.send(WindowEvent_::KeyPressed {
472                            key,
473                            window_title: title,
474                        });
475                    }
476                }
477            }
478            _ => {}
479        }
480    }
481}
482
483impl App {
484    /// Handle a `Show` command: create or update a window.
485    fn handle_show(
486        &mut self,
487        event_loop: &ActiveEventLoop,
488        title: String,
489        framebuffer: Framebuffer,
490    ) {
491        if let Some(state) = self.windows.get_mut(&title) {
492            // Update existing window.
493            let w = framebuffer.width;
494            let h = framebuffer.height;
495            state.framebuffer = framebuffer;
496            state.window.request_redraw();
497            log::debug!("window updated: \"{title}\" ({w}×{h})");
498        } else {
499            // Create a new window.
500            let w = framebuffer.width;
501            let h = framebuffer.height;
502
503            let attrs = WindowAttributes::default()
504                .with_title(&title)
505                .with_inner_size(winit::dpi::LogicalSize::new(w, h));
506
507            let window = match event_loop.create_window(attrs) {
508                Ok(win) => Arc::new(win),
509                Err(e) => {
510                    log::warn!("failed to create window \"{title}\": {e}");
511                    return;
512                }
513            };
514
515            // Lazily initialize the softbuffer context from the first window.
516            if self.context.is_none() {
517                match softbuffer::Context::new(window.clone()) {
518                    Ok(ctx) => self.context = Some(ctx),
519                    Err(e) => {
520                        log::warn!("failed to create softbuffer context: {e}");
521                        return;
522                    }
523                }
524            }
525
526            let context = self.context.as_ref().unwrap();
527
528            let surface = match softbuffer::Surface::new(context, window.clone()) {
529                Ok(s) => s,
530                Err(e) => {
531                    log::warn!("failed to create softbuffer surface for \"{title}\": {e}");
532                    return;
533                }
534            };
535
536            let mut state = WindowState {
537                window,
538                surface,
539                framebuffer,
540            };
541
542            // Do an initial blit so the window shows content immediately.
543            state.blit(&title);
544
545            log::debug!("window created: \"{title}\" ({w}×{h})");
546
547            self.windows.insert(title, state);
548        }
549    }
550
551    /// Handle a `CloseRequested` event for a specific window.
552    fn handle_close(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId) {
553        let title = self.title_for_window(window_id);
554        if let Some(title) = title {
555            log::debug!("window closed: \"{title}\"");
556            self.windows.remove(&title);
557            let _ = self.event_tx.send(WindowEvent_::WindowClosed { title });
558        }
559
560        if self.windows.is_empty() {
561            log::debug!("all windows closed, exiting event loop");
562            let _ = self.event_tx.send(WindowEvent_::AllClosed);
563            event_loop.exit();
564        }
565    }
566
567    /// Handle a `RedrawRequested` event for a specific window.
568    fn handle_redraw(&mut self, window_id: WindowId) {
569        let title = self.title_for_window(window_id);
570        if let Some(title) = title {
571            if let Some(state) = self.windows.get_mut(&title) {
572                state.blit(&title);
573            }
574        }
575    }
576
577    /// Handle an `Exit` command: close all windows and exit.
578    fn handle_exit(&mut self, event_loop: &ActiveEventLoop) {
579        log::debug!("exit command received, closing all windows");
580        self.windows.clear();
581        let _ = self.event_tx.send(WindowEvent_::AllClosed);
582        event_loop.exit();
583    }
584
585    /// Find the title of the window with the given ID.
586    ///
587    /// Linear search is fine — we expect very few windows (< 10).
588    fn title_for_window(&self, window_id: WindowId) -> Option<String> {
589        for (title, state) in &self.windows {
590            if state.window.id() == window_id {
591                return Some(title.clone());
592            }
593        }
594        None
595    }
596}
597
598// ═══════════════════════════════════════════════════════════════════════════════
599// 3.5  WindowState internal struct
600// ═══════════════════════════════════════════════════════════════════════════════
601
602/// Internal state for a single debug window.
603struct WindowState {
604    window: Arc<Window>,
605    surface: softbuffer::Surface<Arc<Window>, Arc<Window>>,
606    framebuffer: Framebuffer,
607}
608
609impl WindowState {
610    /// Blit the framebuffer to the window surface.
611    ///
612    /// Handles window resizing via nearest-neighbor scaling in the `u32`
613    /// domain. This avoids re-running the display strategy on resize.
614    fn blit(&mut self, title: &str) {
615        let size = self.window.inner_size();
616        let win_w = size.width;
617        let win_h = size.height;
618
619        // Skip blit if window is zero-sized (e.g. minimized).
620        if win_w == 0 || win_h == 0 {
621            return;
622        }
623
624        // Safety: we checked non-zero above.
625        let nz_w = NonZeroU32::new(win_w).unwrap();
626        let nz_h = NonZeroU32::new(win_h).unwrap();
627
628        if let Err(e) = self.surface.resize(nz_w, nz_h) {
629            log::warn!("failed to resize surface for \"{title}\": {e}");
630            return;
631        }
632
633        log::trace!(
634            "surface resized: \"{}\" {}×{} → {}×{}",
635            title,
636            self.framebuffer.width,
637            self.framebuffer.height,
638            win_w,
639            win_h
640        );
641
642        let mut buffer = match self.surface.buffer_mut() {
643            Ok(buf) => buf,
644            Err(e) => {
645                log::warn!("failed to get buffer for \"{title}\": {e}");
646                return;
647            }
648        };
649
650        if win_w == self.framebuffer.width && win_h == self.framebuffer.height {
651            // Direct copy — dimensions match exactly.
652            buffer[..self.framebuffer.data.len()].copy_from_slice(&self.framebuffer.data);
653        } else {
654            // Nearest-neighbor scale from framebuffer to window buffer.
655            scale_blit(&self.framebuffer, &mut buffer, win_w, win_h);
656        }
657
658        if let Err(e) = buffer.present() {
659            log::warn!("failed to present buffer for \"{title}\": {e}");
660        }
661    }
662}
663
664// ═══════════════════════════════════════════════════════════════════════════════
665// 3.5a  Nearest-neighbor scaling
666// ═══════════════════════════════════════════════════════════════════════════════
667
668/// Nearest-neighbor scale from a [`Framebuffer`] into a destination `u32` buffer.
669///
670/// Operates entirely in the `u32` domain — no pixel conversion needed.
671/// This is fast enough for a debug tool and avoids re-running the display
672/// strategy on window resize.
673fn scale_blit(src: &Framebuffer, dst: &mut [u32], dst_w: u32, dst_h: u32) {
674    // Handle edge cases.
675    if src.width == 0 || src.height == 0 {
676        // Source is empty — fill destination with black.
677        for pixel in dst.iter_mut() {
678            *pixel = 0;
679        }
680        return;
681    }
682
683    for dy in 0..dst_h {
684        let sy = (dy as u64 * src.height as u64 / dst_h as u64) as u32;
685        let dst_row_start = (dy * dst_w) as usize;
686        let src_row_start = (sy * src.width) as usize;
687
688        for dx in 0..dst_w {
689            let sx = (dx as u64 * src.width as u64 / dst_w as u64) as u32;
690            dst[dst_row_start + dx as usize] = src.data[src_row_start + sx as usize];
691        }
692    }
693}
694
695// ═══════════════════════════════════════════════════════════════════════════════
696// 3.7  One-shot show() convenience function
697// ═══════════════════════════════════════════════════════════════════════════════
698
699/// Display a single image and block until the window is closed or a key
700/// is pressed.
701///
702/// This is the simplest way to inspect an image during development.
703/// Internally converts the image to a framebuffer on the **calling
704/// thread**, then displays it using the winit event loop.
705///
706/// # Blocking behavior
707///
708/// This function blocks until the user presses a key or closes the window.
709/// Calling `show()` multiple times in sequence is valid — the event loop
710/// is created once and reused via `run_app_on_demand`.
711///
712/// # Platform notes
713///
714/// On macOS, this function **must** be called from the main thread.
715///
716/// Uses [`EventLoopExtRunOnDemand`] internally, which is supported on
717/// Windows, Linux, macOS, and Android. Not available on iOS or Web
718/// (but those platforms are not relevant for debug visualization).
719///
720/// # Mixing with `DebugDisplay::run()`
721///
722/// Do not mix `show()` and [`DebugDisplay::run()`] in the same process.
723/// Each creates its own event loop, and winit only allows one per process.
724/// Use [`DebugDisplay::run()`] for multi-window interactive workflows.
725///
726/// # Examples
727///
728/// ```no_run
729/// use fovea_display::{show, Identity};
730/// use fovea::image::Image;
731/// use fovea::pixel::Srgb8;
732///
733/// let img = Image::fill(100, 100, Srgb8::new(128, 64, 200));
734/// show("Preview", &img, Identity);
735/// ```
736pub fn show<V, S>(title: &str, image: &V, strategy: S)
737where
738    V: ImageView,
739    V::Pixel: Copy,
740    S: DisplayStrategy<V::Pixel>,
741{
742    use std::cell::RefCell;
743
744    thread_local! {
745        /// Lazily-initialized event loop reused across all `show()` calls
746        /// on this thread. Winit only allows one `EventLoop` per process,
747        /// so we create it once and drive it with `run_app_on_demand`.
748        static SHOW_EVENT_LOOP: RefCell<EventLoop<UserEvent>> = RefCell::new(
749            EventLoop::<UserEvent>::with_user_event()
750                .build()
751                .expect("failed to create event loop for show()")
752        );
753    }
754
755    // Convert to Framebuffer BEFORE entering the event loop — this avoids
756    // Send bounds on V and S. The Framebuffer (Vec<u32> + dimensions) is
757    // Send and can be moved into the background thread.
758    let fb = Framebuffer::from_image(image, strategy);
759    let title = title.to_string();
760
761    SHOW_EVENT_LOOP.with(|cell| {
762        let mut event_loop = cell.borrow_mut();
763
764        let proxy = event_loop.create_proxy();
765
766        // Command channel: background thread → event loop.
767        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
768
769        // Event channel: event loop → background thread.
770        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
771
772        let ctx = DisplayContext {
773            cmd_tx: cmd_tx.clone(),
774            event_rx,
775            notifier: Notifier::from_proxy(proxy.clone()),
776        };
777
778        // Spawn the user's show+wait_key logic on a background thread.
779        let bg_cmd_tx = cmd_tx;
780        let bg_notifier = Notifier::from_proxy(proxy);
781        std::thread::spawn(move || {
782            ctx.show_framebuffer(&title, fb);
783            let _ = ctx.wait_key();
784
785            // Signal the event loop to exit so run_app_on_demand returns.
786            let _ = bg_cmd_tx.send(WindowCommand::Exit);
787            if !bg_notifier.notify() {
788                log::warn!("event loop proxy send failed (event loop has exited)");
789            }
790        });
791
792        // Drive the event loop until the background thread signals Exit.
793        let mut app = App {
794            cmd_rx,
795            event_tx,
796            context: None,
797            windows: HashMap::new(),
798        };
799
800        event_loop
801            .run_app_on_demand(&mut app)
802            .expect("event loop terminated with error");
803    });
804}
805
806// ═══════════════════════════════════════════════════════════════════════════════
807// Tests
808// ═══════════════════════════════════════════════════════════════════════════════
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use crate::Identity;
814    use crate::strategy::Framebuffer;
815    use fovea::image::Image;
816    use fovea::pixel::Srgba8;
817
818    /// Helper: create a `DisplayContext` with mock channels.
819    ///
820    /// Returns `(ctx, cmd_rx, event_tx)` so the test can inspect
821    /// commands and inject events.
822    fn make_test_ctx() -> (
823        DisplayContext,
824        mpsc::Receiver<WindowCommand>,
825        mpsc::Sender<WindowEvent_>,
826    ) {
827        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
828        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
829        let ctx = DisplayContext::new_for_test(cmd_tx, event_rx);
830        (ctx, cmd_rx, event_tx)
831    }
832
833    // ── scale_blit tests ───────────────────────────────────────────────
834
835    #[test]
836    fn scale_blit_identity() {
837        let src = Framebuffer::from_raw(2, 2, vec![0xAA, 0xBB, 0xCC, 0xDD]);
838        let mut dst = vec![0u32; 4];
839        scale_blit(&src, &mut dst, 2, 2);
840        assert_eq!(dst, vec![0xAA, 0xBB, 0xCC, 0xDD]);
841    }
842
843    #[test]
844    fn scale_blit_upscale_2x() {
845        // 1×1 → 2×2: all pixels should be the same.
846        let src = Framebuffer::from_raw(1, 1, vec![0xFF0000]);
847        let mut dst = vec![0u32; 4];
848        scale_blit(&src, &mut dst, 2, 2);
849        assert_eq!(dst, vec![0xFF0000, 0xFF0000, 0xFF0000, 0xFF0000]);
850    }
851
852    #[test]
853    fn scale_blit_downscale() {
854        // 2×2 → 1×1: should pick pixel (0,0).
855        let src = Framebuffer::from_raw(2, 2, vec![0xAA, 0xBB, 0xCC, 0xDD]);
856        let mut dst = vec![0u32; 1];
857        scale_blit(&src, &mut dst, 1, 1);
858        assert_eq!(dst, vec![0xAA]);
859    }
860
861    #[test]
862    fn scale_blit_empty_source() {
863        let src = Framebuffer::from_raw(0, 0, vec![]);
864        let mut dst = vec![0x12345678u32; 4];
865        scale_blit(&src, &mut dst, 2, 2);
866        // Should fill with black.
867        assert_eq!(dst, vec![0, 0, 0, 0]);
868    }
869
870    #[test]
871    fn scale_blit_non_square_upscale() {
872        // 2×1 → 4×2
873        let src = Framebuffer::from_raw(2, 1, vec![0xAA, 0xBB]);
874        let mut dst = vec![0u32; 8];
875        scale_blit(&src, &mut dst, 4, 2);
876        // Row 0: 0xAA, 0xAA, 0xBB, 0xBB
877        // Row 1: 0xAA, 0xAA, 0xBB, 0xBB (same row mapped)
878        assert_eq!(dst, vec![0xAA, 0xAA, 0xBB, 0xBB, 0xAA, 0xAA, 0xBB, 0xBB]);
879    }
880
881    #[test]
882    fn scale_blit_3x2_to_6x4() {
883        // Source:
884        // [1, 2, 3]
885        // [4, 5, 6]
886        let src = Framebuffer::from_raw(3, 2, vec![1, 2, 3, 4, 5, 6]);
887        let mut dst = vec![0u32; 24]; // 6×4
888        scale_blit(&src, &mut dst, 6, 4);
889
890        // Expected nearest-neighbor 2x:
891        // [1, 1, 2, 2, 3, 3]
892        // [1, 1, 2, 2, 3, 3]
893        // [4, 4, 5, 5, 6, 6]
894        // [4, 4, 5, 5, 6, 6]
895        let expected = vec![
896            1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 4, 4, 5, 5, 6, 6,
897        ];
898        assert_eq!(dst, expected);
899    }
900
901    // ── Notifier tests ─────────────────────────────────────────────────
902
903    #[test]
904    fn notifier_noop_returns_true() {
905        let n = Notifier(Box::new(|| true));
906        assert!(n.notify());
907    }
908
909    #[test]
910    fn notifier_failing_returns_false() {
911        let n = Notifier(Box::new(|| false));
912        assert!(!n.notify());
913    }
914
915    // ── DisplayContext::show_framebuffer tests ──────────────────────────
916
917    #[test]
918    fn show_framebuffer_sends_command() {
919        let (ctx, cmd_rx, _event_tx) = make_test_ctx();
920        let fb = Framebuffer::from_raw(2, 2, vec![0xAA, 0xBB, 0xCC, 0xDD]);
921        ctx.show_framebuffer("my title", fb);
922
923        match cmd_rx.recv().unwrap() {
924            WindowCommand::Show { title, framebuffer } => {
925                assert_eq!(title, "my title");
926                assert_eq!(framebuffer.width, 2);
927                assert_eq!(framebuffer.height, 2);
928                assert_eq!(framebuffer.data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
929            }
930            WindowCommand::Exit => panic!("expected Show, got Exit"),
931        }
932    }
933
934    #[test]
935    fn show_framebuffer_with_closed_channel_does_not_panic() {
936        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
937        let (_event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
938        let ctx = DisplayContext::new_for_test(cmd_tx, event_rx);
939
940        // Drop the receiver so the channel is closed.
941        drop(cmd_rx);
942
943        // Should not panic, just log a warning.
944        let fb = Framebuffer::from_raw(1, 1, vec![0]);
945        ctx.show_framebuffer("dead", fb);
946    }
947
948    #[test]
949    fn show_framebuffer_notifier_failure_does_not_panic() {
950        let (cmd_tx, _cmd_rx) = mpsc::channel::<WindowCommand>();
951        let (_event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
952        // Notifier that always "fails".
953        let ctx = DisplayContext {
954            cmd_tx,
955            event_rx,
956            notifier: Notifier(Box::new(|| false)),
957        };
958
959        let fb = Framebuffer::from_raw(1, 1, vec![0]);
960        ctx.show_framebuffer("fail-notify", fb);
961        // Should not panic — just logs a warning.
962    }
963
964    // ── DisplayContext::show tests ──────────────────────────────────────
965
966    #[test]
967    fn show_converts_image_and_sends_command() {
968        let (ctx, cmd_rx, _event_tx) = make_test_ctx();
969
970        let img = Image::fill(2, 2, Srgba8::new(255, 0, 0, 255));
971        ctx.show("red", &img, Identity);
972
973        match cmd_rx.recv().unwrap() {
974            WindowCommand::Show { title, framebuffer } => {
975                assert_eq!(title, "red");
976                assert_eq!(framebuffer.width, 2);
977                assert_eq!(framebuffer.height, 2);
978                // Red = 0x00FF0000 in 0x00RRGGBB
979                assert!(framebuffer.data.iter().all(|&p| p == 0x00FF0000));
980            }
981            WindowCommand::Exit => panic!("expected Show, got Exit"),
982        }
983    }
984
985    #[test]
986    fn show_zero_size_image() {
987        let (ctx, cmd_rx, _event_tx) = make_test_ctx();
988
989        let img = Image::<Srgba8>::zero(0, 0);
990        ctx.show("empty", &img, Identity);
991
992        match cmd_rx.recv().unwrap() {
993            WindowCommand::Show { title, framebuffer } => {
994                assert_eq!(title, "empty");
995                assert_eq!(framebuffer.width, 0);
996                assert_eq!(framebuffer.height, 0);
997                assert!(framebuffer.data.is_empty());
998            }
999            WindowCommand::Exit => panic!("expected Show, got Exit"),
1000        }
1001    }
1002
1003    // ── DisplayContext::exit tests ──────────────────────────────────────
1004
1005    #[test]
1006    fn exit_sends_exit_command() {
1007        let (ctx, cmd_rx, _event_tx) = make_test_ctx();
1008        ctx.exit();
1009
1010        match cmd_rx.recv().unwrap() {
1011            WindowCommand::Exit => {} // ok
1012            WindowCommand::Show { .. } => panic!("expected Exit, got Show"),
1013        }
1014    }
1015
1016    #[test]
1017    fn exit_with_closed_channel_does_not_panic() {
1018        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
1019        let (_event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
1020        let ctx = DisplayContext::new_for_test(cmd_tx, event_rx);
1021        drop(cmd_rx);
1022        ctx.exit(); // should not panic
1023    }
1024
1025    // ── DisplayContext::wait_key tests ──────────────────────────────────
1026
1027    #[test]
1028    fn wait_key_returns_key_on_key_pressed() {
1029        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1030
1031        event_tx
1032            .send(WindowEvent_::KeyPressed {
1033                key: KeyCode::Space,
1034                window_title: "test".to_string(),
1035            })
1036            .unwrap();
1037
1038        assert_eq!(ctx.wait_key(), Some(KeyCode::Space));
1039    }
1040
1041    #[test]
1042    fn wait_key_skips_window_closed_waits_for_key() {
1043        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1044
1045        // WindowClosed then KeyPressed.
1046        event_tx
1047            .send(WindowEvent_::WindowClosed {
1048                title: "closing".to_string(),
1049            })
1050            .unwrap();
1051        event_tx
1052            .send(WindowEvent_::KeyPressed {
1053                key: KeyCode::Enter,
1054                window_title: "remaining".to_string(),
1055            })
1056            .unwrap();
1057
1058        // wait_key should skip WindowClosed and return the key.
1059        assert_eq!(ctx.wait_key(), Some(KeyCode::Enter));
1060    }
1061
1062    #[test]
1063    fn wait_key_returns_none_on_all_closed() {
1064        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1065
1066        event_tx.send(WindowEvent_::AllClosed).unwrap();
1067
1068        assert_eq!(ctx.wait_key(), None);
1069    }
1070
1071    #[test]
1072    fn wait_key_returns_none_on_channel_disconnect() {
1073        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1074
1075        // Drop the sender — recv() will return Err.
1076        drop(event_tx);
1077
1078        assert_eq!(ctx.wait_key(), None);
1079    }
1080
1081    #[test]
1082    fn wait_key_skips_multiple_window_closed() {
1083        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1084
1085        event_tx
1086            .send(WindowEvent_::WindowClosed {
1087                title: "a".to_string(),
1088            })
1089            .unwrap();
1090        event_tx
1091            .send(WindowEvent_::WindowClosed {
1092                title: "b".to_string(),
1093            })
1094            .unwrap();
1095        event_tx
1096            .send(WindowEvent_::KeyPressed {
1097                key: KeyCode::KeyA,
1098                window_title: "c".to_string(),
1099            })
1100            .unwrap();
1101
1102        assert_eq!(ctx.wait_key(), Some(KeyCode::KeyA));
1103    }
1104
1105    // ── DisplayContext::wait_key_timeout tests ──────────────────────────
1106
1107    #[test]
1108    fn wait_key_timeout_returns_key_before_timeout() {
1109        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1110
1111        event_tx
1112            .send(WindowEvent_::KeyPressed {
1113                key: KeyCode::Escape,
1114                window_title: "w".to_string(),
1115            })
1116            .unwrap();
1117
1118        let result = ctx.wait_key_timeout(Duration::from_secs(5));
1119        assert_eq!(result, Some(KeyCode::Escape));
1120    }
1121
1122    #[test]
1123    fn wait_key_timeout_returns_none_on_timeout() {
1124        let (ctx, _cmd_rx, _event_tx) = make_test_ctx();
1125
1126        // No events sent — should time out.
1127        let result = ctx.wait_key_timeout(Duration::from_millis(10));
1128        assert_eq!(result, None);
1129    }
1130
1131    #[test]
1132    fn wait_key_timeout_returns_none_on_all_closed() {
1133        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1134
1135        event_tx.send(WindowEvent_::AllClosed).unwrap();
1136
1137        let result = ctx.wait_key_timeout(Duration::from_secs(5));
1138        assert_eq!(result, None);
1139    }
1140
1141    #[test]
1142    fn wait_key_timeout_returns_none_on_disconnect() {
1143        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1144        drop(event_tx);
1145
1146        let result = ctx.wait_key_timeout(Duration::from_secs(5));
1147        assert_eq!(result, None);
1148    }
1149
1150    #[test]
1151    fn wait_key_timeout_skips_window_closed() {
1152        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1153
1154        event_tx
1155            .send(WindowEvent_::WindowClosed {
1156                title: "gone".to_string(),
1157            })
1158            .unwrap();
1159        event_tx
1160            .send(WindowEvent_::KeyPressed {
1161                key: KeyCode::KeyZ,
1162                window_title: "still here".to_string(),
1163            })
1164            .unwrap();
1165
1166        let result = ctx.wait_key_timeout(Duration::from_secs(5));
1167        assert_eq!(result, Some(KeyCode::KeyZ));
1168    }
1169
1170    #[test]
1171    fn wait_key_timeout_zero_returns_none_immediately() {
1172        let (ctx, _cmd_rx, event_tx) = make_test_ctx();
1173
1174        // Even though an event is queued, zero timeout should return None
1175        // (or the key if it happens to be received — but we test zero-duration
1176        // short-circuits the loop).
1177        event_tx
1178            .send(WindowEvent_::KeyPressed {
1179                key: KeyCode::KeyX,
1180                window_title: "w".to_string(),
1181            })
1182            .unwrap();
1183
1184        // With Duration::ZERO the remaining-is-zero check fires immediately.
1185        let result = ctx.wait_key_timeout(Duration::ZERO);
1186        // The event may or may not be received in zero time — just verify
1187        // no panic and result is a valid Option.
1188        assert!(result.is_none() || result == Some(KeyCode::KeyX));
1189    }
1190
1191    // ── Combined show + wait_key workflow ──────────────────────────────
1192
1193    #[test]
1194    fn show_then_wait_key_workflow() {
1195        let (ctx, cmd_rx, event_tx) = make_test_ctx();
1196
1197        // Simulate: user shows an image, then waits for a key.
1198        let img = Image::fill(4, 4, Srgba8::new(0, 255, 0, 255));
1199        ctx.show("green", &img, Identity);
1200
1201        // Verify the Show command arrived.
1202        match cmd_rx.recv().unwrap() {
1203            WindowCommand::Show { title, framebuffer } => {
1204                assert_eq!(title, "green");
1205                assert_eq!(framebuffer.width, 4);
1206                assert_eq!(framebuffer.height, 4);
1207                assert_eq!(framebuffer.data.len(), 16);
1208                // Green = 0x0000FF00
1209                assert!(framebuffer.data.iter().all(|&p| p == 0x0000FF00));
1210            }
1211            WindowCommand::Exit => panic!("expected Show"),
1212        }
1213
1214        // Simulate: event loop sends a key event back.
1215        event_tx
1216            .send(WindowEvent_::KeyPressed {
1217                key: KeyCode::KeyQ,
1218                window_title: "green".to_string(),
1219            })
1220            .unwrap();
1221
1222        assert_eq!(ctx.wait_key(), Some(KeyCode::KeyQ));
1223    }
1224
1225    #[test]
1226    fn multiple_shows_then_exit_workflow() {
1227        let (ctx, cmd_rx, _event_tx) = make_test_ctx();
1228
1229        let img1 = Image::fill(1, 1, Srgba8::new(255, 0, 0, 255));
1230        let img2 = Image::fill(1, 1, Srgba8::new(0, 0, 255, 255));
1231        ctx.show("red", &img1, Identity);
1232        ctx.show("blue", &img2, Identity);
1233        ctx.exit();
1234
1235        // Drain: Show, Show, Exit.
1236        let mut titles = Vec::new();
1237        let mut got_exit = false;
1238        for _ in 0..3 {
1239            match cmd_rx.recv().unwrap() {
1240                WindowCommand::Show { title, .. } => titles.push(title),
1241                WindowCommand::Exit => got_exit = true,
1242            }
1243        }
1244        assert_eq!(titles, vec!["red", "blue"]);
1245        assert!(got_exit);
1246    }
1247
1248    // ── WindowCommand / WindowEvent_ channel protocol tests ────────────
1249
1250    #[test]
1251    fn channel_show_sends_correct_command() {
1252        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
1253
1254        cmd_tx
1255            .send(WindowCommand::Show {
1256                title: "test".to_string(),
1257                framebuffer: Framebuffer::from_raw(2, 2, vec![0, 0, 0, 0]),
1258            })
1259            .unwrap();
1260
1261        match cmd_rx.recv().unwrap() {
1262            WindowCommand::Show { title, framebuffer } => {
1263                assert_eq!(title, "test");
1264                assert_eq!(framebuffer.width, 2);
1265                assert_eq!(framebuffer.height, 2);
1266                assert_eq!(framebuffer.data.len(), 4);
1267            }
1268            WindowCommand::Exit => panic!("expected Show, got Exit"),
1269        }
1270    }
1271
1272    #[test]
1273    fn channel_exit_command() {
1274        let (cmd_tx, cmd_rx) = mpsc::channel::<WindowCommand>();
1275
1276        cmd_tx.send(WindowCommand::Exit).unwrap();
1277        match cmd_rx.recv().unwrap() {
1278            WindowCommand::Exit => {} // ok
1279            WindowCommand::Show { .. } => panic!("expected Exit, got Show"),
1280        }
1281    }
1282
1283    #[test]
1284    fn channel_key_pressed_event() {
1285        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
1286
1287        event_tx
1288            .send(WindowEvent_::KeyPressed {
1289                key: KeyCode::Escape,
1290                window_title: "test window".to_string(),
1291            })
1292            .unwrap();
1293
1294        match event_rx.recv().unwrap() {
1295            WindowEvent_::KeyPressed { key, window_title } => {
1296                assert_eq!(key, KeyCode::Escape);
1297                assert_eq!(window_title, "test window");
1298            }
1299            other => panic!("expected KeyPressed, got {:?}", other),
1300        }
1301    }
1302
1303    #[test]
1304    fn channel_window_closed_event() {
1305        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
1306
1307        event_tx
1308            .send(WindowEvent_::WindowClosed {
1309                title: "my window".to_string(),
1310            })
1311            .unwrap();
1312
1313        match event_rx.recv().unwrap() {
1314            WindowEvent_::WindowClosed { title } => {
1315                assert_eq!(title, "my window");
1316            }
1317            other => panic!("expected WindowClosed, got {:?}", other),
1318        }
1319    }
1320
1321    #[test]
1322    fn channel_all_closed_event() {
1323        let (event_tx, event_rx) = mpsc::channel::<WindowEvent_>();
1324
1325        event_tx.send(WindowEvent_::AllClosed).unwrap();
1326
1327        match event_rx.recv().unwrap() {
1328            WindowEvent_::AllClosed => {} // ok
1329            other => panic!("expected AllClosed, got {:?}", other),
1330        }
1331    }
1332
1333    // ── Misc ───────────────────────────────────────────────────────────
1334
1335    #[test]
1336    fn framebuffer_is_send() {
1337        // Framebuffer must be Send so it can cross thread boundaries.
1338        fn assert_send<T: Send>() {}
1339        assert_send::<Framebuffer>();
1340    }
1341
1342    #[test]
1343    fn display_context_is_send() {
1344        fn assert_send<T: Send>() {}
1345        assert_send::<DisplayContext>();
1346    }
1347}