1use 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#[derive(Clone, Debug)]
78pub struct DesktopConfig {
79 pub window_title: String,
81
82 pub window_width: i32,
84
85 pub window_height: i32,
87
88 pub led_radius: f32,
90
91 pub high_dpi: bool,
93
94 pub orthographic_view: bool,
96
97 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
115pub struct Desktop<Dim, Layout> {
125 driver: DesktopDriver<Dim, Layout>,
126 stage: DesktopStageOptions,
127}
128
129impl Desktop<Dim1d, ()> {
130 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 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 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 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 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 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
340pub 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#[derive(Debug)]
372pub enum DesktopError {
373 ChannelSend,
375
376 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
397enum LedMessage {
399 UpdateColors(Vec<LinearSrgb>),
401
402 UpdateBrightness(f32),
404
405 UpdateColorCorrection(ColorCorrection),
407
408 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
455struct Camera {
459 distance: f32,
461
462 target: Vec3,
464
465 yaw: f32,
467
468 pitch: f32,
470
471 aspect_ratio: f32,
473
474 use_orthographic: bool,
476
477 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 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 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 fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
515 self.aspect_ratio = aspect_ratio;
516 }
517
518 fn toggle_projection_mode(&mut self) {
520 self.use_orthographic = !self.use_orthographic;
521 }
522
523 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 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 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 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 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 fn view_projection_matrix(&self) -> Mat4 {
574 self.projection_matrix() * self.view_matrix()
575 }
576}
577
578struct 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 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 let x = 2.0 * screen_x / width - 1.0;
600 let y = 1.0 - 2.0 * screen_y / height;
601
602 let proj_inv = camera.projection_matrix().inverse();
604 let view_inv = camera.view_matrix().inverse();
605
606 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 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 let mut closest_led = None;
628 let mut closest_distance = f32::MAX;
629
630 for (i, &position) in self.positions.iter().enumerate() {
631 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 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 fn clear_selection(&mut self) {
657 self.selected_led = None;
658 }
659}
660
661struct 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 fn mouse_motion_event(&mut self, x: f32, y: f32) {
677 self.egui_mq.mouse_motion_event(x, y);
678 }
679
680 fn mouse_wheel_event(&mut self, x: f32, y: f32) {
682 self.egui_mq.mouse_wheel_event(x, y);
683 }
684
685 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 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 fn key_down_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
697 self.egui_mq.key_down_event(keycode, keymods);
698 }
699
700 fn key_up_event(&mut self, keycode: KeyCode, keymods: KeyMods) {
702 self.egui_mq.key_up_event(keycode, keymods);
703 }
704
705 fn char_event(&mut self, character: char) {
707 self.egui_mq.char_event(character);
708 }
709
710 #[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 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 let (bright_red, bright_green, bright_blue) =
733 (red * brightness, green * brightness, blue * brightness);
734
735 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 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 ui.label(format!(
761 "Linear RGB: R={:.3}, G={:.3}, B={:.3}",
762 red, green, blue,
763 ));
764
765 ui.label(format!("Global Brightness: {:.3}", brightness));
767
768 ui.label(format!(
770 "Brightness-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
771 bright_red, bright_green, bright_blue
772 ));
773
774 ui.label(format!(
776 "Global Color Correction: R={:.3}, G={:.3}, B={:.3}",
777 correction.red, correction.green, correction.blue
778 ));
779
780 ui.label(format!(
782 "Correction-adjusted RGB: R={:.3}, G={:.3}, B={:.3}",
783 correct_red, correct_green, correct_blue
784 ));
785
786 ui.label(format!(
788 "Final sRGB: R={:.3}, G={:.3}, B={:.3}",
789 srgb_red, srgb_green, srgb_blue
790 ));
791
792 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); if ui.button("Deselect").clicked() {
805 led_picker.selected_led = None;
806 }
807 });
808 }
809 });
810 }
811
812 fn draw(&mut self, ctx: &mut dyn RenderingBackend) {
814 self.egui_mq.draw(ctx);
815 }
816}
817
818struct 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 ctx.begin_default_pass(PassAction::clear_color(r, g, b, a));
945
946 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
956struct 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
964struct 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 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 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 let ui_manager = UiManager::new(&mut *ctx);
1014
1015 let led_picker = LedPicker::new(positions.clone(), config.led_radius);
1017
1018 let renderer = Renderer::new(&mut *ctx, config.led_radius);
1020
1021 let (width, height) = window::screen_size();
1023 let camera = Camera::new(width / height, config.orthographic_view);
1024
1025 let colors_buffer = (0..positions.len())
1027 .map(|_| Vec4::new(0.0, 0.0, 0.0, 1.0))
1028 .collect();
1029
1030 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 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 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 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 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 let (red, green, blue) = (
1113 red * self.brightness,
1114 green * self.brightness,
1115 blue * self.brightness,
1116 );
1117
1118 let (red, green, blue) = (
1120 red * self.correction.red,
1121 green * self.correction.green,
1122 blue * self.correction.blue,
1123 );
1124
1125 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 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 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 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 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 if !self.mouse_down {
1194 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
1234mod shader {
1236 use miniquad::*;
1237
1238 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 pub const FRAGMENT: &str = r#"#version 100
1258 varying lowp vec4 color;
1259
1260 void main() {
1261 gl_FragColor = color;
1262 }
1263 "#;
1264
1265 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 #[repr(C)]
1277 pub struct Uniforms {
1278 pub mvp: glam::Mat4,
1279 }
1280}