window.rs

  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}