Skip to content

Commit

Permalink
Merge pull request #1405 from hannobraun/loop
Browse files Browse the repository at this point in the history
Improve error handling in main event loop
  • Loading branch information
hannobraun authored Nov 29, 2022
2 parents 005503c + 3e59f5e commit f457040
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 280 deletions.
292 changes: 292 additions & 0 deletions crates/fj-window/src/event_loop_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
use fj_host::{Host, Model, ModelEvent, Parameters};
use fj_operations::shape_processor::{self, ShapeProcessor};
use fj_viewer::{
GuiState, InputEvent, NormalizedScreenPosition, Screen, ScreenSize,
StatusReport, Viewer,
};
use winit::{
dpi::PhysicalPosition,
event::{
ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta,
VirtualKeyCode, WindowEvent,
},
event_loop::ControlFlow,
};

use crate::window::Window;

pub struct EventLoopHandler {
pub invert_zoom: bool,
pub shape_processor: ShapeProcessor,
pub window: Window,
pub viewer: Viewer,
pub egui_winit_state: egui_winit::State,
pub host: Option<Host>,
pub status: StatusReport,
pub held_mouse_button: Option<MouseButton>,

/// Only handle resize events once every frame. This filters out spurious
/// resize events that can lead to wgpu warnings. See this issue for some
/// context:
/// <https://github.com/rust-windowing/winit/issues/2094>
pub new_size: Option<ScreenSize>,
}

impl EventLoopHandler {
#[allow(clippy::result_large_err)]
pub fn handle_event(
&mut self,
event: Event<()>,
control_flow: &mut ControlFlow,
) -> Result<(), Error> {
if let Some(host) = &self.host {
loop {
let events = host.events();
let event = events
.try_recv()
.map_err(|err| {
if err.is_disconnected() {
panic!("Expected channel to never disconnect");
}
})
.ok();

let event = match event {
Some(status_update) => status_update,
None => break,
};

match event {
ModelEvent::ChangeDetected => {
self.status.update_status(
"Change in model detected. Evaluating model...",
);
}
ModelEvent::Evaluation(evaluation) => {
self.status.update_status(
"Model evaluated. Processing model...",
);

let shape =
self.shape_processor.process(&evaluation.shape)?;
self.viewer.handle_shape_update(shape);

self.status.update_status("Model processed.");
}

ModelEvent::Error(err) => {
return Err(err.into());
}
}
}
}

if let Event::WindowEvent { event, .. } = &event {
// In theory we could/should check if `egui` wants "exclusive" use
// of this event here. But with the current integration with Fornjot
// we're kinda blurring the lines between "app" and "platform", so
// for the moment we pass every event to both `egui` & Fornjot.
//
// The primary visible impact of this currently is that if you drag
// a title bar that overlaps the model then both the model & window
// get moved.
self.egui_winit_state
.on_event(self.viewer.gui.context(), event);
}

// fj-window events
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
Event::WindowEvent {
event:
WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(virtual_key_code),
..
},
..
},
..
} => match virtual_key_code {
VirtualKeyCode::Escape => *control_flow = ControlFlow::Exit,
VirtualKeyCode::Key1 => {
self.viewer.toggle_draw_model();
}
VirtualKeyCode::Key2 => {
self.viewer.toggle_draw_mesh();
}
VirtualKeyCode::Key3 => {
self.viewer.toggle_draw_debug();
}
_ => {}
},
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
self.new_size = Some(ScreenSize {
width: size.width,
height: size.height,
});
}
Event::WindowEvent {
event: WindowEvent::MouseInput { state, button, .. },
..
} => match state {
ElementState::Pressed => {
self.held_mouse_button = Some(button);
self.viewer.add_focus_point();
}
ElementState::Released => {
self.held_mouse_button = None;
self.viewer.remove_focus_point();
}
},
Event::WindowEvent {
event: WindowEvent::MouseWheel { .. },
..
} => self.viewer.add_focus_point(),
Event::MainEventsCleared => {
self.window.window().request_redraw();
}
Event::RedrawRequested(_) => {
// Only do a screen resize once per frame. This protects against
// spurious resize events that cause issues with the renderer.
if let Some(size) = self.new_size.take() {
self.viewer.handle_screen_resize(size);
}

let pixels_per_point =
self.window.window().scale_factor() as f32;

self.egui_winit_state.set_pixels_per_point(pixels_per_point);
let egui_input =
self.egui_winit_state.take_egui_input(self.window.window());

let gui_state = GuiState {
status: &self.status,
model_available: self.host.is_some(),
};
let new_model_path =
self.viewer.draw(pixels_per_point, egui_input, gui_state);

if let Some(model_path) = new_model_path {
let model =
Model::new(model_path, Parameters::empty()).unwrap();
let new_host = Host::from_model(model)?;
self.host = Some(new_host);
}
}
_ => {}
}

let input_event = input_event(
&event,
&self.window,
&self.held_mouse_button,
&mut self.viewer.cursor,
self.invert_zoom,
);
if let Some(input_event) = input_event {
self.viewer.handle_input_event(input_event);
}

Ok(())
}
}

fn input_event<T>(
event: &Event<T>,
window: &Window,
held_mouse_button: &Option<MouseButton>,
previous_cursor: &mut Option<NormalizedScreenPosition>,
invert_zoom: bool,
) -> Option<InputEvent> {
match event {
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
..
} => {
let [width, height] = window.size().as_f64();
let aspect_ratio = width / height;

// Cursor position in normalized coordinates (-1 to +1) with
// aspect ratio taken into account.
let current = NormalizedScreenPosition {
x: position.x / width * 2. - 1.,
y: -(position.y / height * 2. - 1.) / aspect_ratio,
};
let event = match (*previous_cursor, held_mouse_button) {
(Some(previous), Some(button)) => match button {
MouseButton::Left => {
let diff_x = current.x - previous.x;
let diff_y = current.y - previous.y;
let angle_x = -diff_y * ROTATION_SENSITIVITY;
let angle_y = diff_x * ROTATION_SENSITIVITY;

Some(InputEvent::Rotation { angle_x, angle_y })
}
MouseButton::Right => {
Some(InputEvent::Translation { previous, current })
}
_ => None,
},
_ => None,
};
*previous_cursor = Some(current);
event
}
Event::WindowEvent {
event: WindowEvent::MouseWheel { delta, .. },
..
} => {
let delta = match delta {
MouseScrollDelta::LineDelta(_, y) => {
(*y as f64) * ZOOM_FACTOR_LINE
}
MouseScrollDelta::PixelDelta(PhysicalPosition {
y, ..
}) => y * ZOOM_FACTOR_PIXEL,
};

let delta = if invert_zoom { -delta } else { delta };

Some(InputEvent::Zoom(delta))
}
_ => None,
}
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Host error")]
Host(#[from] fj_host::Error),

#[error("Shape processing error")]
ShapeProcessor(#[from] shape_processor::Error),
}

/// Affects the speed of zoom movement given a scroll wheel input in lines.
///
/// Smaller values will move the camera less with the same input.
/// Larger values will move the camera more with the same input.
const ZOOM_FACTOR_LINE: f64 = 0.075;

/// Affects the speed of zoom movement given a scroll wheel input in pixels.
///
/// Smaller values will move the camera less with the same input.
/// Larger values will move the camera more with the same input.
const ZOOM_FACTOR_PIXEL: f64 = 0.005;

/// Affects the speed of rotation given a change in normalized screen position [-1, 1]
///
/// Smaller values will move the camera less with the same input.
/// Larger values will move the camera more with the same input.
const ROTATION_SENSITIVITY: f64 = 5.;
2 changes: 2 additions & 0 deletions crates/fj-window/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@

pub mod run;
pub mod window;

mod event_loop_handler;
Loading

0 comments on commit f457040

Please sign in to comment.