//! Implementation of software buffering for web targets. #![allow(clippy::uninlined_format_args)] use raw_window_handle::WebWindowHandle; use wasm_bindgen::JsCast; use web_sys::CanvasRenderingContext2d; use web_sys::HtmlCanvasElement; use web_sys::ImageData; use crate::error::SwResultExt; use crate::{Rect, SoftBufferError}; use std::convert::TryInto; use std::num::NonZeroU32; /// Display implementation for the web platform. /// /// This just caches the document to prevent having to query it every time. pub struct WebDisplayImpl { document: web_sys::Document, } impl WebDisplayImpl { pub(super) fn new() -> Result { let document = web_sys::window() .swbuf_err("`window` is not present in this runtime")? .document() .swbuf_err("`document` is not present in this runtime")?; Ok(Self { document }) } } pub struct WebImpl { /// The handle to the canvas that we're drawing to. canvas: HtmlCanvasElement, /// The 2D rendering context for the canvas. ctx: CanvasRenderingContext2d, /// The buffer that we're drawing to. buffer: Vec, /// The current width of the canvas. width: u32, } impl WebImpl { pub fn new(display: &WebDisplayImpl, handle: WebWindowHandle) -> Result { let canvas: HtmlCanvasElement = display .document .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id)) // `querySelector` only throws an error if the selector is invalid. .unwrap() .swbuf_err("No canvas found with the given id")? // We already made sure this was a canvas in `querySelector`. .unchecked_into(); let ctx = canvas .get_context("2d") .ok() .swbuf_err("Canvas already controlled using `OffscreenCanvas`")? .swbuf_err( "A canvas context other than `CanvasRenderingContext2d` was already created", )? .dyn_into() .expect("`getContext(\"2d\") didn't return a `CanvasRenderingContext2d`"); Ok(Self { canvas, ctx, buffer: Vec::new(), width: 0, }) } /// Resize the canvas to the given dimensions. pub(crate) fn resize( &mut self, width: NonZeroU32, height: NonZeroU32, ) -> Result<(), SoftBufferError> { let width = width.get(); let height = height.get(); self.buffer.resize(total_len(width, height), 0); self.canvas.set_width(width); self.canvas.set_height(height); self.width = width; Ok(()) } /// Get a pointer to the mutable buffer. pub(crate) fn buffer_mut(&mut self) -> Result { Ok(BufferImpl { imp: self }) } } pub struct BufferImpl<'a> { imp: &'a mut WebImpl, } impl<'a> BufferImpl<'a> { pub fn pixels(&self) -> &[u32] { &self.imp.buffer } pub fn pixels_mut(&mut self) -> &mut [u32] { &mut self.imp.buffer } /// Push the buffer to the canvas. pub fn present(self) -> Result<(), SoftBufferError> { // Create a bitmap from the buffer. let bitmap: Vec<_> = self .imp .buffer .iter() .copied() .flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, pixel as u8, 255]) .collect(); #[cfg(target_feature = "atomics")] let result = { use js_sys::{Uint8Array, Uint8ClampedArray}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_name = ImageData)] type ImageDataExt; #[wasm_bindgen(catch, constructor, js_class = ImageData)] fn new(array: Uint8ClampedArray, sw: u32) -> Result; } let array = Uint8Array::new_with_length(bitmap.len() as u32); array.copy_from(&bitmap); let array = Uint8ClampedArray::new(&array); ImageDataExt::new(array, self.imp.width) .map(JsValue::from) .map(ImageData::unchecked_from_js) }; #[cfg(not(target_feature = "atomics"))] let result = ImageData::new_with_u8_clamped_array(wasm_bindgen::Clamped(&bitmap), self.imp.width); // This should only throw an error if the buffer we pass's size is incorrect. let image_data = result.unwrap(); // This can only throw an error if `data` is detached, which is impossible. self.imp.ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); Ok(()) } pub fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { self.present() } } #[inline(always)] fn total_len(width: u32, height: u32) -> usize { // Convert width and height to `usize`, then multiply. width .try_into() .ok() .and_then(|w: usize| height.try_into().ok().and_then(|h| w.checked_mul(h))) .unwrap_or_else(|| { panic!( "Overflow when calculating total length of buffer: {}x{}", width, height ); }) }