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