ruffle/desktop/src/app.rs

541 lines
25 KiB
Rust

use crate::custom_event::RuffleEvent;
use crate::gui::{GuiController, MENU_HEIGHT};
use crate::player::{LaunchOptions, PlayerController};
use crate::preferences::GlobalPreferences;
use crate::util::{
get_screen_size, gilrs_button_to_gamepad_button, parse_url, pick_file, plot_stats_in_tracy,
winit_to_ruffle_key_code, winit_to_ruffle_text_control,
};
use anyhow::{Context, Error};
use gilrs::{Event, EventType, Gilrs};
use ruffle_core::{PlayerEvent, StageDisplayState};
use ruffle_render::backend::ViewportDimensions;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use url::Url;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Size};
use winit::event::{ElementState, KeyEvent, Modifiers, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop, EventLoopBuilder};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Fullscreen, Icon, Window, WindowBuilder};
pub struct App {
preferences: GlobalPreferences,
window: Rc<Window>,
event_loop: Option<EventLoop<RuffleEvent>>,
gui: Rc<RefCell<GuiController>>,
player: PlayerController,
min_window_size: LogicalSize<u32>,
max_window_size: PhysicalSize<u32>,
initial_movie_url: Option<Url>,
no_gui: bool,
preferred_width: Option<f64>,
preferred_height: Option<f64>,
start_fullscreen: bool,
}
impl App {
pub fn new(preferences: GlobalPreferences) -> Result<Self, Error> {
let movie_url = preferences.cli.movie_url.clone();
let icon_bytes = include_bytes!("../assets/favicon-32.rgba");
let icon =
Icon::from_rgba(icon_bytes.to_vec(), 32, 32).context("Couldn't load app icon")?;
let event_loop = EventLoopBuilder::with_user_event().build()?;
let no_gui = preferences.cli.no_gui;
let min_window_size = (16, if no_gui { 16 } else { MENU_HEIGHT + 16 }).into();
let max_window_size = get_screen_size(&event_loop);
let preferred_width = preferences.cli.width;
let preferred_height = preferences.cli.height;
let start_fullscreen = preferences.cli.fullscreen;
let window = WindowBuilder::new()
.with_visible(false)
.with_title("Ruffle")
.with_window_icon(Some(icon))
.with_min_inner_size(min_window_size)
.with_max_inner_size(max_window_size)
.build(&event_loop)?;
let window = Rc::new(window);
let mut font_database = fontdb::Database::default();
font_database.load_system_fonts();
let mut gui = GuiController::new(
window.clone(),
&event_loop,
preferences.clone(),
&font_database,
movie_url.clone(),
no_gui,
)?;
let mut player = PlayerController::new(
event_loop.create_proxy(),
window.clone(),
gui.descriptors().clone(),
font_database,
preferences.clone(),
);
if let Some(movie_url) = &movie_url {
gui.create_movie(
&mut player,
LaunchOptions::from(&preferences),
movie_url.clone(),
);
} else {
gui.show_open_dialog();
}
Ok(Self {
preferences,
window,
event_loop: Some(event_loop),
gui: Rc::new(RefCell::new(gui)),
player,
min_window_size,
max_window_size,
initial_movie_url: movie_url,
no_gui,
preferred_width,
preferred_height,
start_fullscreen,
})
}
pub fn run(mut self) -> Result<(), Error> {
enum LoadingState {
Loading,
WaitingForResize,
Loaded,
}
let mut loaded = LoadingState::Loading;
let mut mouse_pos = PhysicalPosition::new(0.0, 0.0);
let mut time = Instant::now();
let mut next_frame_time = None;
let mut minimized = false;
let mut modifiers = Modifiers::default();
let mut fullscreen_down = false;
if self.initial_movie_url.is_none() {
// No SWF provided on command line; show window with dummy movie immediately.
self.window.set_visible(true);
loaded = LoadingState::Loaded;
}
let mut gilrs = Gilrs::new()
.inspect_err(|err| {
tracing::warn!("Gamepad support could not be initialized: {err}");
})
.ok();
// Poll UI events.
let event_loop = self.event_loop.take().expect("App already running");
event_loop.run(move |event, elwt| {
let mut check_redraw = false;
match event {
winit::event::Event::LoopExiting => {
if let Some(mut player) = self.player.get() {
player.flush_shared_objects();
}
crate::shutdown();
return;
}
// Core loop
// [NA] This used to be called `MainEventsCleared`, but I think the behaviour is different now.
// We should look at changing our tick to happen somewhere else if we see any behavioural problems.
winit::event::Event::AboutToWait if matches!(loaded, LoadingState::Loaded) => {
let new_time = Instant::now();
let dt = new_time.duration_since(time).as_micros();
if dt > 0 {
time = new_time;
if let Some(mut player) = self.player.get() {
player.tick(dt as f64 / 1000.0);
next_frame_time = Some(new_time + player.time_til_next_frame());
} else {
next_frame_time = None;
}
check_redraw = true;
}
}
// Render
winit::event::Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
// Don't render when minimized to avoid potential swap chain errors in `wgpu`.
if !minimized {
if let Some(mut player) = self.player.get() {
// Even if the movie is paused, user interaction with debug tools can change the render output
player.render();
self.gui.borrow_mut().render(Some(player));
} else {
self.gui.borrow_mut().render(None);
}
plot_stats_in_tracy(&self.gui.borrow().descriptors().wgpu_instance);
}
}
winit::event::Event::WindowEvent { event, .. } => {
if self.gui.borrow_mut().handle_event(&event) {
// Event consumed by GUI.
return;
}
let height_offset = if self.window.fullscreen().is_some() || self.no_gui {
0.0
} else {
MENU_HEIGHT as f64 * self.window.scale_factor()
};
match event {
WindowEvent::CloseRequested => {
elwt.exit();
return;
}
WindowEvent::Resized(size) => {
// TODO: Change this when winit adds a `Window::minimized` or `WindowEvent::Minimize`.
minimized = size.width == 0 && size.height == 0;
if let Some(mut player) = self.player.get() {
let viewport_scale_factor = self.window.scale_factor();
player.set_viewport_dimensions(ViewportDimensions {
width: size.width,
height: size.height - height_offset as u32,
scale_factor: viewport_scale_factor,
});
}
self.window.request_redraw();
if matches!(loaded, LoadingState::WaitingForResize) {
loaded = LoadingState::Loaded;
}
}
WindowEvent::CursorMoved { position, .. } => {
if self.gui.borrow_mut().is_context_menu_visible() {
return;
}
mouse_pos = position;
let event = PlayerEvent::MouseMove {
x: position.x,
y: position.y - height_offset,
};
self.player.handle_event(event);
check_redraw = true;
}
WindowEvent::DroppedFile(file) => {
if let Ok(url) = parse_url(&file) {
self.gui.borrow_mut().create_movie(
&mut self.player,
LaunchOptions::from(&self.preferences),
url,
);
}
}
WindowEvent::MouseInput { button, state, .. } => {
if self.gui.borrow_mut().is_context_menu_visible() {
return;
}
use ruffle_core::events::MouseButton as RuffleMouseButton;
use winit::event::MouseButton;
let x = mouse_pos.x;
let y = mouse_pos.y - height_offset;
let button = match button {
MouseButton::Left => RuffleMouseButton::Left,
MouseButton::Right => RuffleMouseButton::Right,
MouseButton::Middle => RuffleMouseButton::Middle,
_ => RuffleMouseButton::Unknown,
};
let event = match state {
ElementState::Pressed => PlayerEvent::MouseDown { x, y, button },
ElementState::Released => PlayerEvent::MouseUp { x, y, button },
};
if state == ElementState::Released && button == RuffleMouseButton::Right
{
// Show context menu.
// TODO: Should be squelched if player consumes the right click event.
if let Some(mut player) = self.player.get() {
let context_menu = player.prepare_context_menu();
self.gui.borrow_mut().show_context_menu(context_menu);
}
}
self.player.handle_event(event);
check_redraw = true;
}
WindowEvent::MouseWheel { delta, .. } => {
use ruffle_core::events::MouseWheelDelta;
use winit::event::MouseScrollDelta;
let delta = match delta {
MouseScrollDelta::LineDelta(_, dy) => {
MouseWheelDelta::Lines(dy.into())
}
MouseScrollDelta::PixelDelta(pos) => MouseWheelDelta::Pixels(pos.y),
};
let event = PlayerEvent::MouseWheel { delta };
self.player.handle_event(event);
check_redraw = true;
}
WindowEvent::CursorEntered { .. } => {
if let Some(mut player) = self.player.get() {
player.set_mouse_in_stage(true);
if player.needs_render() {
self.window.request_redraw();
}
}
}
WindowEvent::CursorLeft { .. } => {
if let Some(mut player) = self.player.get() {
player.set_mouse_in_stage(false);
}
self.player.handle_event(PlayerEvent::MouseLeave);
check_redraw = true;
}
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers;
}
WindowEvent::KeyboardInput { event, .. } => {
// Handle fullscreen keyboard shortcuts: Alt+Return, Escape.
match event {
KeyEvent {
state: ElementState::Pressed,
logical_key: Key::Named(NamedKey::Enter),
..
} if modifiers.state().alt_key() => {
if !fullscreen_down {
if let Some(mut player) = self.player.get() {
player.update(|uc| {
uc.stage.toggle_display_state(uc);
});
}
}
fullscreen_down = true;
return;
}
KeyEvent {
state: ElementState::Released,
logical_key: Key::Named(NamedKey::Enter),
..
} if fullscreen_down => {
fullscreen_down = false;
}
KeyEvent {
state: ElementState::Pressed,
logical_key: Key::Named(NamedKey::Escape),
..
} => {
if let Some(mut player) = self.player.get() {
if player.is_playing() {
player.update(|uc| {
uc.stage.set_display_state(
uc,
StageDisplayState::Normal,
);
})
}
}
}
_ => (),
}
let key_code = winit_to_ruffle_key_code(&event);
// [NA] TODO: This event used to give a single char. `last()` is functionally the same,
// but we may want to be better at this in the future.
let key_char = event.text.clone().and_then(|text| text.chars().last());
match &event.state {
ElementState::Pressed => {
self.player
.handle_event(PlayerEvent::KeyDown { key_code, key_char });
if let Some(control_code) =
winit_to_ruffle_text_control(&event, &modifiers)
{
self.player.handle_event(PlayerEvent::TextControl {
code: control_code,
});
} else if let Some(text) = event.text {
for codepoint in text.chars() {
self.player
.handle_event(PlayerEvent::TextInput { codepoint });
}
}
}
ElementState::Released => {
self.player
.handle_event(PlayerEvent::KeyUp { key_code, key_char });
}
};
check_redraw = true;
}
_ => (),
}
}
winit::event::Event::UserEvent(RuffleEvent::TaskPoll) => self.player.poll(),
winit::event::Event::UserEvent(RuffleEvent::OnMetadata(swf_header)) => {
let movie_width = swf_header.stage_size().width().to_pixels();
let movie_height = swf_header.stage_size().height().to_pixels();
let height_offset = if self.window.fullscreen().is_some() || self.no_gui {
0.0
} else {
MENU_HEIGHT as f64
};
let window_size: Size = match (self.preferred_width, self.preferred_height) {
(None, None) => {
LogicalSize::new(movie_width, movie_height + height_offset).into()
}
(Some(width), None) => {
let scale = width / movie_width;
let height = movie_height * scale;
PhysicalSize::new(
width.max(1.0),
height.max(1.0) + height_offset * self.window.scale_factor(),
)
.into()
}
(None, Some(height)) => {
let scale = height / movie_height;
let width = movie_width * scale;
PhysicalSize::new(
width.max(1.0),
height.max(1.0) + height_offset * self.window.scale_factor(),
)
.into()
}
(Some(width), Some(height)) => PhysicalSize::new(
width.max(1.0),
height.max(1.0) + height_offset * self.window.scale_factor(),
)
.into(),
};
let window_size = Size::clamp(
window_size,
self.min_window_size.into(),
self.max_window_size.into(),
self.window.scale_factor(),
);
let viewport_size = self.window.inner_size();
let mut window_resize_denied = false;
if let Some(new_viewport_size) = self.window.request_inner_size(window_size) {
if new_viewport_size != viewport_size {
self.gui.borrow_mut().resize(new_viewport_size);
} else {
tracing::warn!("Unable to resize window");
window_resize_denied = true;
}
}
self.window.set_fullscreen(if self.start_fullscreen {
Some(Fullscreen::Borderless(None))
} else {
None
});
self.window.set_visible(true);
let viewport_size = self.window.inner_size();
// On X11 (and possibly other platforms), the window size is not updated immediately.
// On a successful resize request, wait for the window to be resized to the requested size
// before we start running the SWF (which can observe the viewport size in "noScale" mode)
if !window_resize_denied && window_size != viewport_size.into() {
loaded = LoadingState::WaitingForResize;
} else {
loaded = LoadingState::Loaded;
}
let viewport_scale_factor = self.window.scale_factor();
if let Some(mut player) = self.player.get() {
player.set_viewport_dimensions(ViewportDimensions {
width: viewport_size.width,
height: viewport_size.height - height_offset as u32,
scale_factor: viewport_scale_factor,
});
}
}
winit::event::Event::UserEvent(RuffleEvent::ContextMenuItemClicked(index)) => {
if let Some(mut player) = self.player.get() {
player.run_context_menu_callback(index);
}
}
winit::event::Event::UserEvent(RuffleEvent::BrowseAndOpen(options)) => {
if let Some(url) =
pick_file(false, None).and_then(|p| Url::from_file_path(p).ok())
{
self.gui
.borrow_mut()
.create_movie(&mut self.player, *options, url);
}
}
winit::event::Event::UserEvent(RuffleEvent::OpenURL(url, options)) => {
self.gui
.borrow_mut()
.create_movie(&mut self.player, *options, url);
}
winit::event::Event::UserEvent(RuffleEvent::CloseFile) => {
self.window.set_title("Ruffle"); // Reset title since file has been closed.
self.player.destroy();
}
winit::event::Event::UserEvent(RuffleEvent::ExitRequested) => {
elwt.exit();
return;
}
_ => (),
}
if let Some(Event { event, .. }) = gilrs.as_mut().and_then(|gilrs| gilrs.next_event()) {
match event {
EventType::ButtonPressed(button, _) => {
if let Some(button) = gilrs_button_to_gamepad_button(button) {
self.player
.handle_event(PlayerEvent::GamepadButtonDown { button });
check_redraw = true;
}
}
EventType::ButtonReleased(button, _) => {
if let Some(button) = gilrs_button_to_gamepad_button(button) {
self.player
.handle_event(PlayerEvent::GamepadButtonUp { button });
check_redraw = true;
}
}
_ => {}
}
}
// Check for a redraw request.
if check_redraw {
let player = self.player.get();
let gui = self.gui.borrow_mut();
if player.map(|p| p.needs_render()).unwrap_or_default() || gui.needs_render() {
self.window.request_redraw();
}
}
// After polling events, sleep the event loop until the next event or the next frame.
elwt.set_control_flow(if matches!(loaded, LoadingState::Loaded) {
if let Some(next_frame_time) = next_frame_time {
ControlFlow::WaitUntil(next_frame_time)
} else {
// prevent 100% cpu use
// TODO: use set_request_repaint_callback to correctly get egui repaint requests.
ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10))
}
} else {
ControlFlow::Wait
});
})?;
Ok(())
}
}