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