use std::sync::Arc;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowAttributes, WindowId};
use crate::core::{InputEvent3D, InteractivePlot3DSession, PlottingError, PointerButton3D, Result};
use crate::render::three_d::gpu::SurfacePresenter3D;
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(350);
const DOUBLE_CLICK_DISTANCE_PX: f64 = 5.0;
const LINE_SCROLL_DELTA_PX: f32 = 50.0;
pub fn show_interactive_3d(session: InteractivePlot3DSession) -> Result<()> {
let (width, height) = session.size_px();
let title = session.title().unwrap_or("ruviz 3d").to_string();
let event_loop = EventLoop::new()
.map_err(|error| PlottingError::RenderError(format!("3D event loop: {error}")))?;
event_loop.set_control_flow(ControlFlow::Wait);
let mut app = ThreeDWindowApp {
session,
title,
requested_size: PhysicalSize::new(width.max(1), height.max(1)),
window: None,
presenter: None,
cursor: PhysicalPosition::new(0.0, 0.0),
last_left_click: None,
error: None,
};
event_loop
.run_app(&mut app)
.map_err(|error| PlottingError::RenderError(format!("3D event loop: {error}")))?;
app.error.map_or(Ok(()), Err)
}
struct ThreeDWindowApp {
session: InteractivePlot3DSession,
title: String,
requested_size: PhysicalSize<u32>,
window: Option<Arc<Window>>,
presenter: Option<SurfacePresenter3D>,
cursor: PhysicalPosition<f64>,
last_left_click: Option<(Instant, PhysicalPosition<f64>)>,
error: Option<PlottingError>,
}
impl ThreeDWindowApp {
fn handle_event(&mut self, event: InputEvent3D) -> Result<()> {
let result = self.session.handle_input(event)?;
if result.request_redraw {
self.request_redraw();
}
Ok(())
}
fn request_redraw(&self) {
if let Some(window) = &self.window {
window.request_redraw();
}
}
fn resize(&mut self, size: PhysicalSize<u32>, scale_factor: f64) -> Result<()> {
if size.width == 0 || size.height == 0 {
return Ok(());
}
self.requested_size = size;
self.session
.resize(size.width, size.height, scale_factor as f32)?;
if let Some(presenter) = &mut self.presenter {
presenter.resize(size.width, size.height)?;
}
self.request_redraw();
Ok(())
}
fn render(&mut self) -> Result<()> {
let Some(window) = self.window.as_ref() else {
return Ok(());
};
let size = window.inner_size();
if size.width == 0 || size.height == 0 {
return Ok(());
}
self.session
.resize(size.width, size.height, window.scale_factor() as f32)?;
let presenter = self.presenter.as_mut().ok_or_else(|| {
PlottingError::RenderError("direct 3d surface presenter was not retained".to_string())
})?;
let diagnostics = self.session.present_direct(presenter)?;
if let Some(diagnostics) = diagnostics {
debug_assert_eq!(diagnostics.actual_backend, "gpu3d-surface");
debug_assert_eq!(diagnostics.readback_bytes, 0);
}
Ok(())
}
fn pointer_button(button: MouseButton) -> Option<PointerButton3D> {
match button {
MouseButton::Left => Some(PointerButton3D::Left),
MouseButton::Middle => Some(PointerButton3D::Middle),
MouseButton::Right => Some(PointerButton3D::Right),
_ => None,
}
}
fn wheel_delta(delta: MouseScrollDelta) -> f32 {
match delta {
MouseScrollDelta::LineDelta(_, y) => y * LINE_SCROLL_DELTA_PX,
MouseScrollDelta::PixelDelta(position) => -(position.y as f32),
}
}
fn fail(&mut self, event_loop: &ActiveEventLoop, error: PlottingError) {
self.error = Some(error);
event_loop.exit();
}
}
impl ApplicationHandler for ThreeDWindowApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attributes = WindowAttributes::default()
.with_title(self.title.clone())
.with_inner_size(self.requested_size);
match event_loop.create_window(attributes) {
Ok(window) => {
let window = Arc::new(window);
self.requested_size = window.inner_size();
match SurfacePresenter3D::new(
Arc::clone(&window),
self.requested_size.width.max(1),
self.requested_size.height.max(1),
) {
Ok(presenter) => {
self.presenter = Some(presenter);
self.window = Some(window);
}
Err(error) => {
self.fail(event_loop, error);
return;
}
}
self.request_redraw();
}
Err(error) => self.fail(
event_loop,
PlottingError::RenderError(format!("3D window creation: {error}")),
),
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
let result = match event {
WindowEvent::CloseRequested => {
event_loop.exit();
Ok(())
}
WindowEvent::Resized(size) => {
let scale = self
.window
.as_ref()
.map_or(1.0, |window| window.scale_factor());
self.resize(size, scale)
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let size = self
.window
.as_ref()
.map_or(self.requested_size, |window| window.inner_size());
self.resize(size, scale_factor)
}
WindowEvent::CursorMoved { position, .. } => {
self.cursor = position;
self.handle_event(InputEvent3D::PointerMove {
x: position.x as f32,
y: position.y as f32,
})
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(button3d) = Self::pointer_button(button) else {
return;
};
let x = self.cursor.x as f32;
let y = self.cursor.y as f32;
match state {
ElementState::Pressed => self.handle_event(InputEvent3D::PointerDown {
x,
y,
button: button3d,
}),
ElementState::Released => {
let pointer_up = self.handle_event(InputEvent3D::PointerUp {
x,
y,
button: button3d,
});
if pointer_up.is_err() || button3d != PointerButton3D::Left {
pointer_up
} else {
let now = Instant::now();
let is_double = self.last_left_click.is_some_and(|(when, position)| {
now.duration_since(when) <= DOUBLE_CLICK_INTERVAL
&& (self.cursor.x - position.x)
.hypot(self.cursor.y - position.y)
<= DOUBLE_CLICK_DISTANCE_PX
});
self.last_left_click = if is_double {
None
} else {
Some((now, self.cursor))
};
if is_double {
self.handle_event(InputEvent3D::DoubleClick {
x,
y,
button: PointerButton3D::Left,
})
} else {
Ok(())
}
}
}
}
}
WindowEvent::MouseWheel { delta, .. } => self.handle_event(InputEvent3D::Wheel {
delta_y: Self::wheel_delta(delta),
}),
WindowEvent::KeyboardInput { event, .. }
if event.state == ElementState::Pressed
&& matches!(event.logical_key, Key::Named(NamedKey::Escape)) =>
{
self.handle_event(InputEvent3D::Escape)
}
WindowEvent::RedrawRequested => self.render(),
_ => Ok(()),
};
if let Err(error) = result {
self.fail(event_loop, error);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wheel_normalization_has_one_zoom_direction() {
assert_eq!(
ThreeDWindowApp::wheel_delta(MouseScrollDelta::LineDelta(0.0, 2.0)),
100.0
);
assert_eq!(
ThreeDWindowApp::wheel_delta(MouseScrollDelta::PixelDelta(PhysicalPosition::new(
0.0, -12.0
))),
12.0
);
}
#[test]
fn only_mvp_pointer_buttons_are_mapped() {
assert_eq!(
ThreeDWindowApp::pointer_button(MouseButton::Left),
Some(PointerButton3D::Left)
);
assert_eq!(ThreeDWindowApp::pointer_button(MouseButton::Back), None);
}
}