Allow external event loop to drive masonry and xilem (#417)

These changes allow you to create a masonry or xilem app that is driven
by an external event loop.

## Masonry

Existing method for creating masonry app:
```
    masonry::event_loop_runner::run(
        masonry::event_loop_runner::EventLoop::with_user_event(),
        window_attributes,
        root_widget,
        app_driver,
    )
    .unwrap();
```

Instead you can now do this:

```
    let masonry_state = MasonryState::new(window_attributes, &event_loop, root_widget);

    let mut app = AppInterface {
        masonry_state,
        app_driver: Box::new(driver),
    };
    event_loop.run_app(&mut app)
```

Where AppInterface implements the winit
ApplicationHandler<accesskit_winit::Event> trait.

## Xilem

Existing method:
```
    let app = Xilem::new(state, app_logic);
    app.run_windowed(EventLoop::with_user_event(), title)?;
```

Now:
```
    let xilem = Xilem::new(0, app_logic);
    let (root_widget, app_driver) = xilem.split();
    let parts = xilem.split();
    let (root_widget, app_driver) = (parts.root_widget, parts.driver)

   // and then create masonry app just like above using root_widget and app_driver
```

Also adds example/external_event_loop.rs which duplicates
example/flex.rs but with an external event loop.

---------

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
This commit is contained in:
cfagot 2024-07-05 01:28:11 -07:00 committed by GitHub
parent e76cf31258
commit cf3530097b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 362 additions and 83 deletions

View File

@ -12,7 +12,10 @@ use vello::{peniko::Color, AaSupport, RenderParams, Renderer, RendererOptions, S
use wgpu::PresentMode;
use winit::application::ApplicationHandler;
use winit::error::EventLoopError;
use winit::event::{MouseButton as WinitMouseButton, WindowEvent as WinitWindowEvent};
use winit::event::{
DeviceEvent as WinitDeviceEvent, DeviceId, MouseButton as WinitMouseButton,
WindowEvent as WinitWindowEvent,
};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::window::{Window, WindowAttributes, WindowId};
@ -51,11 +54,13 @@ pub enum WindowState<'a> {
},
}
struct MainState<'a> {
/// The state of the Masonry application. If you run Masonry from an external Winit event loop, create a
/// `MasonryState` via [`MasonryState::new`] and forward events to it via the appropriate method (e.g.,
/// calling [`handle_window_event`](MasonryState::handle_window_event) in [`window_event`](ApplicationHandler::window_event)).
pub struct MasonryState<'a> {
render_cx: RenderContext,
render_root: RenderRoot,
pointer_state: PointerState,
app_driver: Box<dyn AppDriver>,
renderer: Option<Renderer>,
// TODO: Winit doesn't seem to let us create these proxies from within the loop
// The reasons for this are unclear
@ -66,6 +71,11 @@ struct MainState<'a> {
window: WindowState<'a>,
}
struct MainState<'a> {
masonry_state: MasonryState<'a>,
app_driver: Box<dyn AppDriver>,
}
/// The type of the event loop used by Masonry.
///
/// This *will* be changed to allow custom event types, but is implemented this way for expedience
@ -96,18 +106,9 @@ pub fn run_with(
root_widget: impl Widget,
app_driver: impl AppDriver + 'static,
) -> Result<(), EventLoopError> {
let render_cx = RenderContext::new();
// TODO: We can't know this scale factor until later?
let scale_factor = 1.0;
let mut main_state = MainState {
render_cx,
render_root: RenderRoot::new(root_widget, WindowSizePolicy::User, scale_factor),
renderer: None,
pointer_state: PointerState::empty(),
masonry_state: MasonryState::new(window, &event_loop, root_widget),
app_driver: Box::new(app_driver),
proxy: event_loop.create_proxy(),
window: WindowState::Uninitialized(window),
};
// If there is no default tracing subscriber, we set our own. If one has
@ -121,6 +122,90 @@ pub fn run_with(
impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.masonry_state.handle_resumed(event_loop);
}
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
self.masonry_state.handle_suspended(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WinitWindowEvent,
) {
self.masonry_state.handle_window_event(
event_loop,
window_id,
event,
self.app_driver.as_mut(),
);
}
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: DeviceId,
event: WinitDeviceEvent,
) {
self.masonry_state.handle_device_event(
event_loop,
device_id,
event,
self.app_driver.as_mut(),
);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: accesskit_winit::Event) {
self.masonry_state
.handle_user_event(event_loop, event, self.app_driver.as_mut());
}
// The following have empty handlers, but adding this here for future proofing. E.g., memory
// warning is very likely to be handled for mobile and we in particular want to make sure
// external event loops can let masonry handle these callbacks.
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_about_to_wait(event_loop);
}
fn new_events(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
cause: winit::event::StartCause,
) {
self.masonry_state.handle_new_events(event_loop, cause);
}
fn exiting(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_exiting(event_loop);
}
fn memory_warning(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_memory_warning(event_loop);
}
}
impl MasonryState<'_> {
pub fn new(window: WindowAttributes, event_loop: &EventLoop, root_widget: impl Widget) -> Self {
let render_cx = RenderContext::new();
// TODO: We can't know this scale factor until later?
let scale_factor = 1.0;
MasonryState {
render_cx,
render_root: RenderRoot::new(root_widget, WindowSizePolicy::User, scale_factor),
renderer: None,
pointer_state: PointerState::empty(),
proxy: event_loop.create_proxy(),
window: WindowState::Uninitialized(window),
}
}
// --- MARK: RESUMED ---
pub fn handle_resumed(&mut self, event_loop: &ActiveEventLoop) {
match std::mem::replace(
&mut self.window,
// TODO: Is there a better default value which could be used?
@ -183,7 +268,9 @@ impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
}
}
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
// --- MARK: SUSPENDED ---
pub fn handle_suspended(&mut self, _event_loop: &ActiveEventLoop) {
match std::mem::replace(
&mut self.window,
// TODO: Is there a better default value which could be used?
@ -206,8 +293,76 @@ impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
}
}
// --- MARK: RENDER ---
fn render(&mut self, scene: Scene) {
let WindowState::Rendering {
window, surface, ..
} = &mut self.window
else {
tracing::warn!("Tried to render whilst suspended or before window created");
return;
};
let scale_factor = window.scale_factor();
// https://github.com/rust-windowing/winit/issues/2308
#[cfg(target_os = "ios")]
let size = window.outer_size();
#[cfg(not(target_os = "ios"))]
let size = window.inner_size();
let width = size.width;
let height = size.height;
if surface.config.width != width || surface.config.height != height {
self.render_cx.resize_surface(surface, width, height);
}
let transformed_scene = if scale_factor == 1.0 {
None
} else {
let mut new_scene = Scene::new();
new_scene.append(&scene, Some(Affine::scale(scale_factor)));
Some(new_scene)
};
let scene_ref = transformed_scene.as_ref().unwrap_or(&scene);
let Ok(surface_texture) = surface.surface.get_current_texture() else {
warn!("failed to acquire next swapchain texture");
return;
};
let dev_id = surface.dev_id;
let device = &self.render_cx.devices[dev_id].device;
let queue = &self.render_cx.devices[dev_id].queue;
let renderer_options = RendererOptions {
surface_format: Some(surface.format),
use_cpu: false,
antialiasing_support: AaSupport {
area: true,
msaa8: false,
msaa16: false,
},
num_init_threads: NonZeroUsize::new(1),
};
let render_params = RenderParams {
base_color: Color::BLACK,
width,
height,
antialiasing_method: vello::AaConfig::Area,
};
self.renderer
.get_or_insert_with(|| Renderer::new(device, renderer_options).unwrap())
.render_to_surface(device, queue, scene_ref, &surface_texture, &render_params)
.expect("failed to render to surface");
surface_texture.present();
device.poll(wgpu::Maintain::Wait);
}
// --- MARK: WINDOW_EVENT ---
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WinitWindowEvent) {
pub fn handle_window_event(
&mut self,
event_loop: &ActiveEventLoop,
_: WindowId,
event: WinitWindowEvent,
app_driver: &mut dyn AppDriver,
) {
let WindowState::Rendering {
window,
accesskit_adapter,
@ -354,11 +509,26 @@ impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
_ => (),
}
self.handle_signals(event_loop);
self.handle_signals(event_loop, app_driver);
}
// --- MARK: DEVICE_EVENT ---
pub fn handle_device_event(
&mut self,
_: &ActiveEventLoop,
_: DeviceId,
_: WinitDeviceEvent,
_: &mut dyn AppDriver,
) {
}
// --- MARK: USER_EVENT ---
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: accesskit_winit::Event) {
pub fn handle_user_event(
&mut self,
event_loop: &ActiveEventLoop,
event: accesskit_winit::Event,
app_driver: &mut dyn AppDriver,
) {
match event.window_event {
// Note that this event can be called at any time, even multiple times if
// the user restarts their screen reader.
@ -372,75 +542,20 @@ impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
}
self.handle_signals(event_loop);
self.handle_signals(event_loop, app_driver);
}
}
impl MainState<'_> {
// --- MARK: RENDER ---
fn render(&mut self, scene: Scene) {
let WindowState::Rendering {
window, surface, ..
} = &mut self.window
else {
tracing::warn!("Tried to render whilst suspended or before window created");
return;
};
let scale_factor = window.scale_factor();
// https://github.com/rust-windowing/winit/issues/2308
#[cfg(target_os = "ios")]
let size = window.outer_size();
#[cfg(not(target_os = "ios"))]
let size = window.inner_size();
let width = size.width;
let height = size.height;
// --- MARK: EMPTY WINIT HANDLERS ---
pub fn handle_about_to_wait(&mut self, _: &ActiveEventLoop) {}
if surface.config.width != width || surface.config.height != height {
self.render_cx.resize_surface(surface, width, height);
}
pub fn handle_new_events(&mut self, _: &ActiveEventLoop, _: winit::event::StartCause) {}
let transformed_scene = if scale_factor == 1.0 {
None
} else {
let mut new_scene = Scene::new();
new_scene.append(&scene, Some(Affine::scale(scale_factor)));
Some(new_scene)
};
let scene_ref = transformed_scene.as_ref().unwrap_or(&scene);
pub fn handle_exiting(&mut self, _: &ActiveEventLoop) {}
let Ok(surface_texture) = surface.surface.get_current_texture() else {
warn!("failed to acquire next swapchain texture");
return;
};
let dev_id = surface.dev_id;
let device = &self.render_cx.devices[dev_id].device;
let queue = &self.render_cx.devices[dev_id].queue;
let renderer_options = RendererOptions {
surface_format: Some(surface.format),
use_cpu: false,
antialiasing_support: AaSupport {
area: true,
msaa8: false,
msaa16: false,
},
num_init_threads: NonZeroUsize::new(1),
};
let render_params = RenderParams {
base_color: Color::BLACK,
width,
height,
antialiasing_method: vello::AaConfig::Area,
};
self.renderer
.get_or_insert_with(|| Renderer::new(device, renderer_options).unwrap())
.render_to_surface(device, queue, scene_ref, &surface_texture, &render_params)
.expect("failed to render to surface");
surface_texture.present();
device.poll(wgpu::Maintain::Wait);
}
pub fn handle_memory_warning(&mut self, _: &ActiveEventLoop) {}
// --- MARK: SIGNALS ---
fn handle_signals(&mut self, _event_loop: &ActiveEventLoop) {
fn handle_signals(&mut self, _event_loop: &ActiveEventLoop, app_driver: &mut dyn AppDriver) {
let WindowState::Rendering { window, .. } = &mut self.window else {
tracing::warn!("Tried to handle a signal whilst suspended or before window created");
return;
@ -453,8 +568,7 @@ impl MainState<'_> {
let mut driver_ctx = DriverCtx {
main_root_widget: root,
};
self.app_driver
.on_action(&mut driver_ctx, widget_id, action);
app_driver.on_action(&mut driver_ctx, widget_id, action);
});
}
render_root::RenderRootSignal::StartIme => {
@ -492,4 +606,18 @@ impl MainState<'_> {
}
}
}
pub fn get_window_state(&self) -> &WindowState {
&self.window
}
pub fn get_root(&mut self) -> &mut RenderRoot {
&mut self.render_root
}
pub fn set_present_mode(&mut self, present_mode: wgpu::PresentMode) {
if let WindowState::Rendering { surface, .. } = &mut self.window {
self.render_cx.set_present_mode(surface, present_mode);
}
}
}

View File

@ -0,0 +1,151 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! Shows driving a Xilem application from a pre-existing Winit event loop.
//! Currently, this supports running as its own window alongside an existing application, or
//! accessing raw events from winit.
//! Support for more custom embeddings would be welcome, but needs more design work
use masonry::{
app_driver::AppDriver,
widget::{CrossAxisAlignment, MainAxisAlignment},
ArcStr,
};
use winit::{
application::ApplicationHandler,
error::EventLoopError,
event::ElementState,
keyboard::{KeyCode, PhysicalKey},
};
use xilem::{
view::{button, flex, label, sized_box},
EventLoop, WidgetView, Xilem,
};
/// A component to make a bigger than usual button
fn big_button(
label: impl Into<ArcStr>,
callback: impl Fn(&mut i32) + Send + Sync + 'static,
) -> impl WidgetView<i32> {
sized_box(button(label, callback)).width(40.).height(40.)
}
fn app_logic(data: &mut i32) -> impl WidgetView<i32> {
flex((
big_button("-", |data| {
*data -= 1;
}),
label(format!("count: {}", data)).text_size(32.),
big_button("+", |data| {
*data += 1;
}),
))
.direction(xilem::Axis::Horizontal)
.cross_axis_alignment(CrossAxisAlignment::Center)
.main_axis_alignment(MainAxisAlignment::Center)
}
/// An application not managed by Xilem, but which wishes to embed Xilem.
struct ExternalApp {
masonry_state: masonry::event_loop_runner::MasonryState<'static>,
app_driver: Box<dyn AppDriver>,
}
impl ApplicationHandler<accesskit_winit::Event> for ExternalApp {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_resumed(event_loop);
}
fn suspended(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_suspended(event_loop);
}
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_about_to_wait(event_loop);
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
self.masonry_state.handle_window_event(
event_loop,
window_id,
event,
self.app_driver.as_mut(),
);
}
fn user_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
event: accesskit_winit::Event,
) {
self.masonry_state
.handle_user_event(event_loop, event, self.app_driver.as_mut());
}
fn device_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
// Handle the escape key to exit the app outside of masonry/xilem
if let winit::event::DeviceEvent::Key(key) = &event {
if key.state == ElementState::Pressed
&& key.physical_key == PhysicalKey::Code(KeyCode::Escape)
{
event_loop.exit();
return;
}
}
self.masonry_state.handle_device_event(
event_loop,
device_id,
event,
self.app_driver.as_mut(),
);
}
fn new_events(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
cause: winit::event::StartCause,
) {
self.masonry_state.handle_new_events(event_loop, cause);
}
fn exiting(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_exiting(event_loop);
}
fn memory_warning(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.masonry_state.handle_memory_warning(event_loop);
}
}
fn main() -> Result<(), EventLoopError> {
let window_size = winit::dpi::LogicalSize::new(800.0, 800.0);
let window_attributes = winit::window::Window::default_attributes()
.with_title("External event loop".to_string())
.with_resizable(true)
.with_min_inner_size(window_size);
let xilem = Xilem::new(0, app_logic);
let event_loop = EventLoop::with_user_event().build().unwrap();
let masonry_state = masonry::event_loop_runner::MasonryState::new(
window_attributes,
&event_loop,
xilem.root_widget,
);
let mut app = ExternalApp {
masonry_state,
app_driver: Box::new(xilem.driver),
};
event_loop.run_app(&mut app)
}

View File

@ -34,8 +34,8 @@ pub struct Xilem<State, Logic, View>
where
View: WidgetView<State>,
{
root_widget: RootWidget<View::Widget>,
driver: MasonryDriver<State, Logic, View, View::ViewState>,
pub root_widget: RootWidget<View::Widget>,
pub driver: MasonryDriver<State, Logic, View, View::ViewState>,
}
impl<State, Logic, View> Xilem<State, Logic, View>