blinksy_desktop/
driver.rs

1//! # Desktop Simulation Driver
2//!
3//! This module provides a graphical simulation of LED layouts and patterns for desktop development
4//! and debugging. It provides an implementation of the [`Driver`] trait, allowing it to be used as
5//! a drop-in replacement for physical LED hardware.
6//!
7//! The simulator creates a 3D visualization window where:
8//!
9//! - LEDs are represented as small 3D objects
10//! - LED positions match the layout's physical arrangement
11//! - Colors and brightness updates are displayed in real-time
12//!
13//! ## Controls
14//!
15//! - **Mouse drag**: Rotate the camera around the LEDs
16//! - **Mouse wheel**: Zoom in/out
17//! - **R key**: Reset camera to default position
18//! - **O key**: Toggle between orthographic and perspective projection
19//!
20//! ## Usage
21//!
22//! ```rust,no_run
23//! use blinksy::{
24//!     ControlBuilder,
25//!     layout2d,
26//!     layout::{Shape2d, Vec2},
27//!     patterns::rainbow::{Rainbow, RainbowParams}
28//! };
29//! use blinksy_desktop::{driver::Desktop, time::elapsed_in_ms};
30//!
31//! // Define your layout
32//! layout2d!(
33//!     PanelLayout,
34//!     [Shape2d::Grid {
35//!         start: Vec2::new(-1., -1.),
36//!         horizontal_end: Vec2::new(1., -1.),
37//!         vertical_end: Vec2::new(-1., 1.),
38//!         horizontal_pixel_count: 16,
39//!         vertical_pixel_count: 16,
40//!         serpentine: true,
41//!     }]
42//! );
43//!
44//! Desktop::new_2d::<PanelLayout>().start(|driver| {
45//!     // Create a control using the Desktop driver instead of physical hardware
46//!     let mut control = ControlBuilder::new_2d()
47//!         .with_layout::<PanelLayout>()
48//!         .with_pattern::<Rainbow>(RainbowParams::default())
49//!         .with_driver(driver)
50//!         .build();
51//!
52//!     // Run your normal animation loop
53//!     loop {
54//!         control.tick(elapsed_in_ms()).unwrap();
55//!         std::thread::sleep(std::time::Duration::from_millis(16));
56//!     }
57//! });
58//! ```
59//!
60//! [`Driver`]: blinksy::driver::Driver
61
62use blinksy::{
63    color::{ColorCorrection, FromColor, LinearSrgb, Srgb},
64    driver::Driver,
65    layout::{Layout1d, Layout2d, Layout3d, LayoutForDim},
66    markers::{Dim1d, Dim2d, Dim3d},
67};
68use core::{fmt, marker::PhantomData};
69use egui_miniquad as egui_mq;
70use glam::{vec3, Mat4, Vec3, Vec4, Vec4Swizzles};
71use miniquad::*;
72use std::sync::mpsc::{channel, Receiver, SendError, Sender};
73
74/// Configuration options for the desktop simulator.
75///
76/// Allows customizing the appearance and behavior of the LED simulator window.
77#[derive(Clone, Debug)]
78pub struct DesktopConfig {
79    /// Window title
80    pub window_title: String,
81
82    /// Window width in pixels
83    pub window_width: i32,
84
85    /// Window height in pixels
86    pub window_height: i32,
87
88    /// Size of the LED representations
89    pub led_radius: f32,
90
91    /// Whether to use high DPI mode
92    pub high_dpi: bool,
93
94    /// Initial camera view mode (true for orthographic, false for perspective)
95    pub orthographic_view: bool,
96
97    /// Background color (R, G, B, A) where each component is 0.0 - 1.0
98    pub background_color: (f32, f32, f32, f32),
99}
100
101impl Default for DesktopConfig {
102    fn default() -> Self {
103        Self {
104            window_title: "Blinksy".to_string(),
105            window_width: 540,
106            window_height: 540,
107            led_radius: 0.05,
108            high_dpi: true,
109            orthographic_view: true,
110            background_color: (0.1, 0.1, 0.1, 1.0),
111        }
112    }
113}
114
115/// Desktop simulator for LED layouts in a desktop window.
116///
117/// Provides a visual representation of your LED layout using miniquad,
118/// with a `Driver` to render updates.
119///
120/// # Type Parameters
121///
122/// * `Dim` - The dimension marker (Dim1d or Dim2d or Dim3d)
123/// * `Layout` - The specific layout type
124pub struct Desktop<Dim, Layout> {
125    driver: DesktopDriver<Dim, Layout>,
126    stage: DesktopStageOptions,
127}
128
129impl Desktop<Dim1d, ()> {
130    /// Creates a new graphics simulator for 1D layouts.
131    ///
132    /// This method initializes a rendering window showing a linear strip of LEDs.
133    ///
134    /// # Type Parameters
135    ///
136    /// * `Layout` - The layout type implementing Layout1d
137    ///
138    /// # Returns
139    ///
140    /// A Desktop simulator configured for the specified 1D layout
141    pub fn new_1d<Layout>() -> Desktop<Dim1d, Layout>
142    where
143        Layout: Layout1d,
144    {
145        Self::new_1d_with_config::<Layout>(DesktopConfig::default())
146    }
147
148    /// Creates a new graphics simulator for 1D layouts with custom configuration.
149    ///
150    /// # Type Parameters
151    ///
152    /// * `Layout` - The layout type implementing Layout1d
153    ///
154    /// # Parameters
155    ///
156    /// * `config` - Configuration options for the simulator window
157    ///
158    /// # Returns
159    ///
160    /// A Desktop simulator configured for the specified 1D layout
161    pub fn new_1d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim1d, Layout>
162    where
163        Layout: Layout1d,
164    {
165        let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
166        for x in Layout::points() {
167            positions.push(vec3(x, 0.0, 0.0));
168        }
169
170        let (sender, receiver) = channel();
171        let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
172        let is_window_closed_2 = is_window_closed.clone();
173
174        let driver = DesktopDriver {
175            dim: PhantomData,
176            layout: PhantomData,
177            brightness: 1.0,
178            correction: ColorCorrection::default(),
179            sender,
180            is_window_closed,
181        };
182        let stage = DesktopStageOptions {
183            positions,
184            receiver,
185            config,
186            is_window_closed: is_window_closed_2,
187        };
188
189        Desktop { driver, stage }
190    }
191}
192
193impl Desktop<Dim2d, ()> {
194    /// Creates a new graphics simulator for 2D layouts.
195    ///
196    /// This method initializes a rendering window showing a 2D arrangement of LEDs
197    /// based on the layout's coordinates.
198    ///
199    /// # Type Parameters
200    ///
201    /// * `Layout` - The layout type implementing Layout2d
202    ///
203    /// # Returns
204    ///
205    /// A Desktop simulator configured for the specified 2D layout
206    pub fn new_2d<Layout>() -> Desktop<Dim2d, Layout>
207    where
208        Layout: Layout2d,
209    {
210        Self::new_2d_with_config::<Layout>(DesktopConfig::default())
211    }
212
213    /// Creates a new graphics simulator for 2D layouts with custom configuration.
214    ///
215    /// # Type Parameters
216    ///
217    /// * `Layout` - The layout type implementing Layout2d
218    ///
219    /// # Parameters
220    ///
221    /// * `config` - Configuration options for the simulator window
222    ///
223    /// # Returns
224    ///
225    /// A Desktop simulator configured for the specified 2D layout
226    pub fn new_2d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim2d, Layout>
227    where
228        Layout: Layout2d,
229    {
230        let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
231        for point in Layout::points() {
232            positions.push(vec3(point.x, point.y, 0.0));
233        }
234
235        let (sender, receiver) = channel();
236        let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
237        let is_window_closed_2 = is_window_closed.clone();
238
239        let driver = DesktopDriver {
240            dim: PhantomData,
241            layout: PhantomData,
242            brightness: 1.0,
243            correction: ColorCorrection::default(),
244            sender,
245            is_window_closed,
246        };
247        let stage = DesktopStageOptions {
248            positions,
249            receiver,
250            config,
251            is_window_closed: is_window_closed_2,
252        };
253
254        Desktop { driver, stage }
255    }
256}
257
258impl Desktop<Dim3d, ()> {
259    /// Creates a new graphics simulator for 3D layouts.
260    ///
261    /// This method initializes a rendering window showing a 3D arrangement of LEDs
262    /// based on the layout's coordinates.
263    ///
264    /// # Type Parameters
265    ///
266    /// * `Layout` - The layout type implementing Layout3d
267    ///
268    /// # Returns
269    ///
270    /// A Desktop simulator configured for the specified 3D layout
271    pub fn new_3d<Layout>() -> Desktop<Dim3d, Layout>
272    where
273        Layout: Layout3d,
274    {
275        Self::new_3d_with_config::<Layout>(DesktopConfig::default())
276    }
277
278    /// Creates a new graphics simulator for 3D layouts with custom configuration.
279    ///
280    /// # Type Parameters
281    ///
282    /// * `Layout` - The layout type implementing Layout3d
283    ///
284    /// # Parameters
285    ///
286    /// * `config` - Configuration options for the simulator window
287    ///
288    /// # Returns
289    ///
290    /// A Desktop simulator configured for the specified 3D layout
291    pub fn new_3d_with_config<Layout>(config: DesktopConfig) -> Desktop<Dim3d, Layout>
292    where
293        Layout: Layout3d,
294    {
295        let mut positions = Vec::with_capacity(Layout::PIXEL_COUNT);
296        for point in Layout::points() {
297            positions.push(vec3(point.x, point.y, point.z));
298        }
299
300        let (sender, receiver) = channel();
301        let is_window_closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
302        let is_window_closed_2 = is_window_closed.clone();
303
304        let driver = DesktopDriver {
305            dim: PhantomData,
306            layout: PhantomData,
307            brightness: 1.0,
308            correction: ColorCorrection::default(),
309            sender,
310            is_window_closed,
311        };
312        let stage = DesktopStageOptions {
313            positions,
314            receiver,
315            config,
316            is_window_closed: is_window_closed_2,
317        };
318
319        Desktop { driver, stage }
320    }
321}
322
323impl<Dim, Layout> Desktop<Dim, Layout>
324where
325    Dim: 'static + Send,
326    Layout: 'static + Send,
327{
328    pub fn start<F>(self, f: F)
329    where
330        F: 'static + FnOnce(DesktopDriver<Dim, Layout>) + Send,
331    {
332        let Self { driver, stage } = self;
333
334        std::thread::spawn(move || f(driver));
335
336        DesktopStage::start(move || DesktopStage::new(stage));
337    }
338}
339
340/// Desktop driver for simulating LED layouts in a desktop window.
341///
342/// This struct implements the `Driver` trait.
343///
344/// # Type Parameters
345///
346/// * `Dim` - The dimension marker (Dim1d or Dim2d or Dim3d)
347/// * `Layout` - The specific layout type
348pub struct DesktopDriver<Dim, Layout> {
349    dim: PhantomData<Dim>,
350    layout: PhantomData<Layout>,
351    brightness: f32,
352    correction: ColorCorrection,
353    sender: Sender<LedMessage>,
354    is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
355}
356
357impl<Dim, Layout> DesktopDriver<Dim, Layout> {
358    fn send(&self, message: LedMessage) -> Result<(), DesktopError> {
359        if self
360            .is_window_closed
361            .load(std::sync::atomic::Ordering::Relaxed)
362        {
363            return Err(DesktopError::WindowClosed);
364        }
365        self.sender.send(message)?;
366        Ok(())
367    }
368}
369
370/// Errors that can occur when using the Desktop driver.
371#[derive(Debug)]
372pub enum DesktopError {
373    /// Sending to the render thread failed because it has already hung up.
374    ChannelSend,
375
376    /// Window has been closed.
377    WindowClosed,
378}
379
380impl fmt::Display for DesktopError {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        match self {
383            DesktopError::ChannelSend => write!(f, "render thread channel disconnected"),
384            DesktopError::WindowClosed => write!(f, "window closed"),
385        }
386    }
387}
388
389impl core::error::Error for DesktopError {}
390
391impl From<SendError<LedMessage>> for DesktopError {
392    fn from(_: SendError<LedMessage>) -> Self {
393        DesktopError::ChannelSend
394    }
395}
396
397/// Messages for communication with the rendering thread.
398enum LedMessage {
399    /// Update the colors of all LEDs
400    UpdateColors(Vec<LinearSrgb>),
401
402    /// Update the global brightness
403    UpdateBrightness(f32),
404
405    /// Update the global color correction
406    UpdateColorCorrection(ColorCorrection),
407
408    /// Terminate the rendering thread
409    Quit,
410}
411
412impl<Dim, Layout> Driver for DesktopDriver<Dim, Layout>
413where
414    Layout: LayoutForDim<Dim>,
415{
416    type Error = DesktopError;
417    type Color = LinearSrgb;
418
419    fn write<I, C>(
420        &mut self,
421        pixels: I,
422        brightness: f32,
423        correction: ColorCorrection,
424    ) -> Result<(), Self::Error>
425    where
426        I: IntoIterator<Item = C>,
427        Self::Color: FromColor<C>,
428    {
429        if self.brightness != brightness {
430            self.brightness = brightness;
431            self.send(LedMessage::UpdateBrightness(brightness))?;
432        }
433
434        if self.correction != correction {
435            self.correction = correction;
436            self.send(LedMessage::UpdateColorCorrection(correction))?;
437        }
438
439        let colors: Vec<LinearSrgb> = pixels
440            .into_iter()
441            .map(|color| LinearSrgb::from_color(color))
442            .collect();
443
444        self.send(LedMessage::UpdateColors(colors))?;
445        Ok(())
446    }
447}
448
449impl<Dim, Layout> Drop for DesktopDriver<Dim, Layout> {
450    fn drop(&mut self) {
451        let _ = self.send(LedMessage::Quit);
452    }
453}
454
455/// Camera controller for the 3D LED visualization.
456///
457/// Handles camera movement, rotation, and projection calculations.
458struct Camera {
459    /// Distance from camera to target
460    distance: f32,
461
462    /// Position camera is looking at
463    target: Vec3,
464
465    /// Horizontal rotation angle in radians
466    yaw: f32,
467
468    /// Vertical rotation angle in radians
469    pitch: f32,
470
471    /// Width/height ratio of the viewport
472    aspect_ratio: f32,
473
474    /// Use orthographic (true) or perspective (false) projection
475    use_orthographic: bool,
476
477    /// Field of view in radians (used for perspective projection)
478    fov: f32,
479}
480
481impl Camera {
482    const DEFAULT_DISTANCE: f32 = 2.0;
483    const DEFAULT_TARGET: Vec3 = Vec3::ZERO;
484    const DEFAULT_YAW: f32 = core::f32::consts::PI * 0.5;
485    const DEFAULT_PITCH: f32 = 0.0;
486    const MIN_DISTANCE: f32 = 0.5;
487    const MAX_DISTANCE: f32 = 10.0;
488    const MAX_PITCH: f32 = core::f32::consts::PI / 2.0 - 0.1;
489    const MIN_PITCH: f32 = -core::f32::consts::PI / 2.0 + 0.1;
490
491    /// Create a new camera with default settings
492    fn new(aspect_ratio: f32, use_orthographic: bool) -> Self {
493        let default_fov = 2.0 * ((1.0 / Self::DEFAULT_DISTANCE).atan());
494        Self {
495            distance: Self::DEFAULT_DISTANCE,
496            target: Self::DEFAULT_TARGET,
497            yaw: Self::DEFAULT_YAW,
498            pitch: Self::DEFAULT_PITCH,
499            aspect_ratio,
500            use_orthographic,
501            fov: default_fov,
502        }
503    }
504
505    /// Reset camera to default position and orientation
506    fn reset(&mut self) {
507        self.distance = Self::DEFAULT_DISTANCE;
508        self.target = Self::DEFAULT_TARGET;
509        self.yaw = Self::DEFAULT_YAW;
510        self.pitch = Self::DEFAULT_PITCH;
511    }
512
513    /// Update camera aspect ratio when window is resized
514    fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
515        self.aspect_ratio = aspect_ratio;
516    }
517
518    /// Toggle between orthographic and perspective projection
519    fn toggle_projection_mode(&mut self) {
520        self.use_orthographic = !self.use_orthographic;
521    }
522
523    /// Update camera rotation based on mouse movement
524    fn rotate(&mut self, delta_x: f32, delta_y: f32) {
525        self.yaw -= delta_x * 0.01;
526        self.pitch += delta_y * 0.01;
527        self.pitch = self.pitch.clamp(Self::MIN_PITCH, Self::MAX_PITCH);
528    }
529
530    /// Update camera zoom based on mouse wheel movement
531    fn zoom(&mut self, delta: f32) {
532        self.distance -= delta * 0.2;
533        self.distance = self.distance.clamp(Self::MIN_DISTANCE, Self::MAX_DISTANCE);
534    }
535
536    /// Calculate the current camera position based on spherical coordinates
537    fn position(&self) -> Vec3 {
538        let x = self.distance * self.pitch.cos() * self.yaw.cos();
539        let y = self.distance * self.pitch.sin();
540        let z = self.distance * self.pitch.cos() * self.yaw.sin();
541        self.target + vec3(x, y, z)
542    }
543
544    /// Calculate view matrix for the current camera state
545    fn view_matrix(&self) -> Mat4 {
546        let eye = self.position();
547        let up = if self.pitch.abs() > std::f32::consts::PI * 0.49 {
548            Vec3::new(self.yaw.sin(), 0.0, -self.yaw.cos())
549        } else {
550            Vec3::Y
551        };
552        Mat4::look_at_rh(eye, self.target, up)
553    }
554
555    /// Calculate projection matrix based on current settings
556    fn projection_matrix(&self) -> Mat4 {
557        if self.use_orthographic {
558            let vertical_size = 1.0 * (self.distance / 2.0);
559            Mat4::orthographic_rh_gl(
560                -vertical_size * self.aspect_ratio,
561                vertical_size * self.aspect_ratio,
562                -vertical_size,
563                vertical_size,
564                -100.0,
565                100.0,
566            )
567        } else {
568            Mat4::perspective_rh_gl(self.fov, self.aspect_ratio, 0.1, 100.0)
569        }
570    }
571
572    /// Get the combined view-projection matrix
573    fn view_projection_matrix(&self) -> Mat4 {
574        self.projection_matrix() * self.view_matrix()
575    }
576}
577
578/// Manages LED selection and interaction
579struct LedPicker {
580    positions: Vec<Vec3>,
581    selected_led: Option<usize>,
582    radius: f32,
583}
584
585impl LedPicker {
586    fn new(positions: Vec<Vec3>, radius: f32) -> Self {
587        Self {
588            positions,
589            selected_led: None,
590            radius,
591        }
592    }
593
594    /// Convert screen coordinates to a ray in world space
595    fn screen_pos_to_ray(&self, screen_x: f32, screen_y: f32, camera: &Camera) -> (Vec3, Vec3) {
596        let (width, height) = window::screen_size();
597
598        // Normalize device coordinates (-1 to 1)
599        let x = 2.0 * screen_x / width - 1.0;
600        let y = 1.0 - 2.0 * screen_y / height;
601
602        // Compute inverse matrices
603        let proj_inv = camera.projection_matrix().inverse();
604        let view_inv = camera.view_matrix().inverse();
605
606        // Calculate ray origin and direction
607        let near_point = proj_inv * Vec4::new(x, y, -1.0, 1.0);
608        let far_point = proj_inv * Vec4::new(x, y, 1.0, 1.0);
609
610        let near_point = near_point / near_point.w;
611        let far_point = far_point / far_point.w;
612
613        let near_point_world = view_inv * near_point;
614        let far_point_world = view_inv * far_point;
615
616        let origin = near_point_world.xyz();
617        let direction = (far_point_world.xyz() - near_point_world.xyz()).normalize();
618
619        (origin, direction)
620    }
621
622    /// Pick an LED based on screen coordinates
623    fn pick_led(&self, screen_x: f32, screen_y: f32, camera: &Camera) -> Option<usize> {
624        let (ray_origin, ray_direction) = self.screen_pos_to_ray(screen_x, screen_y, camera);
625
626        // Find the closest LED that intersects with the ray
627        let mut closest_led = None;
628        let mut closest_distance = f32::MAX;
629
630        for (i, &position) in self.positions.iter().enumerate() {
631            // Sphere-ray intersection test
632            let oc = ray_origin - position;
633            let a = ray_direction.dot(ray_direction);
634            let b = 2.0 * oc.dot(ray_direction);
635            let c = oc.dot(oc) - self.radius * self.radius;
636            let discriminant = b * b - 4.0 * a * c;
637
638            if discriminant > 0.0 {
639                let t = (-b - discriminant.sqrt()) / (2.0 * a);
640                if t > 0.0 && t < closest_distance {
641                    closest_distance = t;
642                    closest_led = Some(i);
643                }
644            }
645        }
646
647        closest_led
648    }
649
650    /// Try to select an LED at the given screen coordinates
651    fn try_select_at(&mut self, screen_x: f32, screen_y: f32, camera: &Camera) {
652        self.selected_led = self.pick_led(screen_x, screen_y, camera);
653    }
654
655    /// Clear the current selection
656    fn clear_selection(&mut self) {
657        self.selected_led = None;
658    }
659}
660
661/// Manages UI state and rendering
662struct UiManager {
663    egui_mq: egui_mq::EguiMq,
664    want_mouse_capture: bool,
665}
666
667impl UiManager {
668    fn new(ctx: &mut dyn RenderingBackend) -> Self {
669        Self {
670            egui_mq: egui_mq::EguiMq::new(ctx),
671            want_mouse_capture: false,
672        }
673    }
674
675    /// Forward mouse motion events to egui
676    fn mouse_motion_event(&mut self, x: f32, y: f32) {
677        self.egui_mq.mouse_motion_event(x, y);
678    }
679
680    /// Forward mouse wheel events to egui
681    fn mouse_wheel_event(&mut self, x: f32, y: f32) {
682        self.egui_mq.mouse_wheel_event(x, y);
683    }
684
685    /// Forward mouse button down events to egui
686    fn mouse_button_down_event(&mut self, button: MouseButton, x: f32, y: f32) {
687        self.egui_mq.mouse_button_down_event(button, x, y);
688    }
689
690    /// Forward mouse button up events to egui
691    fn mouse_button_up_event(&mut self, button: MouseButton, x: f32, y: f32) {
692        self.egui_mq.mouse_button_up_event(button, x, y);
693    }
694
695    /// Forward key down events to egui
696    fn key_down_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
697        self.egui_mq.key_down_event(keycode, keymods);
698    }
699
700    /// Forward key up events to egui
701    fn key_up_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
702        self.egui_mq.key_up_event(keycode, keymods);
703    }
704
705    /// Forward character events to egui
706    fn char_event(&mut self, character: char) {
707        self.egui_mq.char_event(character);
708    }
709
710    /// Render the LED information UI
711    #[allow(clippy::too_many_arguments)]
712    fn render_led_info(
713        &mut self,
714        ctx: &mut dyn RenderingBackend,
715        led_picker: &mut LedPicker,
716        positions: &[Vec3],
717        colors: &[LinearSrgb],
718        brightness: f32,
719        correction: ColorCorrection,
720    ) {
721        self.egui_mq.run(ctx, |_mq_ctx, egui_ctx| {
722            self.want_mouse_capture = egui_ctx.wants_pointer_input();
723
724            // Only show LED info window if an LED is selected
725            if let Some(led_idx) = led_picker.selected_led {
726                let pos = positions[led_idx];
727                let color = colors[led_idx];
728
729                let (red, green, blue) = (color.red, color.green, color.blue);
730
731                // Apply brightness
732                let (bright_red, bright_green, bright_blue) =
733                    (red * brightness, green * brightness, blue * brightness);
734
735                // Apply color correction
736                let (correct_red, correct_green, correct_blue) = (
737                    bright_red * correction.red,
738                    bright_green * correction.green,
739                    bright_blue * correction.blue,
740                );
741
742                // Convert to sRGB
743                let Srgb {
744                    red: srgb_red,
745                    green: srgb_green,
746                    blue: srgb_blue,
747                } = LinearSrgb::new(correct_red, correct_green, correct_blue).to_srgb();
748
749                egui::Window::new("LED Information")
750                    .collapsible(false)
751                    .resizable(false)
752                    .show(egui_ctx, |ui| {
753                        ui.label(format!("LED Index: {}", led_idx));
754                        ui.label(format!(
755                            "Position: ({:.3}, {:.3}, {:.3})",
756                            pos.x, pos.y, pos.z
757                        ));
758
759                        // Display raw RGB values
760                        ui.label(format!(
761                            "Linear RGB: R={:.3}, G={:.3}, B={:.3}",
762                            red, green, blue,
763                        ));
764
765                        // Display global brightness
766                        ui.label(format!("Global Brightness: {:.3}", brightness));
767
768                        // Display brightness-adjusted RGB values
769                        ui.label(format!(
770                            "Brightness-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
771                            bright_red, bright_green, bright_blue
772                        ));
773
774                        // Display global color correction
775                        ui.label(format!(
776                            "Global Color Correction: R={:.3}, G={:.3}, B={:.3}",
777                            correction.red, correction.green, correction.blue
778                        ));
779
780                        // Display brightness-adjusted RGB values
781                        ui.label(format!(
782                            "Correction-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
783                            correct_red, correct_green, correct_blue
784                        ));
785
786                        // Display sRGB values
787                        ui.label(format!(
788                            "Final sRGB: R={:.3}, G={:.3}, B={:.3}",
789                            srgb_red, srgb_green, srgb_blue
790                        ));
791
792                        // Show color preview
793                        let (_, color_rect) =
794                            ui.allocate_space(egui::vec2(ui.available_width(), 30.0));
795                        let color_preview = egui::Color32::from_rgb(
796                            (srgb_red * 255.0) as u8,
797                            (srgb_green * 255.0) as u8,
798                            (srgb_blue * 255.0) as u8,
799                        );
800                        ui.painter().rect_filled(color_rect, 4.0, color_preview);
801                        ui.add_space(10.0); // Space after the color preview
802
803                        // Deselect button
804                        if ui.button("Deselect").clicked() {
805                            led_picker.selected_led = None;
806                        }
807                    });
808            }
809        });
810    }
811
812    /// Draw egui content
813    fn draw(&mut self, ctx: &mut dyn RenderingBackend) {
814        self.egui_mq.draw(ctx);
815    }
816}
817
818/// Manages rendering of LEDs
819struct Renderer {
820    pipeline: Pipeline,
821    bindings: Bindings,
822}
823
824impl Renderer {
825    fn new(ctx: &mut dyn RenderingBackend, led_radius: f32) -> Self {
826        let vertex_buffer = Self::create_vertex_buffer(ctx, led_radius);
827        let index_buffer = Self::create_index_buffer(ctx);
828
829        let bindings = Bindings {
830            vertex_buffers: vec![vertex_buffer],
831            index_buffer,
832            images: vec![],
833        };
834
835        let shader = ctx
836            .new_shader(
837                ShaderSource::Glsl {
838                    vertex: shader::VERTEX,
839                    fragment: shader::FRAGMENT,
840                },
841                shader::meta(),
842            )
843            .unwrap();
844
845        let pipeline = ctx.new_pipeline(
846            &[
847                BufferLayout::default(),
848                BufferLayout {
849                    step_func: VertexStep::PerInstance,
850                    ..Default::default()
851                },
852                BufferLayout {
853                    step_func: VertexStep::PerInstance,
854                    ..Default::default()
855                },
856            ],
857            &[
858                VertexAttribute::with_buffer("in_pos", VertexFormat::Float3, 0),
859                VertexAttribute::with_buffer("in_color", VertexFormat::Float4, 0),
860                VertexAttribute::with_buffer("in_inst_pos", VertexFormat::Float3, 1),
861                VertexAttribute::with_buffer("in_inst_color", VertexFormat::Float4, 2),
862            ],
863            shader,
864            PipelineParams {
865                depth_test: Comparison::LessOrEqual,
866                depth_write: true,
867                ..Default::default()
868            },
869        );
870
871        Self { pipeline, bindings }
872    }
873
874    fn create_vertex_buffer(ctx: &mut dyn RenderingBackend, r: f32) -> BufferId {
875        #[rustfmt::skip]
876        let vertices: &[f32] = &[
877            0.0, -r, 0.0, 1.0, 0.0, 0.0, 1.0,
878            r, 0.0, r, 0.0, 1.0, 0.0, 1.0,
879            r, 0.0, -r, 0.0, 0.0, 1.0, 1.0,
880            -r, 0.0, -r, 1.0, 1.0, 0.0, 1.0,
881            -r, 0.0, r, 0.0, 1.0, 1.0, 1.0,
882            0.0, r, 0.0, 1.0, 0.0, 1.0, 1.0,
883        ];
884
885        ctx.new_buffer(
886            BufferType::VertexBuffer,
887            BufferUsage::Immutable,
888            BufferSource::slice(vertices),
889        )
890    }
891
892    fn create_index_buffer(ctx: &mut dyn RenderingBackend) -> BufferId {
893        #[rustfmt::skip]
894        let indices: &[u16] = &[
895            0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1,
896            5, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 1
897        ];
898
899        ctx.new_buffer(
900            BufferType::IndexBuffer,
901            BufferUsage::Immutable,
902            BufferSource::slice(indices),
903        )
904    }
905
906    fn update_positions_buffer(
907        &mut self,
908        ctx: &mut dyn RenderingBackend,
909        positions: &[Vec3],
910    ) -> BufferId {
911        let positions_buffer = ctx.new_buffer(
912            BufferType::VertexBuffer,
913            BufferUsage::Stream,
914            BufferSource::slice(positions),
915        );
916        self.bindings.vertex_buffers.push(positions_buffer);
917        positions_buffer
918    }
919
920    fn update_colors_buffer(
921        &mut self,
922        ctx: &mut dyn RenderingBackend,
923        colors: &[Vec4],
924    ) -> BufferId {
925        let colors_buffer = ctx.new_buffer(
926            BufferType::VertexBuffer,
927            BufferUsage::Stream,
928            BufferSource::slice(colors),
929        );
930        self.bindings.vertex_buffers.push(colors_buffer);
931        colors_buffer
932    }
933
934    fn render(
935        &self,
936        ctx: &mut dyn RenderingBackend,
937        positions: &[Vec3],
938        view_proj: Mat4,
939        background_color: (f32, f32, f32, f32),
940    ) {
941        let (r, g, b, a) = background_color;
942
943        // Clear the background
944        ctx.begin_default_pass(PassAction::clear_color(r, g, b, a));
945
946        // Draw the LEDs
947        ctx.apply_pipeline(&self.pipeline);
948        ctx.apply_bindings(&self.bindings);
949        ctx.apply_uniforms(UniformsSource::table(&shader::Uniforms { mvp: view_proj }));
950
951        ctx.draw(0, 24, positions.len() as i32);
952        ctx.end_render_pass();
953    }
954}
955
956/// Constructor options for `DesktopStage`.
957struct DesktopStageOptions {
958    pub positions: Vec<Vec3>,
959    pub receiver: Receiver<LedMessage>,
960    pub config: DesktopConfig,
961    pub is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
962}
963
964/// The rendering stage that handles the miniquad window and OpenGL drawing.
965struct DesktopStage {
966    ctx: Box<dyn RenderingBackend>,
967    positions: Vec<Vec3>,
968    colors: Vec<LinearSrgb>,
969    colors_buffer: Vec<Vec4>,
970    brightness: f32,
971    correction: ColorCorrection,
972    receiver: Receiver<LedMessage>,
973    camera: Camera,
974    config: DesktopConfig,
975    is_window_closed: std::sync::Arc<std::sync::atomic::AtomicBool>,
976    mouse_down: bool,
977    last_mouse_x: f32,
978    last_mouse_y: f32,
979    ui_manager: UiManager,
980    led_picker: LedPicker,
981    renderer: Renderer,
982}
983
984impl DesktopStage {
985    /// Start the rendering loop.
986    pub fn start<F, H>(f: F)
987    where
988        F: 'static + FnOnce() -> H,
989        H: EventHandler + 'static,
990    {
991        let conf = conf::Conf {
992            window_title: "Blinksy".to_string(),
993            window_width: 800,
994            window_height: 600,
995            high_dpi: true,
996            ..Default::default()
997        };
998        miniquad::start(conf, move || Box::new(f()));
999    }
1000
1001    /// Create a new DesktopStage with the given LED positions, colors, and configuration.
1002    fn new(options: DesktopStageOptions) -> Self {
1003        let DesktopStageOptions {
1004            positions,
1005            receiver,
1006            config,
1007            is_window_closed,
1008        } = options;
1009
1010        let mut ctx: Box<dyn RenderingBackend> = window::new_rendering_backend();
1011
1012        // Initialize UI manager
1013        let ui_manager = UiManager::new(&mut *ctx);
1014
1015        // Initialize LED picker
1016        let led_picker = LedPicker::new(positions.clone(), config.led_radius);
1017
1018        // Initialize renderer
1019        let renderer = Renderer::new(&mut *ctx, config.led_radius);
1020
1021        // Initialize camera
1022        let (width, height) = window::screen_size();
1023        let camera = Camera::new(width / height, config.orthographic_view);
1024
1025        // Initialize colors buffer
1026        let colors_buffer = (0..positions.len())
1027            .map(|_| Vec4::new(0.0, 0.0, 0.0, 1.0))
1028            .collect();
1029
1030        // Create the stage
1031        let mut stage = Self {
1032            ctx,
1033            positions: positions.clone(),
1034            colors: Vec::new(),
1035            colors_buffer,
1036            brightness: 1.0,
1037            correction: ColorCorrection::default(),
1038            receiver,
1039            camera,
1040            config,
1041            is_window_closed,
1042            mouse_down: false,
1043            last_mouse_x: 0.0,
1044            last_mouse_y: 0.0,
1045            ui_manager,
1046            led_picker,
1047            renderer,
1048        };
1049
1050        // Setup buffers
1051        stage
1052            .renderer
1053            .update_positions_buffer(&mut *stage.ctx, &positions);
1054        stage
1055            .renderer
1056            .update_colors_buffer(&mut *stage.ctx, &stage.colors_buffer);
1057
1058        stage
1059    }
1060
1061    /// Process any pending messages from the main thread.
1062    fn process_messages(&mut self) {
1063        while let Ok(message) = self.receiver.try_recv() {
1064            match message {
1065                LedMessage::UpdateColors(colors) => {
1066                    self.colors = colors;
1067                }
1068                LedMessage::UpdateBrightness(brightness) => {
1069                    self.brightness = brightness;
1070                }
1071                LedMessage::UpdateColorCorrection(correction) => {
1072                    self.correction = correction;
1073                }
1074                LedMessage::Quit => {
1075                    window::quit();
1076                }
1077            }
1078        }
1079    }
1080
1081    /// Handles input for camera controls
1082    fn handle_camera_input(&mut self, keycode: KeyCode) {
1083        match keycode {
1084            KeyCode::R => {
1085                self.camera.reset();
1086            }
1087            KeyCode::O => {
1088                self.camera.toggle_projection_mode();
1089            }
1090            KeyCode::Escape => {
1091                // Clear selection when Escape is pressed
1092                self.led_picker.clear_selection();
1093            }
1094            _ => {}
1095        }
1096    }
1097}
1098
1099impl EventHandler for DesktopStage {
1100    fn update(&mut self) {
1101        self.process_messages();
1102    }
1103
1104    fn draw(&mut self) {
1105        let colors_buffer: Vec<Vec4> = self
1106            .colors
1107            .iter()
1108            .map(|color| {
1109                let (red, green, blue) = (color.red, color.green, color.blue);
1110
1111                // Apply brightness
1112                let (red, green, blue) = (
1113                    red * self.brightness,
1114                    green * self.brightness,
1115                    blue * self.brightness,
1116                );
1117
1118                // Apply color correction
1119                let (red, green, blue) = (
1120                    red * self.correction.red,
1121                    green * self.correction.green,
1122                    blue * self.correction.blue,
1123                );
1124
1125                // Convert to sRGB
1126                let Srgb { red, green, blue } = LinearSrgb::new(red, green, blue).to_srgb();
1127
1128                Vec4::new(red, green, blue, 1.)
1129            })
1130            .collect();
1131
1132        // Update colors buffer
1133        self.colors_buffer = colors_buffer;
1134        self.ctx.buffer_update(
1135            self.renderer.bindings.vertex_buffers[2],
1136            BufferSource::slice(&self.colors_buffer),
1137        );
1138
1139        // Render the LEDs
1140        let view_proj = self.camera.view_projection_matrix();
1141        self.renderer.render(
1142            &mut *self.ctx,
1143            &self.positions,
1144            view_proj,
1145            self.config.background_color,
1146        );
1147
1148        // Render UI with LED info if needed
1149        self.ui_manager.render_led_info(
1150            &mut *self.ctx,
1151            &mut self.led_picker,
1152            &self.positions,
1153            &self.colors,
1154            self.brightness,
1155            self.correction,
1156        );
1157
1158        // Draw egui
1159        self.ui_manager.draw(&mut *self.ctx);
1160
1161        self.ctx.commit_frame();
1162    }
1163
1164    fn resize_event(&mut self, width: f32, height: f32) {
1165        self.camera.set_aspect_ratio(width / height);
1166    }
1167
1168    fn mouse_motion_event(&mut self, x: f32, y: f32) {
1169        self.ui_manager.mouse_motion_event(x, y);
1170
1171        if self.mouse_down && !self.ui_manager.want_mouse_capture {
1172            let dx = x - self.last_mouse_x;
1173            let dy = y - self.last_mouse_y;
1174            self.camera.rotate(dx, dy);
1175        }
1176        self.last_mouse_x = x;
1177        self.last_mouse_y = y;
1178    }
1179
1180    fn mouse_wheel_event(&mut self, x: f32, y: f32) {
1181        self.ui_manager.mouse_wheel_event(x, y);
1182
1183        if !self.ui_manager.want_mouse_capture {
1184            self.camera.zoom(y);
1185        }
1186    }
1187
1188    fn mouse_button_down_event(&mut self, button: MouseButton, x: f32, y: f32) {
1189        self.ui_manager.mouse_button_down_event(button, x, y);
1190
1191        if button == MouseButton::Left && !self.ui_manager.want_mouse_capture {
1192            // Check for LED selection on click
1193            if !self.mouse_down {
1194                // Only do picking when button is first pressed
1195                self.led_picker.try_select_at(x, y, &self.camera);
1196            }
1197
1198            self.mouse_down = true;
1199            self.last_mouse_x = x;
1200            self.last_mouse_y = y;
1201        }
1202    }
1203
1204    fn mouse_button_up_event(&mut self, button: MouseButton, x: f32, y: f32) {
1205        self.ui_manager.mouse_button_up_event(button, x, y);
1206
1207        if button == MouseButton::Left {
1208            self.mouse_down = false;
1209        }
1210    }
1211
1212    fn key_down_event(&mut self, keycode: KeyCode, keymods: KeyMods, _repeat: bool) {
1213        self.ui_manager.key_down_event(keycode, keymods);
1214
1215        if !self.ui_manager.want_mouse_capture {
1216            self.handle_camera_input(keycode);
1217        }
1218    }
1219
1220    fn key_up_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
1221        self.ui_manager.key_up_event(keycode, keymods);
1222    }
1223
1224    fn char_event(&mut self, character: char, _keymods: KeyMods, _repeat: bool) {
1225        self.ui_manager.char_event(character);
1226    }
1227
1228    fn quit_requested_event(&mut self) {
1229        self.is_window_closed
1230            .store(true, std::sync::atomic::Ordering::Relaxed);
1231    }
1232}
1233
1234/// Shader definitions for rendering LEDs
1235mod shader {
1236    use miniquad::*;
1237
1238    /// Vertex shader for LED rendering
1239    pub const VERTEX: &str = r#"#version 100
1240    attribute vec3 in_pos;
1241    attribute vec4 in_color;
1242    attribute vec3 in_inst_pos;
1243    attribute vec4 in_inst_color;
1244
1245    varying lowp vec4 color;
1246
1247    uniform mat4 mvp;
1248
1249    void main() {
1250        vec4 pos = vec4(in_pos + in_inst_pos, 1.0);
1251        gl_Position = mvp * pos;
1252        color = in_inst_color;
1253    }
1254    "#;
1255
1256    /// Fragment shader for LED rendering
1257    pub const FRAGMENT: &str = r#"#version 100
1258    varying lowp vec4 color;
1259
1260    void main() {
1261        gl_FragColor = color;
1262    }
1263    "#;
1264
1265    /// Shader metadata describing uniforms
1266    pub fn meta() -> ShaderMeta {
1267        ShaderMeta {
1268            images: vec![],
1269            uniforms: UniformBlockLayout {
1270                uniforms: vec![UniformDesc::new("mvp", UniformType::Mat4)],
1271            },
1272        }
1273    }
1274
1275    /// Uniform structure for shader
1276    #[repr(C)]
1277    pub struct Uniforms {
1278        pub mvp: glam::Mat4,
1279    }
1280}