use std::sync::Arc;
use std::sync::mpsc;
use winit::event::WindowEvent;
use winit::window::Window;
use accesskit::ActionRequest;
use teksilo_render::Renderer;
#[derive(Debug, thiserror::Error)]
#[error("Surface error: {0}")]
pub struct SurfaceRenderError(pub String);
#[derive(Debug)]
pub enum FrameOutcome {
Rendered,
Skipped,
NeedsReconfigure,
Error(SurfaceRenderError),
}
pub struct PlatformWindow {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
renderer: Renderer,
scale_factor: f64,
a11y_adapter: Option<accesskit_winit::Adapter>,
a11y_action_rx: mpsc::Receiver<ActionRequest>,
}
impl PlatformWindow {
pub async fn new_with_a11y(
window: Window,
event_loop: &winit::event_loop::ActiveEventLoop,
) -> Self {
let window = Arc::new(window);
let size = window.inner_size();
let scale_factor = window.scale_factor();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let surface = instance
.create_surface(window.clone())
.expect("wgpu surface creation failed for the platform window");
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
..Default::default()
})
.await
.expect("no compatible wgpu adapter available");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("teksilo_device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("wgpu device request failed");
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.or_else(|| surface_caps.formats.first().copied())
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: surface_caps
.alpha_modes
.first()
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
view_formats: vec![],
desired_maximum_frame_latency: 2,
color_space: wgpu::SurfaceColorSpace::Auto,
};
surface.configure(&device, &surface_config);
let renderer = Renderer::new(device, queue, surface_format);
let (action_tx, action_rx) = mpsc::channel();
let a11y_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop,
&window,
TeksiloActivationHandler,
TeksiloActionHandler { tx: action_tx },
TeksiloDeactivationHandler,
);
window.set_visible(true);
Self {
window,
surface,
surface_config,
renderer,
scale_factor,
a11y_adapter: Some(a11y_adapter),
a11y_action_rx: action_rx,
}
}
pub async fn new(window: Window) -> Self {
let window = Arc::new(window);
let size = window.inner_size();
let scale_factor = window.scale_factor();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let surface = instance
.create_surface(window.clone())
.expect("wgpu surface creation failed for the platform window");
let gpu_adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
..Default::default()
})
.await
.expect("no compatible wgpu adapter available");
let (device, queue) = gpu_adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("teksilo_device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("wgpu device request failed");
let surface_caps = surface.get_capabilities(&gpu_adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.or_else(|| surface_caps.formats.first().copied())
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: surface_caps
.alpha_modes
.first()
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
view_formats: vec![],
desired_maximum_frame_latency: 2,
color_space: wgpu::SurfaceColorSpace::Auto,
};
surface.configure(&device, &surface_config);
let renderer = Renderer::new(device, queue, surface_format);
let (_action_tx, action_rx) = mpsc::channel();
Self {
window,
surface,
surface_config,
renderer,
scale_factor,
a11y_adapter: None,
a11y_action_rx: action_rx,
}
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn window_arc(&self) -> Arc<Window> {
self.window.clone()
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn scale_factor(&self) -> f64 {
self.scale_factor
}
pub fn set_scale_factor(&mut self, factor: f64) {
self.scale_factor = factor;
}
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.surface_config.width = new_size.width;
self.surface_config.height = new_size.height;
self.surface
.configure(self.renderer.device(), &self.surface_config);
}
}
pub fn surface_size(&self) -> (u32, u32) {
(self.surface_config.width, self.surface_config.height)
}
pub fn reconfigure_surface(&mut self) {
self.surface
.configure(self.renderer.device(), &self.surface_config);
}
pub fn render_frame(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
) -> FrameOutcome {
let current = self.surface.get_current_texture();
let output = match current {
wgpu::CurrentSurfaceTexture::Success(tex)
| wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Timeout => {
return FrameOutcome::Skipped;
}
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
return FrameOutcome::NeedsReconfigure;
}
other => return FrameOutcome::Error(SurfaceRenderError(format!("{other:?}"))),
};
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let (w, h) = self.surface_size();
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
self.renderer.queue().present(output);
FrameOutcome::Rendered
}
pub fn capture_offscreen(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
crop: Option<teksilo_canvas::Rect>,
) -> (Vec<u8>, u32, u32) {
fn crop_rgba(
src: &[u8],
w: u32,
h: u32,
rect: teksilo_canvas::Rect,
) -> (Vec<u8>, u32, u32) {
let x0 = (rect.x.floor().max(0.0) as u32).min(w);
let y0 = (rect.y.floor().max(0.0) as u32).min(h);
let x1 = ((rect.x + rect.width).ceil().max(0.0) as u32).min(w);
let y1 = ((rect.y + rect.height).ceil().max(0.0) as u32).min(h);
if x1 <= x0 || y1 <= y0 {
return (Vec::new(), 0, 0);
}
let cw = x1 - x0;
let ch = y1 - y0;
let mut out = Vec::with_capacity((cw * ch * 4) as usize);
for y in y0..y1 {
let row_start = ((y * w + x0) * 4) as usize;
let row_end = row_start + (cw * 4) as usize;
out.extend_from_slice(&src[row_start..row_end]);
}
(out, cw, ch)
}
let (w, h) = self.surface_size();
let format = self.surface_config.format;
debug_assert!(
matches!(
format,
wgpu::TextureFormat::Rgba8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8Unorm
| wgpu::TextureFormat::Bgra8UnormSrgb
),
"capture_offscreen: unsupported surface format {format:?} (expected 8-bit RGBA/BGRA)"
);
let texture = self
.renderer
.device()
.create_texture(&wgpu::TextureDescriptor {
label: Some("teksilo-automation capture"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
let mut bytes = teksilo_render::test_support::read_texture_rgba(
self.renderer.device(),
self.renderer.queue(),
&texture,
w,
h,
);
if matches!(
format,
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
) {
for px in bytes.as_chunks_mut::<4>().0 {
px.swap(0, 2);
}
}
match crop {
Some(rect) => crop_rgba(&bytes, w, h, rect),
None => (bytes, w, h),
}
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn update_accessibility(&mut self, update: accesskit::TreeUpdate) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.update_if_active(|| update);
}
}
pub fn process_accessibility_event(&mut self, event: &WindowEvent) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.process_event(&self.window, event);
}
}
pub fn drain_accessibility_actions(&self) -> Vec<ActionRequest> {
let mut actions = Vec::new();
while let Ok(req) = self.a11y_action_rx.try_recv() {
actions.push(req);
}
actions
}
}
struct TeksiloActivationHandler;
impl accesskit::ActivationHandler for TeksiloActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
let root = accesskit::Node::new(accesskit::Role::Window);
Some(accesskit::TreeUpdate {
nodes: vec![(accesskit::NodeId(0), root)],
tree: Some(accesskit::TreeInfo::new(accesskit::NodeId(0))),
tree_id: accesskit::TreeId::ROOT,
focus: accesskit::NodeId(0),
})
}
}
struct TeksiloActionHandler {
tx: mpsc::Sender<ActionRequest>,
}
impl accesskit::ActionHandler for TeksiloActionHandler {
fn do_action(&mut self, request: ActionRequest) {
let _ = self.tx.send(request);
}
}
struct TeksiloDeactivationHandler;
impl accesskit::DeactivationHandler for TeksiloDeactivationHandler {
fn deactivate_accessibility(&mut self) {
}
}