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            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}