spatialrust_viewer/
native.rs1use std::sync::{Arc, Mutex};
2
3use winit::{
4 application::ApplicationHandler,
5 dpi::LogicalSize,
6 event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent},
7 event_loop::{ActiveEventLoop, EventLoop},
8 window::{Window, WindowAttributes, WindowId},
9};
10
11use crate::{InputAction, ViewerController, ViewerError, ViewerResult, ViewerState, ViewportSize};
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct NativeViewerOptions {
16 pub title: String,
18 pub width: u32,
20 pub height: u32,
22}
23
24impl Default for NativeViewerOptions {
25 fn default() -> Self {
26 Self { title: "SpatialRust Viewer".into(), width: 1280, height: 720 }
27 }
28}
29
30pub struct NativeViewer {
36 state: Arc<Mutex<ViewerState>>,
37 controller: ViewerController,
38 options: NativeViewerOptions,
39}
40
41impl NativeViewer {
42 pub fn try_new(state: ViewerState, options: NativeViewerOptions) -> ViewerResult<Self> {
44 if options.title.trim().is_empty() || options.width == 0 || options.height == 0 {
45 return Err(ViewerError::Native(
46 "title and non-zero window dimensions are required".into(),
47 ));
48 }
49 Ok(Self {
50 state: Arc::new(Mutex::new(state)),
51 controller: ViewerController::default(),
52 options,
53 })
54 }
55
56 #[must_use]
58 pub fn state(&self) -> Arc<Mutex<ViewerState>> {
59 Arc::clone(&self.state)
60 }
61
62 pub fn run(self) -> ViewerResult<()> {
64 let event_loop =
65 EventLoop::new().map_err(|error| ViewerError::Native(error.to_string()))?;
66 let mut application = NativeApplication {
67 state: self.state,
68 controller: self.controller,
69 options: self.options,
70 window: None,
71 cursor: None,
72 drag: None,
73 };
74 event_loop.run_app(&mut application).map_err(|error| ViewerError::Native(error.to_string()))
75 }
76}
77
78struct NativeApplication {
79 state: Arc<Mutex<ViewerState>>,
80 controller: ViewerController,
81 options: NativeViewerOptions,
82 window: Option<Window>,
83 cursor: Option<(f64, f64)>,
84 drag: Option<MouseButton>,
85}
86
87impl NativeApplication {
88 fn apply(&self, action: InputAction) {
89 if let Ok(mut state) = self.state.lock() {
90 let _ = self.controller.apply(&mut state, action);
91 }
92 }
93}
94
95impl ApplicationHandler for NativeApplication {
96 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
97 if self.window.is_some() {
98 return;
99 }
100 let attributes = WindowAttributes::default()
101 .with_title(self.options.title.clone())
102 .with_inner_size(LogicalSize::new(self.options.width, self.options.height));
103 match event_loop.create_window(attributes) {
104 Ok(window) => self.window = Some(window),
105 Err(error) => {
106 eprintln!("SpatialRust native viewer: {error}");
107 event_loop.exit();
108 }
109 }
110 }
111
112 fn window_event(
113 &mut self,
114 event_loop: &ActiveEventLoop,
115 window_id: WindowId,
116 event: WindowEvent,
117 ) {
118 if self.window.as_ref().map_or(true, |window| window.id() != window_id) {
119 return;
120 }
121 match event {
122 WindowEvent::CloseRequested => event_loop.exit(),
123 WindowEvent::Resized(size) if size.width > 0 && size.height > 0 => {
124 if let Ok(viewport) = ViewportSize::try_new(size.width, size.height) {
125 self.apply(InputAction::Resize(viewport));
126 }
127 }
128 WindowEvent::DroppedFile(path) => {
129 self.apply(InputAction::FileDropped(path.to_string_lossy().into_owned()));
130 }
131 WindowEvent::MouseInput { state, button, .. } => {
132 self.drag = (state == ElementState::Pressed).then_some(button);
133 }
134 WindowEvent::CursorMoved { position, .. } => {
135 if let (Some((last_x, last_y)), Some(button)) = (self.cursor, self.drag) {
136 let delta_x = (position.x - last_x) as f32;
137 let delta_y = (position.y - last_y) as f32;
138 let action = if button == MouseButton::Left {
139 InputAction::Orbit { delta_x, delta_y }
140 } else {
141 InputAction::Pan { delta_x, delta_y }
142 };
143 self.apply(action);
144 }
145 self.cursor = Some((position.x, position.y));
146 }
147 WindowEvent::MouseWheel { delta, .. } => {
148 let amount = match delta {
149 MouseScrollDelta::LineDelta(_, y) => y,
150 MouseScrollDelta::PixelDelta(position) => position.y as f32 / 40.0,
151 };
152 self.apply(InputAction::Zoom(amount));
153 }
154 WindowEvent::RedrawRequested => {
155 if let Some(window) = &self.window {
158 window.request_redraw();
159 }
160 }
161 _ => {}
162 }
163 }
164}