use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
use smudge_core::app::{App, AppConfig, FrameOutput, FrameStats, RepaintMode};
use smudge_core::clipboard::{Clipboard, MemoryClipboard};
use smudge_core::element::IntoElement;
use smudge_core::geometry::{Color, PixelRect, Rect, Size};
use smudge_cpu::CpuRenderer;
use softbuffer::{Context, Surface};
use web_time::Instant;
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle,
};
use winit::window::{Window, WindowId};
#[cfg(not(target_arch = "wasm32"))]
use crate::clipboard::SystemClipboard;
use crate::error::PlatformError;
use crate::events::EventTranslator;
use crate::present::{
DamageHistory, PresentStats, PresentStatsHandle, copy_rect, damage_region, present_skipped,
tint_rects,
};
use crate::renderer::RendererChoice;
#[cfg(feature = "gpu")]
use smudge_gpu::AdapterSummary;
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
pub(crate) enum PlatformEvent {
A11y(accesskit_winit::Event),
}
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
impl From<accesskit_winit::Event> for PlatformEvent {
fn from(event: accesskit_winit::Event) -> PlatformEvent {
PlatformEvent::A11y(event)
}
}
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
type UserEvent = PlatformEvent;
#[cfg(any(not(feature = "a11y"), target_arch = "wasm32"))]
type UserEvent = ();
pub type FrameHook = Rc<dyn Fn(FrameStats, PresentStats)>;
pub type EventHook = Rc<dyn Fn(&mut App)>;
#[derive(Clone)]
pub struct WindowOptions {
pub title: String,
pub size: Size,
pub clear_color: Color,
pub repaint: RepaintMode,
pub show_damage: bool,
pub renderer: RendererChoice,
pub stats: Option<PresentStatsHandle>,
pub on_frame: Option<FrameHook>,
pub on_event: Option<EventHook>,
pub frame_limit: Option<u64>,
}
impl std::fmt::Debug for WindowOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowOptions")
.field("title", &self.title)
.field("size", &self.size)
.field("clear_color", &self.clear_color)
.field("repaint", &self.repaint)
.field("show_damage", &self.show_damage)
.field("renderer", &self.renderer)
.field("stats", &self.stats)
.field("on_frame", &self.on_frame.is_some())
.field("on_event", &self.on_event.is_some())
.field("frame_limit", &self.frame_limit)
.finish()
}
}
pub fn run<E: IntoElement + 'static>(
options: WindowOptions,
build: impl FnOnce() -> E + 'static,
) -> Result<(), PlatformError> {
run_with(options, build, |_| {})
}
pub fn run_with<E: IntoElement + 'static>(
options: WindowOptions,
build: impl FnOnce() -> E + 'static,
configure: impl FnOnce(&mut App) + 'static,
) -> Result<(), PlatformError> {
if !options.clear_color.is_opaque() {
return Err(PlatformError::ClearColor);
}
let renderer = options.renderer.with_env_override()?;
let event_loop = EventLoop::<UserEvent>::with_user_event()
.build()
.map_err(PlatformError::EventLoop)?;
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
let proxy = event_loop.create_proxy();
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut platform = Platform {
renderer,
options,
build: Some(build),
configure: Some(Box::new(configure)),
state: None,
error: None,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
proxy,
};
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::EventLoopExtWebSys;
event_loop.spawn_app(platform);
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
{
event_loop
.run_app(&mut platform)
.map_err(PlatformError::EventLoop)?;
match platform.error.take() {
Some(error) => Err(error),
None => Ok(()),
}
}
}
struct Platform<E, B>
where
E: IntoElement,
B: FnOnce() -> E,
{
renderer: RendererChoice,
options: WindowOptions,
build: Option<B>,
configure: Option<Configure>,
state: Option<WindowState>,
error: Option<PlatformError>,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
proxy: winit::event_loop::EventLoopProxy<UserEvent>,
}
type Configure = Box<dyn FnOnce(&mut App)>;
struct WindowState {
window: Arc<Window>,
app: App,
translator: EventTranslator,
presenter: Presenter,
stats: PresentStats,
stats_handle: Option<PresentStatsHandle>,
on_frame: Option<FrameHook>,
on_event: Option<EventHook>,
rendered: bool,
frame_limit: Option<u64>,
frames_presented: u64,
scale: f32,
physical: (u32, u32),
ime_area: Option<Rect>,
ime_scale: f32,
ime_allowed: bool,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
adapter: accesskit_winit::Adapter,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y: smudge_a11y::Projection,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y_active: bool,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y_full: bool,
}
#[derive(Default)]
struct PendingFrame {
damage: Vec<PixelRect>,
full: bool,
}
impl<E, B> Platform<E, B>
where
E: IntoElement + 'static,
B: FnOnce() -> E + 'static,
{
fn create(&mut self, event_loop: &ActiveEventLoop) -> Result<WindowState, PlatformError> {
let attributes = Window::default_attributes()
.with_title(self.options.title.clone())
.with_inner_size(LogicalSize::new(
f64::from(self.options.size.width),
f64::from(self.options.size.height),
))
.with_visible(false);
#[cfg(target_arch = "wasm32")]
let attributes = {
use winit::platform::web::WindowAttributesExtWebSys;
attributes.with_append(true)
};
let window = Arc::new(
event_loop
.create_window(attributes)
.map_err(PlatformError::Window)?,
);
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
let adapter = accesskit_winit::Adapter::with_event_loop_proxy(
event_loop,
&window,
self.proxy.clone(),
);
window.set_visible(true);
let scale = window.scale_factor() as f32;
let physical = window.inner_size();
let build = self
.build
.take()
.expect("the window is created only once, so its element is still waiting");
let mut app = App::new(
AppConfig {
viewport: logical_size(physical, scale),
scale,
clear_color: self.options.clear_color,
repaint: self.options.repaint,
},
build,
);
if let Some(configure) = self.configure.take() {
configure(&mut app);
}
let (width, height) = app.surface_size();
let presenter = create_presenter(
&window,
self.renderer,
width,
height,
self.options.show_damage,
)?;
#[cfg(feature = "gpu")]
if let (Some(handle), Some(summary)) =
(self.options.stats.as_ref(), presenter.adapter_summary())
{
handle.set_adapter(summary);
}
let weak = Arc::downgrade(&window);
app.set_wake_callback(move || {
if let Some(window) = weak.upgrade() {
window.request_redraw();
}
});
window.request_redraw();
#[cfg(not(target_arch = "wasm32"))]
let clipboard: Box<dyn Clipboard> = match SystemClipboard::new() {
Ok(clipboard) => Box::new(clipboard),
Err(_) => Box::new(MemoryClipboard::default()),
};
#[cfg(target_arch = "wasm32")]
let clipboard: Box<dyn Clipboard> = Box::new(MemoryClipboard::default());
app.set_clipboard(clipboard);
Ok(WindowState {
window,
app,
translator: EventTranslator::new(),
presenter,
stats: PresentStats::default(),
stats_handle: self.options.stats.clone(),
on_frame: self.options.on_frame.take(),
on_event: self.options.on_event.take(),
rendered: false,
frame_limit: self.options.frame_limit,
frames_presented: 0,
scale,
physical: (physical.width, physical.height),
ime_area: None,
ime_scale: scale,
ime_allowed: false,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
adapter,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y: smudge_a11y::Projection::new(self.options.title.clone()),
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y_active: false,
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
a11y_full: false,
})
}
fn resize(&mut self, physical: PhysicalSize<u32>, scale: f64) -> Result<(), PlatformError> {
let Some(state) = self.state.as_mut() else {
return Ok(());
};
let scale = scale as f32;
if !scale.is_finite() || scale <= 0.0 {
return Ok(());
}
if physical.width == 0 || physical.height == 0 {
return Ok(());
}
if state.scale == scale && state.physical == (physical.width, physical.height) {
return Ok(());
}
let (Some(width), Some(height)) = (
NonZeroU32::new(physical.width),
NonZeroU32::new(physical.height),
) else {
return Ok(());
};
state.presenter.resize(width, height)?;
state.app.set_viewport(logical_size(physical, scale), scale);
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
if state.scale != scale {
state.a11y_full = true;
}
debug_assert_eq!(
state.app.surface_size(),
(width.get(), height.get()),
"the app's surface size must match the resized surface and renderer"
);
state.scale = scale;
state.physical = (physical.width, physical.height);
state.window.request_redraw();
Ok(())
}
fn redraw(&mut self) -> Result<(), PlatformError> {
let Some(state) = self.state.as_mut() else {
return Ok(());
};
if state.rendered
&& let Some(on_event) = &state.on_event
{
(on_event)(&mut state.app);
}
let driven = state.app.has_tick() || state.on_event.is_some();
let output = state.app.frame();
state.rendered = true;
let presented = state.presenter.render_and_present(&output, driven)?;
if let Some(stats) = presented {
state.stats = stats;
if let Some(handle) = &state.stats_handle {
handle.store(stats);
}
state.frames_presented += 1;
if let Some(on_frame) = &state.on_frame {
on_frame(state.app.stats().clone(), stats);
}
}
state.update_ime();
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
state.update_a11y();
if driven || (presented.is_some() && state.below_frame_limit()) {
state.window.request_redraw();
}
Ok(())
}
fn wait_for_timers(&mut self, event_loop: &ActiveEventLoop) {
let Some(state) = self.state.as_mut() else {
return;
};
if state.app.has_tick() || state.on_event.is_some() {
return;
}
if wait_decision(state.app.next_deadline(), Instant::now()) == WaitDecision::Now {
state.app.run_due_timers();
if state.app.needs_frame() {
state.window.request_redraw();
}
}
match wait_decision(state.app.next_deadline(), Instant::now()) {
WaitDecision::Now => state.window.request_redraw(),
WaitDecision::Until(deadline) => {
event_loop.set_control_flow(ControlFlow::WaitUntil(deadline));
}
WaitDecision::Wait => event_loop.set_control_flow(ControlFlow::Wait),
}
}
fn fail(&mut self, event_loop: &ActiveEventLoop, error: PlatformError) {
if self.error.is_none() {
self.error = Some(error);
}
event_loop.exit();
}
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
fn handle_a11y_event(&mut self, event: accesskit_winit::Event) {
let Some(state) = self.state.as_mut() else {
return;
};
match event.window_event {
accesskit_winit::WindowEvent::InitialTreeRequested => {
state.a11y_active = true;
state.a11y_full = true;
if state.app.needs_frame() {
state.window.request_redraw();
} else {
state.update_a11y();
}
}
accesskit_winit::WindowEvent::ActionRequested(request) => {
let id = smudge_core::tree::WidgetId::from_u64(request.target_node.0);
match request.action {
accesskit::Action::Click => {
state.app.press(id);
}
accesskit::Action::Focus => {
state.app.request_focus(id);
}
_ => {}
}
if state.app.needs_frame() {
state.window.request_redraw();
}
}
accesskit_winit::WindowEvent::AccessibilityDeactivated => {
state.a11y_active = false;
}
}
}
}
impl<E, B> ApplicationHandler<UserEvent> for Platform<E, B>
where
E: IntoElement + 'static,
B: FnOnce() -> E + 'static,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
match self.create(event_loop) {
Ok(state) => self.state = Some(state),
Err(error) => self.fail(event_loop, error),
}
}
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
let UserEvent::A11y(event) = event;
self.handle_a11y_event(event);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
if let Some(state) = self.state.as_mut() {
state.adapter.process_event(&state.window, &event);
}
let result = match event {
WindowEvent::CloseRequested => {
event_loop.exit();
Ok(())
}
WindowEvent::Resized(physical) => {
let scale = self
.state
.as_ref()
.map_or(1.0, |state| state.window.scale_factor());
self.resize(physical, scale)
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
match self.state.as_ref().map(|state| state.window.inner_size()) {
Some(physical) => self.resize(physical, scale_factor),
None => Ok(()),
}
}
WindowEvent::RedrawRequested => {
let result = self.redraw();
if result.is_ok() && self.state.as_ref().is_some_and(WindowState::finished) {
event_loop.exit();
}
result
}
other => {
if let Some(state) = self.state.as_mut() {
let input = state.translator.translate(&other, state.scale);
if let Some(input) = input {
state.app.handle_event(input);
if state.app.needs_frame() {
state.window.request_redraw();
}
}
state.update_ime();
}
Ok(())
}
};
if let Err(error) = result {
self.fail(event_loop, error);
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.wait_for_timers(event_loop);
}
}
impl WindowState {
fn update_ime(&mut self) {
let area = self.app.ime_area();
if area == self.ime_area && self.scale == self.ime_scale {
return;
}
self.ime_area = area;
self.ime_scale = self.scale;
match area {
Some(area) => {
if !self.ime_allowed {
self.ime_allowed = true;
self.window.set_ime_allowed(true);
}
self.window.set_ime_cursor_area(
LogicalPosition::new(f64::from(area.x0), f64::from(area.y0)),
LogicalSize::new(f64::from(area.width()), f64::from(area.height())),
);
}
None => {
if self.ime_allowed {
self.ime_allowed = false;
self.window.set_ime_allowed(false);
}
}
}
}
#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
fn update_a11y(&mut self) {
if !self.a11y_active || !(self.a11y_full || self.app.has_a11y_changes()) {
return;
}
let full = std::mem::take(&mut self.a11y_full);
let (width, height) = self.physical;
let (projection, app, scale) = (&mut self.a11y, &mut self.app, self.scale);
self.adapter.update_if_active(|| {
projection.update(app, f64::from(width), f64::from(height), scale, full)
});
}
fn finished(&self) -> bool {
self.frame_limit
.is_some_and(|limit| self.frames_presented >= limit)
}
fn below_frame_limit(&self) -> bool {
self.frame_limit
.is_some_and(|limit| self.frames_presented < limit)
}
}
fn create_presenter(
window: &Arc<Window>,
choice: RendererChoice,
width: u32,
height: u32,
show_damage: bool,
) -> Result<Presenter, PlatformError> {
#[cfg(not(feature = "gpu"))]
{
match choice {
RendererChoice::Gpu => Err(PlatformError::Renderer(String::from(
"the GPU renderer needs a build with the gpu feature",
))),
_ => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
window,
width,
height,
show_damage,
)?))),
}
}
#[cfg(feature = "gpu")]
match choice {
RendererChoice::Cpu => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
window,
width,
height,
show_damage,
)?))),
RendererChoice::Auto if show_damage => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
window,
width,
height,
show_damage,
)?))),
RendererChoice::Gpu | RendererChoice::Auto => {
let explicit = choice == RendererChoice::Gpu;
if explicit && show_damage {
return Err(PlatformError::Renderer(String::from(
"the damage overlay tints the window buffer of the CPU \
presentation path and cannot run on the GPU renderer",
)));
}
match crate::gpu::GpuPath::new(window, width, height, explicit)? {
Some(gpu) => Ok(Presenter::Gpu(Box::new(gpu))),
None => Ok(Presenter::Cpu(Box::new(CpuPresenter::new(
window,
width,
height,
show_damage,
)?))),
}
}
}
}
enum Presenter {
Cpu(Box<CpuPresenter>),
#[cfg(feature = "gpu")]
Gpu(Box<crate::gpu::GpuPath>),
}
impl Presenter {
fn render_and_present(
&mut self,
frame: &FrameOutput<'_>,
has_tick: bool,
) -> Result<Option<PresentStats>, PlatformError> {
match self {
Presenter::Cpu(cpu) => cpu.render_and_present(frame, has_tick),
#[cfg(feature = "gpu")]
Presenter::Gpu(gpu) => gpu.render_and_present(frame, has_tick),
}
}
fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), PlatformError> {
match self {
Presenter::Cpu(cpu) => cpu.resize(width, height),
#[cfg(feature = "gpu")]
Presenter::Gpu(gpu) => gpu.resize(width.get(), height.get()),
}
}
#[cfg(feature = "gpu")]
fn adapter_summary(&self) -> Option<AdapterSummary> {
match self {
Presenter::Cpu(_) => None,
Presenter::Gpu(gpu) => Some(gpu.adapter_summary()),
}
}
}
struct CpuPresenter {
surface: Surface<SharedWindow, SharedWindow>,
window: SharedWindow,
renderer: CpuRenderer,
show_damage: bool,
history: DamageHistory,
pending: PendingFrame,
region: Vec<PixelRect>,
reported: Vec<softbuffer::Rect>,
}
impl CpuPresenter {
fn new(
window: &Arc<Window>,
width: u32,
height: u32,
show_damage: bool,
) -> Result<CpuPresenter, PlatformError> {
let shared = SharedWindow(Arc::clone(window));
let context = Context::new(shared.clone()).map_err(PlatformError::Surface)?;
let mut surface = Surface::new(&context, shared).map_err(PlatformError::Surface)?;
drop(context);
if let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) {
surface
.resize(width, height)
.map_err(PlatformError::Surface)?;
}
Ok(CpuPresenter {
surface,
window: SharedWindow(Arc::clone(window)),
renderer: CpuRenderer::new(width, height),
show_damage,
history: DamageHistory::default(),
pending: PendingFrame::default(),
region: Vec::new(),
reported: Vec::new(),
})
}
fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), PlatformError> {
self.surface
.resize(width, height)
.map_err(PlatformError::Surface)?;
self.renderer.resize(width.get(), height.get());
Ok(())
}
fn render_and_present(
&mut self,
frame: &FrameOutput<'_>,
has_tick: bool,
) -> Result<Option<PresentStats>, PlatformError> {
let (width, height) = (self.renderer.width(), self.renderer.height());
if width == 0 || height == 0 {
return Ok(None);
}
let raster_start = Instant::now();
self.renderer.render(frame);
let raster = raster_start.elapsed();
self.pending.damage.clear();
self.pending.damage.extend_from_slice(frame.damage);
self.pending.full = frame.full;
if present_skipped(&self.pending.damage, has_tick, self.show_damage) {
return Ok(None);
}
let acquire_start = Instant::now();
let mut buffer = self.surface.buffer_mut().map_err(PlatformError::Surface)?;
let acquire = acquire_start.elapsed();
let present_start = Instant::now();
debug_assert_eq!(
buffer.len(),
self.renderer.pixels().len() / 4,
"the window buffer must hold one entry per rendered pixel"
);
let age = buffer.age();
let with_damage = !self.show_damage
&& damage_region(
age,
&self.pending.damage,
self.pending.full,
&self.history,
&mut self.region,
);
let mut bytes_copied = 0;
if with_damage {
self.reported.clear();
for rect in &self.region {
let (Some(rect_width), Some(rect_height)) =
(NonZeroU32::new(rect.width), NonZeroU32::new(rect.height))
else {
continue;
};
bytes_copied += u64::from(rect.width) * u64::from(rect.height) * 4;
self.reported.push(softbuffer::Rect {
x: rect.x,
y: rect.y,
width: rect_width,
height: rect_height,
});
}
for &rect in &self.region {
copy_rect(self.renderer.pixels(), width, &mut buffer, rect);
}
self.window.0.pre_present_notify();
buffer
.present_with_damage(&self.reported)
.map_err(PlatformError::Surface)?;
} else {
bytes_copied = u64::from(width) * u64::from(height) * 4;
copy_rect(
self.renderer.pixels(),
width,
&mut buffer,
PixelRect {
x: 0,
y: 0,
width,
height,
},
);
if self.show_damage {
tint_rects(&mut buffer, width, &self.pending.damage);
}
self.window.0.pre_present_notify();
buffer.present().map_err(PlatformError::Surface)?;
}
self.history.push(&self.pending.damage, self.pending.full);
Ok(Some(PresentStats {
buffer_age: age,
with_damage,
bytes_copied,
surface_width: width,
surface_height: height,
raster,
acquire,
present: present_start.elapsed(),
renderer: "cpu",
}))
}
}
fn logical_size(physical: PhysicalSize<u32>, scale: f32) -> Size {
Size {
width: physical.width as f32 / scale,
height: physical.height as f32 / scale,
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum WaitDecision {
Now,
Until(Instant),
Wait,
}
fn wait_decision(deadline: Option<Instant>, now: Instant) -> WaitDecision {
match deadline {
Some(deadline) if deadline <= now => WaitDecision::Now,
Some(deadline) => WaitDecision::Until(deadline),
None => WaitDecision::Wait,
}
}
#[derive(Clone)]
pub(crate) struct SharedWindow(pub(crate) Arc<Window>);
impl HasWindowHandle for SharedWindow {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
self.0.window_handle()
}
}
impl HasDisplayHandle for SharedWindow {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
self.0.display_handle()
}
}
#[cfg(test)]
#[path = "window_tests.rs"]
mod tests;