1#![allow(clippy::uninlined_format_args)]
4
5use js_sys::Object;
6use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
7use wasm_bindgen::{JsCast, JsValue};
8use web_sys::ImageData;
9use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
10use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
11
12use crate::backend_interface::*;
13use crate::error::{InitError, SwResultExt};
14use crate::{util, NoDisplayHandle, NoWindowHandle, Rect, SoftBufferError};
15use std::marker::PhantomData;
16use std::num::NonZeroU32;
17
18pub struct WebDisplayImpl<D> {
22 document: web_sys::Document,
23 _display: D,
24}
25
26impl<D: HasDisplayHandle> ContextInterface<D> for WebDisplayImpl<D> {
27 fn new(display: D) -> Result<Self, InitError<D>> {
28 let raw = display.display_handle()?.as_raw();
29 let RawDisplayHandle::Web(..) = raw else {
30 return Err(InitError::Unsupported(display));
31 };
32
33 let document = web_sys::window()
34 .swbuf_err("`Window` is not present in this runtime")?
35 .document()
36 .swbuf_err("`Document` is not present in this runtime")?;
37
38 Ok(Self {
39 document,
40 _display: display,
41 })
42 }
43}
44
45pub struct WebImpl<D, W> {
46 canvas: Canvas,
48
49 buffer: Vec<u32>,
51
52 buffer_presented: bool,
54
55 size: Option<(NonZeroU32, NonZeroU32)>,
57
58 window_handle: W,
60
61 _display: PhantomData<D>,
63}
64
65enum Canvas {
68 Canvas {
69 canvas: HtmlCanvasElement,
70 ctx: CanvasRenderingContext2d,
71 },
72 OffscreenCanvas {
73 canvas: OffscreenCanvas,
74 ctx: OffscreenCanvasRenderingContext2d,
75 },
76}
77
78impl<D: HasDisplayHandle, W: HasWindowHandle> WebImpl<D, W> {
79 fn from_canvas(canvas: HtmlCanvasElement, window: W) -> Result<Self, SoftBufferError> {
80 let ctx = Self::resolve_ctx(canvas.get_context("2d").ok(), "CanvasRenderingContext2d")?;
81
82 Ok(Self {
83 canvas: Canvas::Canvas { canvas, ctx },
84 buffer: Vec::new(),
85 buffer_presented: false,
86 size: None,
87 window_handle: window,
88 _display: PhantomData,
89 })
90 }
91
92 fn from_offscreen_canvas(canvas: OffscreenCanvas, window: W) -> Result<Self, SoftBufferError> {
93 let ctx = Self::resolve_ctx(
94 canvas.get_context("2d").ok(),
95 "OffscreenCanvasRenderingContext2d",
96 )?;
97
98 Ok(Self {
99 canvas: Canvas::OffscreenCanvas { canvas, ctx },
100 buffer: Vec::new(),
101 buffer_presented: false,
102 size: None,
103 window_handle: window,
104 _display: PhantomData,
105 })
106 }
107
108 fn resolve_ctx<T: JsCast>(
109 result: Option<Option<Object>>,
110 name: &str,
111 ) -> Result<T, SoftBufferError> {
112 let ctx = result
113 .swbuf_err("Canvas already controlled using `OffscreenCanvas`")?
114 .swbuf_err(format!(
115 "A canvas context other than `{name}` was already created"
116 ))?
117 .dyn_into()
118 .unwrap_or_else(|_| panic!("`getContext(\"2d\") didn't return a `{name}`"));
119
120 Ok(ctx)
121 }
122
123 fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> {
124 let (buffer_width, _buffer_height) = self
125 .size
126 .expect("Must set size of surface before calling `present_with_damage()`");
127
128 let union_damage = if let Some(rect) = util::union_damage(damage) {
129 rect
130 } else {
131 return Ok(());
132 };
133
134 let bitmap: Vec<_> = self
136 .buffer
137 .chunks_exact(buffer_width.get() as usize)
138 .skip(union_damage.y as usize)
139 .take(union_damage.height.get() as usize)
140 .flat_map(|row| {
141 row.iter()
142 .skip(union_damage.x as usize)
143 .take(union_damage.width.get() as usize)
144 })
145 .copied()
146 .flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, pixel as u8, 255])
147 .collect();
148
149 debug_assert_eq!(
150 bitmap.len() as u32,
151 union_damage.width.get() * union_damage.height.get() * 4
152 );
153
154 #[cfg(target_feature = "atomics")]
155 #[allow(non_local_definitions)]
156 let result = {
157 use js_sys::{Uint8Array, Uint8ClampedArray};
160 use wasm_bindgen::prelude::wasm_bindgen;
161
162 #[wasm_bindgen]
163 extern "C" {
164 #[wasm_bindgen(js_name = ImageData)]
165 type ImageDataExt;
166
167 #[wasm_bindgen(catch, constructor, js_class = ImageData)]
168 fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
169 }
170
171 let array = Uint8Array::new_with_length(bitmap.len() as u32);
172 array.copy_from(&bitmap);
173 let array = Uint8ClampedArray::new(&array);
174 ImageDataExt::new(array, union_damage.width.get())
175 .map(JsValue::from)
176 .map(ImageData::unchecked_from_js)
177 };
178 #[cfg(not(target_feature = "atomics"))]
179 let result = ImageData::new_with_u8_clamped_array(
180 wasm_bindgen::Clamped(&bitmap),
181 union_damage.width.get(),
182 );
183 let image_data = result.unwrap();
185
186 for rect in damage {
187 self.canvas
189 .put_image_data(
190 &image_data,
191 union_damage.x.into(),
192 union_damage.y.into(),
193 (rect.x - union_damage.x).into(),
194 (rect.y - union_damage.y).into(),
195 rect.width.get().into(),
196 rect.height.get().into(),
197 )
198 .unwrap();
199 }
200
201 self.buffer_presented = true;
202
203 Ok(())
204 }
205}
206
207impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for WebImpl<D, W> {
208 type Context = WebDisplayImpl<D>;
209 type Buffer<'a>
210 = BufferImpl<'a, D, W>
211 where
212 Self: 'a;
213
214 fn new(window: W, display: &WebDisplayImpl<D>) -> Result<Self, InitError<W>> {
215 let raw = window.window_handle()?.as_raw();
216 let canvas: HtmlCanvasElement = match raw {
217 RawWindowHandle::Web(handle) => {
218 display
219 .document
220 .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id))
221 .unwrap()
223 .swbuf_err("No canvas found with the given id")?
224 .unchecked_into()
226 }
227 RawWindowHandle::WebCanvas(handle) => {
228 let value: &JsValue = unsafe { handle.obj.cast().as_ref() };
229 value.clone().unchecked_into()
230 }
231 RawWindowHandle::WebOffscreenCanvas(handle) => {
232 let value: &JsValue = unsafe { handle.obj.cast().as_ref() };
233 let canvas: OffscreenCanvas = value.clone().unchecked_into();
234
235 return Self::from_offscreen_canvas(canvas, window).map_err(InitError::Failure);
236 }
237 _ => return Err(InitError::Unsupported(window)),
238 };
239
240 Self::from_canvas(canvas, window).map_err(InitError::Failure)
241 }
242
243 #[inline]
245 fn window(&self) -> &W {
246 &self.window_handle
247 }
248
249 fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
252 if self.size != Some((width, height)) {
253 self.buffer_presented = false;
254 self.buffer.resize(total_len(width.get(), height.get()), 0);
255 self.canvas.set_width(width.get());
256 self.canvas.set_height(height.get());
257 self.size = Some((width, height));
258 }
259
260 Ok(())
261 }
262
263 fn buffer_mut(&mut self) -> Result<BufferImpl<'_, D, W>, SoftBufferError> {
264 Ok(BufferImpl { imp: self })
265 }
266
267 fn fetch(&mut self) -> Result<Vec<u32>, SoftBufferError> {
268 let (width, height) = self
269 .size
270 .expect("Must set size of surface before calling `fetch()`");
271
272 let image_data = self
273 .canvas
274 .get_image_data(0., 0., width.get().into(), height.get().into())
275 .ok()
276 .swbuf_err("`Canvas` contains pixels from a different origin")?;
278
279 Ok(image_data
280 .data()
281 .0
282 .chunks_exact(4)
283 .map(|chunk| u32::from_be_bytes([0, chunk[0], chunk[1], chunk[2]]))
284 .collect())
285 }
286}
287
288pub trait SurfaceExtWeb: Sized {
290 fn from_canvas(canvas: HtmlCanvasElement) -> Result<Self, SoftBufferError>;
296
297 fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result<Self, SoftBufferError>;
302}
303
304impl SurfaceExtWeb for crate::Surface<NoDisplayHandle, NoWindowHandle> {
305 fn from_canvas(canvas: HtmlCanvasElement) -> Result<Self, SoftBufferError> {
306 let imple = crate::SurfaceDispatch::Web(WebImpl::from_canvas(canvas, NoWindowHandle(()))?);
307
308 Ok(Self {
309 surface_impl: Box::new(imple),
310 _marker: PhantomData,
311 })
312 }
313
314 fn from_offscreen_canvas(offscreen_canvas: OffscreenCanvas) -> Result<Self, SoftBufferError> {
315 let imple = crate::SurfaceDispatch::Web(WebImpl::from_offscreen_canvas(
316 offscreen_canvas,
317 NoWindowHandle(()),
318 )?);
319
320 Ok(Self {
321 surface_impl: Box::new(imple),
322 _marker: PhantomData,
323 })
324 }
325}
326
327impl Canvas {
328 fn set_width(&self, width: u32) {
329 match self {
330 Self::Canvas { canvas, .. } => canvas.set_width(width),
331 Self::OffscreenCanvas { canvas, .. } => canvas.set_width(width),
332 }
333 }
334
335 fn set_height(&self, height: u32) {
336 match self {
337 Self::Canvas { canvas, .. } => canvas.set_height(height),
338 Self::OffscreenCanvas { canvas, .. } => canvas.set_height(height),
339 }
340 }
341
342 fn get_image_data(&self, sx: f64, sy: f64, sw: f64, sh: f64) -> Result<ImageData, JsValue> {
343 match self {
344 Canvas::Canvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh),
345 Canvas::OffscreenCanvas { ctx, .. } => ctx.get_image_data(sx, sy, sw, sh),
346 }
347 }
348
349 #[allow(clippy::too_many_arguments)]
352 fn put_image_data(
353 &self,
354 imagedata: &ImageData,
355 dx: f64,
356 dy: f64,
357 dirty_x: f64,
358 dirty_y: f64,
359 width: f64,
360 height: f64,
361 ) -> Result<(), JsValue> {
362 match self {
363 Self::Canvas { ctx, .. } => ctx
364 .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height(
365 imagedata, dx, dy, dirty_x, dirty_y, width, height,
366 ),
367 Self::OffscreenCanvas { ctx, .. } => ctx
368 .put_image_data_with_dirty_x_and_dirty_y_and_dirty_width_and_dirty_height(
369 imagedata, dx, dy, dirty_x, dirty_y, width, height,
370 ),
371 }
372 }
373}
374
375pub struct BufferImpl<'a, D, W> {
376 imp: &'a mut WebImpl<D, W>,
377}
378
379impl<D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferImpl<'_, D, W> {
380 fn pixels(&self) -> &[u32] {
381 &self.imp.buffer
382 }
383
384 fn pixels_mut(&mut self) -> &mut [u32] {
385 &mut self.imp.buffer
386 }
387
388 fn age(&self) -> u8 {
389 if self.imp.buffer_presented {
390 1
391 } else {
392 0
393 }
394 }
395
396 fn present(self) -> Result<(), SoftBufferError> {
398 let (width, height) = self
399 .imp
400 .size
401 .expect("Must set size of surface before calling `present()`");
402 self.imp.present_with_damage(&[Rect {
403 x: 0,
404 y: 0,
405 width,
406 height,
407 }])
408 }
409
410 fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> {
411 self.imp.present_with_damage(damage)
412 }
413}
414
415#[inline(always)]
416fn total_len(width: u32, height: u32) -> usize {
417 width
419 .try_into()
420 .ok()
421 .and_then(|w: usize| height.try_into().ok().and_then(|h| w.checked_mul(h)))
422 .unwrap_or_else(|| {
423 panic!(
424 "Overflow when calculating total length of buffer: {}x{}",
425 width, height
426 );
427 })
428}