1use crate::display::WebDisplay;
2use crate::events::{ClickState, WebEventListeners, is_mac_platform};
3use std::sync::Arc;
4use std::{cell::Cell, cell::RefCell, rc::Rc};
5
6use gpui::{
7 AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
8 Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
9 PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
10 ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
11 WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
12};
13use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
14use wasm_bindgen::prelude::*;
15
16#[derive(Default)]
17pub(crate) struct WebWindowCallbacks {
18 pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
19 pub(crate) input: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
20 pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
21 pub(crate) hover_status_change: Option<Box<dyn FnMut(bool)>>,
22 pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
23 pub(crate) moved: Option<Box<dyn FnMut()>>,
24 pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
25 pub(crate) close: Option<Box<dyn FnOnce()>>,
26 pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
27 pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
28}
29
30pub(crate) struct WebWindowMutableState {
31 pub(crate) renderer: WgpuRenderer,
32 pub(crate) bounds: Bounds<Pixels>,
33 pub(crate) scale_factor: f32,
34 pub(crate) max_texture_dimension: u32,
35 pub(crate) title: String,
36 pub(crate) input_handler: Option<PlatformInputHandler>,
37 pub(crate) is_fullscreen: bool,
38 pub(crate) is_active: bool,
39 pub(crate) is_hovered: bool,
40 pub(crate) mouse_position: Point<Pixels>,
41 pub(crate) modifiers: Modifiers,
42 pub(crate) capslock: Capslock,
43}
44
45pub(crate) struct WebWindowInner {
46 pub(crate) browser_window: web_sys::Window,
47 pub(crate) canvas: web_sys::HtmlCanvasElement,
48 pub(crate) input_element: web_sys::HtmlInputElement,
49 pub(crate) has_device_pixel_support: bool,
50 pub(crate) is_mac: bool,
51 pub(crate) state: RefCell<WebWindowMutableState>,
52 pub(crate) callbacks: RefCell<WebWindowCallbacks>,
53 pub(crate) click_state: RefCell<ClickState>,
54 pub(crate) pressed_button: Cell<Option<MouseButton>>,
55 pub(crate) last_physical_size: Cell<(u32, u32)>,
56 pub(crate) notify_scale: Cell<bool>,
57 pub(crate) is_composing: Cell<bool>,
58 mql_handle: RefCell<Option<MqlHandle>>,
59 pending_physical_size: Cell<Option<(u32, u32)>>,
60}
61
62pub struct WebWindow {
63 inner: Rc<WebWindowInner>,
64 display: Rc<dyn PlatformDisplay>,
65 #[allow(dead_code)]
66 handle: AnyWindowHandle,
67 _raf_closure: Closure<dyn FnMut()>,
68 _resize_observer: Option<web_sys::ResizeObserver>,
69 _resize_observer_closure: Closure<dyn FnMut(js_sys::Array)>,
70 _event_listeners: WebEventListeners,
71}
72
73impl WebWindow {
74 pub fn new(
75 handle: AnyWindowHandle,
76 _params: WindowParams,
77 context: &WgpuContext,
78 browser_window: web_sys::Window,
79 ) -> anyhow::Result<Self> {
80 let document = browser_window
81 .document()
82 .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?;
83
84 let canvas: web_sys::HtmlCanvasElement = document
85 .create_element("canvas")
86 .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))?
87 .dyn_into()
88 .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?;
89
90 let dpr = browser_window.device_pixel_ratio() as f32;
91 let max_texture_dimension = context.device.limits().max_texture_dimension_2d;
92 let has_device_pixel_support = check_device_pixel_support();
93
94 canvas.set_tab_index(-1);
95
96 let style = canvas.style();
97 style
98 .set_property("width", "100%")
99 .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?;
100 style
101 .set_property("height", "100%")
102 .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?;
103 style
104 .set_property("display", "block")
105 .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?;
106 style
107 .set_property("outline", "none")
108 .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?;
109 style
110 .set_property("touch-action", "none")
111 .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?;
112
113 let body = document
114 .body()
115 .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?;
116 body.append_child(&canvas)
117 .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?;
118
119 let input_element: web_sys::HtmlInputElement = document
120 .create_element("input")
121 .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))?
122 .dyn_into()
123 .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?;
124 let input_style = input_element.style();
125 input_style.set_property("position", "fixed").ok();
126 input_style.set_property("top", "0").ok();
127 input_style.set_property("left", "0").ok();
128 input_style.set_property("width", "1px").ok();
129 input_style.set_property("height", "1px").ok();
130 input_style.set_property("opacity", "0").ok();
131 body.append_child(&input_element)
132 .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?;
133 input_element.focus().ok();
134
135 let device_size = Size {
136 width: DevicePixels(0),
137 height: DevicePixels(0),
138 };
139
140 let renderer_config = WgpuSurfaceConfig {
141 size: device_size,
142 transparent: false,
143 preferred_present_mode: None,
144 };
145
146 let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?;
147
148 let display: Rc<dyn PlatformDisplay> = Rc::new(WebDisplay::new(browser_window.clone()));
149
150 let initial_bounds = Bounds {
151 origin: Point::default(),
152 size: Size::default(),
153 };
154
155 let mutable_state = WebWindowMutableState {
156 renderer,
157 bounds: initial_bounds,
158 scale_factor: dpr,
159 max_texture_dimension,
160 title: String::new(),
161 input_handler: None,
162 is_fullscreen: false,
163 is_active: true,
164 is_hovered: false,
165 mouse_position: Point::default(),
166 modifiers: Modifiers::default(),
167 capslock: Capslock::default(),
168 };
169
170 let is_mac = is_mac_platform(&browser_window);
171
172 let inner = Rc::new(WebWindowInner {
173 browser_window,
174 canvas,
175 input_element,
176 has_device_pixel_support,
177 is_mac,
178 state: RefCell::new(mutable_state),
179 callbacks: RefCell::new(WebWindowCallbacks::default()),
180 click_state: RefCell::new(ClickState::default()),
181 pressed_button: Cell::new(None),
182 last_physical_size: Cell::new((0, 0)),
183 notify_scale: Cell::new(false),
184 is_composing: Cell::new(false),
185 mql_handle: RefCell::new(None),
186 pending_physical_size: Cell::new(None),
187 });
188
189 let raf_closure = inner.create_raf_closure();
190 inner.schedule_raf(&raf_closure);
191
192 let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner));
193 let resize_observer =
194 web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok();
195
196 if let Some(ref observer) = resize_observer {
197 inner.observe_canvas(observer);
198 inner.watch_dpr_changes(observer);
199 }
200
201 let event_listeners = inner.register_event_listeners();
202
203 Ok(Self {
204 inner,
205 display,
206 handle,
207 _raf_closure: raf_closure,
208 _resize_observer: resize_observer,
209 _resize_observer_closure: resize_observer_closure,
210 _event_listeners: event_listeners,
211 })
212 }
213
214 fn create_resize_observer_closure(
215 inner: Rc<WebWindowInner>,
216 ) -> Closure<dyn FnMut(js_sys::Array)> {
217 Closure::new(move |entries: js_sys::Array| {
218 let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() {
219 Some(entry) => entry,
220 None => return,
221 };
222
223 let dpr = inner.browser_window.device_pixel_ratio();
224 let dpr_f32 = dpr as f32;
225
226 let (physical_width, physical_height, logical_width, logical_height) =
227 if inner.has_device_pixel_support {
228 let size: web_sys::ResizeObserverSize = entry
229 .device_pixel_content_box_size()
230 .get(0)
231 .unchecked_into();
232 let pw = size.inline_size() as u32;
233 let ph = size.block_size() as u32;
234 let lw = pw as f64 / dpr;
235 let lh = ph as f64 / dpr;
236 (pw, ph, lw as f32, lh as f32)
237 } else {
238 // Safari fallback: use contentRect (always CSS px).
239 let rect = entry.content_rect();
240 let lw = rect.width() as f32;
241 let lh = rect.height() as f32;
242 let pw = (lw as f64 * dpr).round() as u32;
243 let ph = (lh as f64 * dpr).round() as u32;
244 (pw, ph, lw, lh)
245 };
246
247 let scale_changed = inner.notify_scale.replace(false);
248 let prev = inner.last_physical_size.get();
249 let size_changed = prev != (physical_width, physical_height);
250
251 if !scale_changed && !size_changed {
252 return;
253 }
254 inner
255 .last_physical_size
256 .set((physical_width, physical_height));
257
258 // Skip rendering to a zero-size canvas (e.g. display:none).
259 if physical_width == 0 || physical_height == 0 {
260 let mut s = inner.state.borrow_mut();
261 s.bounds.size = Size::default();
262 s.scale_factor = dpr_f32;
263 // Still fire the callback so GPUI knows the window is gone.
264 drop(s);
265 let mut cbs = inner.callbacks.borrow_mut();
266 if let Some(ref mut callback) = cbs.resize {
267 callback(Size::default(), dpr_f32);
268 }
269 return;
270 }
271
272 let max_texture_dimension = inner.state.borrow().max_texture_dimension;
273 let clamped_width = physical_width.min(max_texture_dimension);
274 let clamped_height = physical_height.min(max_texture_dimension);
275
276 inner
277 .pending_physical_size
278 .set(Some((clamped_width, clamped_height)));
279
280 {
281 let mut s = inner.state.borrow_mut();
282 s.bounds.size = Size {
283 width: px(logical_width),
284 height: px(logical_height),
285 };
286 s.scale_factor = dpr_f32;
287 }
288
289 let new_size = Size {
290 width: px(logical_width),
291 height: px(logical_height),
292 };
293
294 let mut cbs = inner.callbacks.borrow_mut();
295 if let Some(ref mut callback) = cbs.resize {
296 callback(new_size, dpr_f32);
297 }
298 })
299 }
300}
301
302impl WebWindowInner {
303 fn create_raf_closure(self: &Rc<Self>) -> Closure<dyn FnMut()> {
304 let raf_handle: Rc<RefCell<Option<js_sys::Function>>> = Rc::new(RefCell::new(None));
305 let raf_handle_inner = Rc::clone(&raf_handle);
306
307 let this = Rc::clone(self);
308 let closure = Closure::new(move || {
309 {
310 let mut callbacks = this.callbacks.borrow_mut();
311 if let Some(ref mut callback) = callbacks.request_frame {
312 callback(RequestFrameOptions {
313 require_presentation: true,
314 force_render: false,
315 });
316 }
317 }
318
319 // Re-schedule for the next frame
320 if let Some(ref func) = *raf_handle_inner.borrow() {
321 this.browser_window.request_animation_frame(func).ok();
322 }
323 });
324
325 let js_func: js_sys::Function =
326 closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
327 *raf_handle.borrow_mut() = Some(js_func);
328
329 closure
330 }
331
332 fn schedule_raf(&self, closure: &Closure<dyn FnMut()>) {
333 self.browser_window
334 .request_animation_frame(closure.as_ref().unchecked_ref())
335 .ok();
336 }
337
338 fn observe_canvas(&self, observer: &web_sys::ResizeObserver) {
339 observer.unobserve(&self.canvas);
340 if self.has_device_pixel_support {
341 let options = web_sys::ResizeObserverOptions::new();
342 options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox);
343 observer.observe_with_options(&self.canvas, &options);
344 } else {
345 observer.observe(&self.canvas);
346 }
347 }
348
349 fn watch_dpr_changes(self: &Rc<Self>, observer: &web_sys::ResizeObserver) {
350 let current_dpr = self.browser_window.device_pixel_ratio();
351 let media_query =
352 format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})");
353 let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else {
354 return;
355 };
356
357 let this = Rc::clone(self);
358 let observer = observer.clone();
359
360 let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
361 this.notify_scale.set(true);
362 this.observe_canvas(&observer);
363 this.watch_dpr_changes(&observer);
364 });
365
366 mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
367 .ok();
368
369 *self.mql_handle.borrow_mut() = Some(MqlHandle {
370 mql,
371 _closure: closure,
372 });
373 }
374
375 pub(crate) fn register_visibility_change(
376 self: &Rc<Self>,
377 ) -> Option<Closure<dyn FnMut(JsValue)>> {
378 let document = self.browser_window.document()?;
379 let this = Rc::clone(self);
380
381 let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
382 let is_visible = this
383 .browser_window
384 .document()
385 .map(|doc| {
386 let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into())
387 .ok()
388 .and_then(|v| v.as_string())
389 .unwrap_or_default();
390 state_str == "visible"
391 })
392 .unwrap_or(true);
393
394 {
395 let mut state = this.state.borrow_mut();
396 state.is_active = is_visible;
397 }
398 let mut callbacks = this.callbacks.borrow_mut();
399 if let Some(ref mut callback) = callbacks.active_status_change {
400 callback(is_visible);
401 }
402 });
403
404 document
405 .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref())
406 .ok();
407
408 Some(closure)
409 }
410
411 pub(crate) fn with_input_handler<R>(
412 &self,
413 f: impl FnOnce(&mut PlatformInputHandler) -> R,
414 ) -> Option<R> {
415 let mut handler = self.state.borrow_mut().input_handler.take()?;
416 let result = f(&mut handler);
417 self.state.borrow_mut().input_handler = Some(handler);
418 Some(result)
419 }
420
421 pub(crate) fn register_appearance_change(
422 self: &Rc<Self>,
423 ) -> Option<Closure<dyn FnMut(JsValue)>> {
424 let mql = self
425 .browser_window
426 .match_media("(prefers-color-scheme: dark)")
427 .ok()??;
428
429 let this = Rc::clone(self);
430 let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
431 let mut callbacks = this.callbacks.borrow_mut();
432 if let Some(ref mut callback) = callbacks.appearance_changed {
433 callback();
434 }
435 });
436
437 mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
438 .ok();
439
440 Some(closure)
441 }
442}
443
444fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance {
445 let is_dark = browser_window
446 .match_media("(prefers-color-scheme: dark)")
447 .ok()
448 .flatten()
449 .map(|mql| mql.matches())
450 .unwrap_or(false);
451
452 if is_dark {
453 WindowAppearance::Dark
454 } else {
455 WindowAppearance::Light
456 }
457}
458
459struct MqlHandle {
460 mql: web_sys::MediaQueryList,
461 _closure: Closure<dyn FnMut(JsValue)>,
462}
463
464impl Drop for MqlHandle {
465 fn drop(&mut self) {
466 self.mql
467 .remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref())
468 .ok();
469 }
470}
471
472// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available.
473fn check_device_pixel_support() -> bool {
474 let global: JsValue = js_sys::global().into();
475 let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else {
476 return false;
477 };
478 let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else {
479 return false;
480 };
481 let descriptor = js_sys::Object::get_own_property_descriptor(
482 &prototype.unchecked_into::<js_sys::Object>(),
483 &"devicePixelContentBoxSize".into(),
484 );
485 !descriptor.is_undefined()
486}
487
488impl raw_window_handle::HasWindowHandle for WebWindow {
489 fn window_handle(
490 &self,
491 ) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
492 let canvas_ref: &JsValue = self.inner.canvas.as_ref();
493 let obj = std::ptr::NonNull::from(canvas_ref).cast::<std::ffi::c_void>();
494 let handle = raw_window_handle::WebCanvasWindowHandle::new(obj);
495 Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) })
496 }
497}
498
499impl raw_window_handle::HasDisplayHandle for WebWindow {
500 fn display_handle(
501 &self,
502 ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
503 Ok(raw_window_handle::DisplayHandle::web())
504 }
505}
506
507impl PlatformWindow for WebWindow {
508 fn bounds(&self) -> Bounds<Pixels> {
509 self.inner.state.borrow().bounds
510 }
511
512 fn is_maximized(&self) -> bool {
513 false
514 }
515
516 fn window_bounds(&self) -> WindowBounds {
517 WindowBounds::Windowed(self.bounds())
518 }
519
520 fn content_size(&self) -> Size<Pixels> {
521 self.inner.state.borrow().bounds.size
522 }
523
524 fn resize(&mut self, size: Size<Pixels>) {
525 let style = self.inner.canvas.style();
526 style
527 .set_property("width", &format!("{}px", f32::from(size.width)))
528 .ok();
529 style
530 .set_property("height", &format!("{}px", f32::from(size.height)))
531 .ok();
532 }
533
534 fn scale_factor(&self) -> f32 {
535 self.inner.state.borrow().scale_factor
536 }
537
538 fn appearance(&self) -> WindowAppearance {
539 current_appearance(&self.inner.browser_window)
540 }
541
542 fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
543 Some(self.display.clone())
544 }
545
546 fn mouse_position(&self) -> Point<Pixels> {
547 self.inner.state.borrow().mouse_position
548 }
549
550 fn modifiers(&self) -> Modifiers {
551 self.inner.state.borrow().modifiers
552 }
553
554 fn capslock(&self) -> Capslock {
555 self.inner.state.borrow().capslock
556 }
557
558 fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
559 self.inner.state.borrow_mut().input_handler = Some(input_handler);
560 }
561
562 fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
563 self.inner.state.borrow_mut().input_handler.take()
564 }
565
566 fn prompt(
567 &self,
568 _level: PromptLevel,
569 _msg: &str,
570 _detail: Option<&str>,
571 _answers: &[PromptButton],
572 ) -> Option<futures::channel::oneshot::Receiver<usize>> {
573 None
574 }
575
576 fn activate(&self) {
577 self.inner.state.borrow_mut().is_active = true;
578 }
579
580 fn is_active(&self) -> bool {
581 self.inner.state.borrow().is_active
582 }
583
584 fn is_hovered(&self) -> bool {
585 self.inner.state.borrow().is_hovered
586 }
587
588 fn background_appearance(&self) -> WindowBackgroundAppearance {
589 WindowBackgroundAppearance::Opaque
590 }
591
592 fn set_title(&mut self, title: &str) {
593 self.inner.state.borrow_mut().title = title.to_owned();
594 if let Some(document) = self.inner.browser_window.document() {
595 document.set_title(title);
596 }
597 }
598
599 fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
600
601 fn minimize(&self) {
602 log::warn!("WebWindow::minimize is not supported in the browser");
603 }
604
605 fn zoom(&self) {
606 log::warn!("WebWindow::zoom is not supported in the browser");
607 }
608
609 fn toggle_fullscreen(&self) {
610 let mut state = self.inner.state.borrow_mut();
611 state.is_fullscreen = !state.is_fullscreen;
612
613 if state.is_fullscreen {
614 let canvas: &web_sys::Element = self.inner.canvas.as_ref();
615 canvas.request_fullscreen().ok();
616 } else {
617 if let Some(document) = self.inner.browser_window.document() {
618 document.exit_fullscreen();
619 }
620 }
621 }
622
623 fn is_fullscreen(&self) -> bool {
624 self.inner.state.borrow().is_fullscreen
625 }
626
627 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
628 self.inner.callbacks.borrow_mut().request_frame = Some(callback);
629 }
630
631 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
632 self.inner.callbacks.borrow_mut().input = Some(callback);
633 }
634
635 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
636 self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
637 }
638
639 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
640 self.inner.callbacks.borrow_mut().hover_status_change = Some(callback);
641 }
642
643 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
644 self.inner.callbacks.borrow_mut().resize = Some(callback);
645 }
646
647 fn on_moved(&self, callback: Box<dyn FnMut()>) {
648 self.inner.callbacks.borrow_mut().moved = Some(callback);
649 }
650
651 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
652 self.inner.callbacks.borrow_mut().should_close = Some(callback);
653 }
654
655 fn on_close(&self, callback: Box<dyn FnOnce()>) {
656 self.inner.callbacks.borrow_mut().close = Some(callback);
657 }
658
659 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
660 self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback);
661 }
662
663 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
664 self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
665 }
666
667 fn draw(&self, scene: &Scene) {
668 if let Some((width, height)) = self.inner.pending_physical_size.take() {
669 if self.inner.canvas.width() != width || self.inner.canvas.height() != height {
670 self.inner.canvas.set_width(width);
671 self.inner.canvas.set_height(height);
672 }
673
674 let mut state = self.inner.state.borrow_mut();
675 state.renderer.update_drawable_size(Size {
676 width: DevicePixels(width as i32),
677 height: DevicePixels(height as i32),
678 });
679 drop(state);
680 }
681
682 self.inner.state.borrow_mut().renderer.draw(scene);
683 }
684
685 fn completed_frame(&self) {
686 // On web, presentation happens automatically via wgpu surface present
687 }
688
689 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
690 self.inner.state.borrow().renderer.sprite_atlas().clone()
691 }
692
693 fn is_subpixel_rendering_supported(&self) -> bool {
694 self.inner
695 .state
696 .borrow()
697 .renderer
698 .supports_dual_source_blending()
699 }
700
701 fn gpu_specs(&self) -> Option<GpuSpecs> {
702 Some(self.inner.state.borrow().renderer.gpu_specs())
703 }
704
705 fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
706
707 fn request_decorations(&self, _decorations: WindowDecorations) {}
708
709 fn show_window_menu(&self, _position: Point<Pixels>) {}
710
711 fn start_window_move(&self) {}
712
713 fn start_window_resize(&self, _edge: ResizeEdge) {}
714
715 fn window_decorations(&self) -> Decorations {
716 Decorations::Server
717 }
718
719 fn set_app_id(&mut self, _app_id: &str) {}
720
721 fn window_controls(&self) -> WindowControls {
722 WindowControls {
723 fullscreen: true,
724 maximize: false,
725 minimize: false,
726 window_menu: false,
727 }
728 }
729
730 fn set_client_inset(&self, _inset: Pixels) {}
731}