use std::sync::Arc;
use std::time::{Duration, Instant};
use cotis::renders::{CotisRenderer, CotisRendererAsync, RenderCompatibleWith};
use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
use cotis_utils::interactivity::keyboard::KeyboardKey;
use cotis_utils::interactivity::mouse::MouseButton;
use cotis_utils::math::{Dimensions, Vector2};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::error::EventLoopError;
use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
use winit::window::{Window, WindowId};
use crate::device::GpuDevice;
use crate::drawable::{WgpuDrawContext, WgpuDrawable};
use crate::error::WgpuBackendError;
use crate::input::InputSnapshot;
use crate::pipeline::GeometryBatch;
use crate::surface::SurfaceState;
use crate::text::TextState;
#[derive(Debug, Clone)]
pub struct WindowConfig {
pub title: String,
pub width: u32,
pub height: u32,
}
impl Default for WindowConfig {
fn default() -> Self {
Self {
title: "cotis-wgpu".to_string(),
width: 1000,
height: 800,
}
}
}
struct WgpuRendererInner {
window: Arc<Window>,
gpu: GpuDevice,
surface: SurfaceState,
geometry: GeometryBatch,
pub(crate) text: TextState,
background: wgpu::Color,
size: (u32, u32),
pub(crate) dpi_scale: f32,
pub(crate) input: InputSnapshot,
last_frame: Instant,
close_requested: bool,
}
pub struct CotisWgpuRenderer {
event_loop: EventLoop<()>,
inner: WgpuRendererInner,
}
impl CotisWgpuRenderer {
pub fn auto_start() -> Self {
Self::new(wgpu::Color {
r: 45.0 / 255.0,
g: 45.0 / 255.0,
b: 55.0 / 255.0,
a: 1.0,
})
.expect("failed to start cotis-wgpu renderer")
}
pub fn new(background: wgpu::Color) -> Result<Self, WgpuBackendError> {
Self::new_with_config(WindowConfig::default(), background)
}
pub fn new_with_config(
config: WindowConfig,
background: wgpu::Color,
) -> Result<Self, WgpuBackendError> {
let mut event_loop = EventLoop::new()?;
event_loop.set_control_flow(ControlFlow::Poll);
let mut bootstrap = BootstrapHandler {
config,
background,
inner: None,
error: None,
close_requested: false,
};
loop {
let status = event_loop.pump_app_events(Some(Duration::ZERO), &mut bootstrap);
if let Some(inner) = bootstrap.inner.take() {
return Ok(Self { event_loop, inner });
}
if bootstrap.close_requested || matches!(status, PumpStatus::Exit(_)) {
return Err(bootstrap.error.unwrap_or(WgpuBackendError::Init(
"window closed during startup".into(),
)));
}
}
}
pub fn window_should_close(&mut self) -> bool {
self.pump_events();
self.inner.close_requested
}
pub fn window(&self) -> &Arc<Window> {
&self.inner.window
}
pub fn set_background(&mut self, background: wgpu::Color) {
self.inner.background = background;
}
pub(crate) fn window_dimensions(&self) -> Dimensions {
self.inner.window_dimensions()
}
pub(crate) fn delta_time(&self) -> f32 {
self.inner.delta_time()
}
pub(crate) fn dpi_scale(&self) -> f32 {
self.inner.dpi_scale
}
pub(crate) fn input(&self) -> &InputSnapshot {
&self.inner.input
}
pub(crate) fn text(&self) -> &TextState {
&self.inner.text
}
fn pump_events(&mut self) {
let mut handler = RuntimeHandler {
inner: &mut self.inner,
};
let status = self
.event_loop
.pump_app_events(Some(Duration::ZERO), &mut handler);
if matches!(status, PumpStatus::Exit(_)) {
self.inner.close_requested = true;
}
}
}
impl WgpuRendererInner {
fn from_window(window: Arc<Window>, background: wgpu::Color) -> Result<Self, WgpuBackendError> {
let size = window.inner_size();
let dpi_scale = window.scale_factor() as f32;
let gpu = GpuDevice::new(&window)?;
let surface = SurfaceState::new(&gpu, &window, size.width, size.height)?;
let geometry = GeometryBatch::new(
&gpu.device,
surface.format(),
(size.width as i32, size.height as i32),
);
let text = TextState::new(
&gpu.device,
&gpu.queue,
surface.format(),
(size.width, size.height),
dpi_scale,
);
Ok(WgpuRendererInner {
window,
gpu,
surface,
geometry,
text,
background,
size: (size.width, size.height),
dpi_scale,
input: InputSnapshot::default(),
last_frame: Instant::now(),
close_requested: false,
})
}
fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
self.size = (width, height);
self.surface.resize(&self.gpu, width, height);
self.geometry.resize_vertices((width as i32, height as i32));
}
fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.dpi_scale = dpi_scale;
self.text.set_dpi_scale(dpi_scale);
}
fn present_frame(
&mut self,
draw: impl FnOnce(&mut WgpuDrawContext<'_>),
) -> Result<(), WgpuBackendError> {
let drawable = self.surface.acquire_frame(&self.gpu)?;
let view = drawable
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("cotis-wgpu frame encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("cotis-wgpu frame pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(self.background),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.surface.depth.view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
let mut depth = 0.1_f32;
let mut draw_context = WgpuDrawContext {
geometry: &mut self.geometry,
text: &mut self.text,
scissor_active: false,
scissor_position: crate::pipeline::UIPosition::new(),
scissor_bounds: crate::pipeline::UIPosition::new(),
depth: &mut depth,
dpi_scale: self.dpi_scale,
};
draw(&mut draw_context);
self.geometry.flush(&mut render_pass, &self.gpu.queue);
self.text.flush(
&self.gpu.device,
&self.gpu.queue,
&mut render_pass,
&self.surface.config,
);
}
self.gpu.queue.submit(std::iter::once(encoder.finish()));
drawable.present();
self.last_frame = Instant::now();
Ok(())
}
fn draw_commands<'a, T: WgpuDrawable + 'a>(
&mut self,
render_commands: impl Iterator<Item = T>,
) -> Result<(), WgpuBackendError> {
self.present_frame(|draw_context| {
for command in render_commands {
Box::new(command).draw(draw_context);
}
})
}
fn draw_boxed_commands<'b>(
&mut self,
render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>,
) -> Result<(), WgpuBackendError> {
self.present_frame(|draw_context| {
for command in render_commands {
command.draw(draw_context);
}
})
}
pub(crate) fn window_dimensions(&self) -> Dimensions {
Dimensions::new(self.size.0 as f32, self.size.1 as f32)
}
pub(crate) fn delta_time(&self) -> f32 {
self.last_frame.elapsed().as_secs_f32()
}
}
impl<'b> CotisRenderer<Box<dyn WgpuDrawable + 'b>> for CotisWgpuRenderer {
fn draw_frame(&mut self, render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>) {
self.pump_events();
if let Err(error) = self.inner.draw_boxed_commands(render_commands) {
log::warn!("frame render failed: {error}");
}
}
}
impl<T: WgpuDrawable, L: WgpuDrawable + IsRenderList> CotisRenderer<RenderTList<T, L>>
for CotisWgpuRenderer
{
fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, L>>) {
self.pump_events();
if let Err(error) = self.inner.draw_commands(render_commands) {
log::warn!("frame render failed: {error}");
}
}
}
impl<T: WgpuDrawable> CotisRenderer<RenderTList<T, ()>> for CotisWgpuRenderer {
fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, ()>>) {
self.pump_events();
if let Err(error) = self.inner.draw_commands(render_commands) {
log::warn!("frame render failed: {error}");
}
}
}
impl<'b> CotisRendererAsync<Box<dyn WgpuDrawable + 'b>> for CotisWgpuRenderer {
async fn draw_frame(
&mut self,
render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>,
) {
CotisRenderer::draw_frame(self, render_commands);
}
}
impl<T: WgpuDrawable, L: WgpuDrawable + IsRenderList> CotisRendererAsync<RenderTList<T, L>>
for CotisWgpuRenderer
{
async fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, L>>) {
CotisRenderer::draw_frame(self, render_commands);
}
}
impl<T: WgpuDrawable> CotisRendererAsync<RenderTList<T, ()>> for CotisWgpuRenderer {
async fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, ()>>) {
CotisRenderer::draw_frame(self, render_commands);
}
}
impl<Manager> RenderCompatibleWith<Manager> for CotisWgpuRenderer {
fn init(&mut self, _layout_manager: &mut Manager) {}
}
struct BootstrapHandler {
config: WindowConfig,
background: wgpu::Color,
inner: Option<WgpuRendererInner>,
error: Option<WgpuBackendError>,
close_requested: bool,
}
impl BootstrapHandler {
fn try_create(&mut self, event_loop: &ActiveEventLoop) {
if self.inner.is_some() {
return;
}
let window_attributes = Window::default_attributes()
.with_title(self.config.title.clone())
.with_inner_size(LogicalSize::new(self.config.width, self.config.height));
let window = match event_loop.create_window(window_attributes) {
Ok(window) => Arc::new(window),
Err(error) => {
self.error = Some(WgpuBackendError::Init(error.to_string()));
event_loop.exit();
return;
}
};
match WgpuRendererInner::from_window(window, self.background) {
Ok(inner) => {
self.inner = Some(inner);
}
Err(error) => {
self.error = Some(error);
event_loop.exit();
}
}
}
}
impl ApplicationHandler for BootstrapHandler {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.try_create(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
if matches!(event, WindowEvent::CloseRequested) {
self.close_requested = true;
event_loop.exit();
}
}
}
struct RuntimeHandler<'a> {
inner: &'a mut WgpuRendererInner,
}
impl ApplicationHandler for RuntimeHandler<'_> {
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::CloseRequested => self.inner.close_requested = true,
WindowEvent::Resized(size) => {
self.inner.resize(size.width, size.height);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.inner.set_dpi_scale(scale_factor as f32);
}
WindowEvent::CursorMoved { position, .. } => {
self.inner.input.mouse_position =
Vector2::new(position.x as f32, position.y as f32);
}
WindowEvent::MouseInput { state, button, .. } => {
if let Some(mapped) = map_mouse_button(button) {
if state == ElementState::Pressed {
self.inner.input.mouse_buttons.insert(mapped);
} else {
self.inner.input.mouse_buttons.swap_remove(&mapped);
}
}
}
WindowEvent::MouseWheel { delta, .. } => {
self.inner.input.scroll_delta = match delta {
MouseScrollDelta::LineDelta(x, y) => Vector2::new(x, y),
MouseScrollDelta::PixelDelta(position) => {
Vector2::new(position.x as f32, position.y as f32)
}
};
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key
&& let Some(key) = map_keyboard_key(code)
{
if event.state == ElementState::Pressed {
self.inner.input.pressed_keys.insert(key);
} else {
self.inner.input.pressed_keys.swap_remove(&key);
}
}
}
WindowEvent::Ime(winit::event::Ime::Preedit(text, _)) => {
self.inner.input.pressed_chars.extend(text.chars());
}
_ => {}
}
}
}
impl From<EventLoopError> for WgpuBackendError {
fn from(value: EventLoopError) -> Self {
Self::Init(value.to_string())
}
}
fn map_mouse_button(button: winit::event::MouseButton) -> Option<MouseButton> {
match button {
winit::event::MouseButton::Left => Some(MouseButton::MouseButtonLeft),
winit::event::MouseButton::Right => Some(MouseButton::MouseButtonRight),
winit::event::MouseButton::Middle => Some(MouseButton::MouseButtonMiddle),
winit::event::MouseButton::Back => Some(MouseButton::MouseButtonBack),
winit::event::MouseButton::Forward => Some(MouseButton::MouseButtonForward),
_ => None,
}
}
fn map_keyboard_key(code: KeyCode) -> Option<KeyboardKey> {
let value = match code {
KeyCode::Space => KeyboardKey::KeySpace,
KeyCode::Escape => KeyboardKey::KeyEscape,
KeyCode::Enter => KeyboardKey::KeyEnter,
KeyCode::Tab => KeyboardKey::KeyTab,
KeyCode::Backspace => KeyboardKey::KeyBackspace,
KeyCode::Insert => KeyboardKey::KeyInsert,
KeyCode::Delete => KeyboardKey::KeyDelete,
KeyCode::ArrowRight => KeyboardKey::KeyRight,
KeyCode::ArrowLeft => KeyboardKey::KeyLeft,
KeyCode::ArrowDown => KeyboardKey::KeyDown,
KeyCode::ArrowUp => KeyboardKey::KeyUp,
KeyCode::PageUp => KeyboardKey::KeyPageUp,
KeyCode::PageDown => KeyboardKey::KeyPageDown,
KeyCode::Home => KeyboardKey::KeyHome,
KeyCode::End => KeyboardKey::KeyEnd,
KeyCode::KeyA => KeyboardKey::KeyA,
KeyCode::KeyB => KeyboardKey::KeyB,
KeyCode::KeyC => KeyboardKey::KeyC,
KeyCode::KeyD => KeyboardKey::KeyD,
KeyCode::KeyE => KeyboardKey::KeyE,
KeyCode::KeyF => KeyboardKey::KeyF,
KeyCode::KeyG => KeyboardKey::KeyG,
KeyCode::KeyH => KeyboardKey::KeyH,
KeyCode::KeyI => KeyboardKey::KeyI,
KeyCode::KeyJ => KeyboardKey::KeyJ,
KeyCode::KeyK => KeyboardKey::KeyK,
KeyCode::KeyL => KeyboardKey::KeyL,
KeyCode::KeyM => KeyboardKey::KeyM,
KeyCode::KeyN => KeyboardKey::KeyN,
KeyCode::KeyO => KeyboardKey::KeyO,
KeyCode::KeyP => KeyboardKey::KeyP,
KeyCode::KeyQ => KeyboardKey::KeyQ,
KeyCode::KeyR => KeyboardKey::KeyR,
KeyCode::KeyS => KeyboardKey::KeyS,
KeyCode::KeyT => KeyboardKey::KeyT,
KeyCode::KeyU => KeyboardKey::KeyU,
KeyCode::KeyV => KeyboardKey::KeyV,
KeyCode::KeyW => KeyboardKey::KeyW,
KeyCode::KeyX => KeyboardKey::KeyX,
KeyCode::KeyY => KeyboardKey::KeyY,
KeyCode::KeyZ => KeyboardKey::KeyZ,
KeyCode::Digit0 => KeyboardKey::KeyZero,
KeyCode::Digit1 => KeyboardKey::KeyOne,
KeyCode::Digit2 => KeyboardKey::KeyTwo,
KeyCode::Digit3 => KeyboardKey::KeyThree,
KeyCode::Digit4 => KeyboardKey::KeyFour,
KeyCode::Digit5 => KeyboardKey::KeyFive,
KeyCode::Digit6 => KeyboardKey::KeySix,
KeyCode::Digit7 => KeyboardKey::KeySeven,
KeyCode::Digit8 => KeyboardKey::KeyEight,
KeyCode::Digit9 => KeyboardKey::KeyNine,
_ => return None,
};
Some(value)
}