Files
zed/crates/gpui_web/src/window.rs
T
Balamurali Pandranki ef42f9db2d gpui_wgpu: Add surface lifecycle methods for mobile platforms (#50815)
## Summary

- Add `unconfigure_surface()` and `replace_surface()` methods to
`WgpuRenderer` for mobile platform window lifecycle management
- Prefer `PresentMode::Mailbox` (triple-buffering) over `Fifo` to avoid
blocking during lifecycle transitions
- Early return in `draw()` when surface is unconfigured to prevent
driver hangs

## Motivation

On Android, the native window (`ANativeWindow`) is destroyed when the
app goes to the background and recreated when it returns to the
foreground. The same happens during orientation changes. Without surface
lifecycle methods, the only option is to destroy the entire
`WgpuRenderer` and create a new one on resume.

The problem: GPUI's scene cache holds `AtlasTextureId` references from
the old renderer's atlas. A new renderer has an empty atlas, so those
cached IDs cause index-out-of-bounds panics.

The fix: Keep the renderer (device, queue, atlas, pipelines) alive
across surface destruction. Only the wgpu `Surface` needs to be
replaced.

### `unconfigure_surface()`
Marks the surface as unconfigured so `draw()` skips rendering via the
existing `surface_configured` guard. Drops intermediate textures that
reference the old surface dimensions. The renderer stays fully alive.

### `replace_surface()`
Creates a new `wgpu::Surface` from fresh window handles using the
**same** `wgpu::Instance` that created the original adapter/device.
Reconfigures the surface and marks it as configured so rendering
resumes. All cached atlas textures remain valid.

### PresentMode::Mailbox
`Fifo` (VSync) blocks in `get_current_texture()` and can deadlock if the
compositor is frozen during a lifecycle transition (e.g.
`TerminateWindow` → `InitWindow` on Android). Mailbox (triple-buffering)
avoids this. Falls back to `AutoNoVsync` → `Fifo` if unsupported.

### draw() early return
Some drivers (notably Adreno) block indefinitely when acquiring a
texture from an unconfigured surface. The early return prevents this.

## Context

This is needed by
[gpui-mobile](https://github.com/itsbalamurali/gpui-mobile), a project
bringing GPUI to Android and iOS. The Android implementation needs these
methods to handle:

1. **Background/foreground transitions** — `TerminateWindow` destroys
the native window, `InitWindow` recreates it
2. **Orientation changes** — Surface is destroyed and recreated with new
dimensions
3. **Split-screen transitions** — Similar surface recreation

Without this change, we maintain a local fork of `gpui_wgpu` with just
these additions. Upstreaming them would let mobile platform
implementations use the official crate directly.

## Test plan

- [x] Tested on Android (Motorola, Adreno 720 GPU) — 3 consecutive
background/foreground cycles, zero panics, atlas textures preserved
- [x] Tested orientation changes (portrait→landscape→portrait) — surface
replacement completed in <40ms per rotation
- [x] Verified `draw()` correctly skips rendering when surface is
unconfigured
- [x] Verified no regression on desktop — methods are additive, existing
code paths unchanged
- [x] PresentMode fallback chain works on devices that don't support
Mailbox

## Release Notes

- N/A
2026-03-31 13:16:53 +00:00

732 lines
26 KiB
Rust

use crate::display::WebDisplay;
use crate::events::{ClickState, WebEventListeners, is_mac_platform};
use std::sync::Arc;
use std::{cell::Cell, cell::RefCell, rc::Rc};
use gpui::{
AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
use wasm_bindgen::prelude::*;
#[derive(Default)]
pub(crate) struct WebWindowCallbacks {
pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
pub(crate) input: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) hover_status_change: Option<Box<dyn FnMut(bool)>>,
pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
pub(crate) moved: Option<Box<dyn FnMut()>>,
pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
pub(crate) close: Option<Box<dyn FnOnce()>>,
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
}
pub(crate) struct WebWindowMutableState {
pub(crate) renderer: WgpuRenderer,
pub(crate) bounds: Bounds<Pixels>,
pub(crate) scale_factor: f32,
pub(crate) max_texture_dimension: u32,
pub(crate) title: String,
pub(crate) input_handler: Option<PlatformInputHandler>,
pub(crate) is_fullscreen: bool,
pub(crate) is_active: bool,
pub(crate) is_hovered: bool,
pub(crate) mouse_position: Point<Pixels>,
pub(crate) modifiers: Modifiers,
pub(crate) capslock: Capslock,
}
pub(crate) struct WebWindowInner {
pub(crate) browser_window: web_sys::Window,
pub(crate) canvas: web_sys::HtmlCanvasElement,
pub(crate) input_element: web_sys::HtmlInputElement,
pub(crate) has_device_pixel_support: bool,
pub(crate) is_mac: bool,
pub(crate) state: RefCell<WebWindowMutableState>,
pub(crate) callbacks: RefCell<WebWindowCallbacks>,
pub(crate) click_state: RefCell<ClickState>,
pub(crate) pressed_button: Cell<Option<MouseButton>>,
pub(crate) last_physical_size: Cell<(u32, u32)>,
pub(crate) notify_scale: Cell<bool>,
pub(crate) is_composing: Cell<bool>,
mql_handle: RefCell<Option<MqlHandle>>,
pending_physical_size: Cell<Option<(u32, u32)>>,
}
pub struct WebWindow {
inner: Rc<WebWindowInner>,
display: Rc<dyn PlatformDisplay>,
#[allow(dead_code)]
handle: AnyWindowHandle,
_raf_closure: Closure<dyn FnMut()>,
_resize_observer: Option<web_sys::ResizeObserver>,
_resize_observer_closure: Closure<dyn FnMut(js_sys::Array)>,
_event_listeners: WebEventListeners,
}
impl WebWindow {
pub fn new(
handle: AnyWindowHandle,
_params: WindowParams,
context: &WgpuContext,
browser_window: web_sys::Window,
) -> anyhow::Result<Self> {
let document = browser_window
.document()
.ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?;
let canvas: web_sys::HtmlCanvasElement = document
.create_element("canvas")
.map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))?
.dyn_into()
.map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?;
let dpr = browser_window.device_pixel_ratio() as f32;
let max_texture_dimension = context.device.limits().max_texture_dimension_2d;
let has_device_pixel_support = check_device_pixel_support();
canvas.set_tab_index(-1);
let style = canvas.style();
style
.set_property("width", "100%")
.map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?;
style
.set_property("height", "100%")
.map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?;
style
.set_property("display", "block")
.map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?;
style
.set_property("outline", "none")
.map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?;
style
.set_property("touch-action", "none")
.map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?;
let body = document
.body()
.ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?;
body.append_child(&canvas)
.map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?;
let input_element: web_sys::HtmlInputElement = document
.create_element("input")
.map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))?
.dyn_into()
.map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?;
let input_style = input_element.style();
input_style.set_property("position", "fixed").ok();
input_style.set_property("top", "0").ok();
input_style.set_property("left", "0").ok();
input_style.set_property("width", "1px").ok();
input_style.set_property("height", "1px").ok();
input_style.set_property("opacity", "0").ok();
body.append_child(&input_element)
.map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?;
input_element.focus().ok();
let device_size = Size {
width: DevicePixels(0),
height: DevicePixels(0),
};
let renderer_config = WgpuSurfaceConfig {
size: device_size,
transparent: false,
preferred_present_mode: None,
};
let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?;
let display: Rc<dyn PlatformDisplay> = Rc::new(WebDisplay::new(browser_window.clone()));
let initial_bounds = Bounds {
origin: Point::default(),
size: Size::default(),
};
let mutable_state = WebWindowMutableState {
renderer,
bounds: initial_bounds,
scale_factor: dpr,
max_texture_dimension,
title: String::new(),
input_handler: None,
is_fullscreen: false,
is_active: true,
is_hovered: false,
mouse_position: Point::default(),
modifiers: Modifiers::default(),
capslock: Capslock::default(),
};
let is_mac = is_mac_platform(&browser_window);
let inner = Rc::new(WebWindowInner {
browser_window,
canvas,
input_element,
has_device_pixel_support,
is_mac,
state: RefCell::new(mutable_state),
callbacks: RefCell::new(WebWindowCallbacks::default()),
click_state: RefCell::new(ClickState::default()),
pressed_button: Cell::new(None),
last_physical_size: Cell::new((0, 0)),
notify_scale: Cell::new(false),
is_composing: Cell::new(false),
mql_handle: RefCell::new(None),
pending_physical_size: Cell::new(None),
});
let raf_closure = inner.create_raf_closure();
inner.schedule_raf(&raf_closure);
let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner));
let resize_observer =
web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok();
if let Some(ref observer) = resize_observer {
inner.observe_canvas(observer);
inner.watch_dpr_changes(observer);
}
let event_listeners = inner.register_event_listeners();
Ok(Self {
inner,
display,
handle,
_raf_closure: raf_closure,
_resize_observer: resize_observer,
_resize_observer_closure: resize_observer_closure,
_event_listeners: event_listeners,
})
}
fn create_resize_observer_closure(
inner: Rc<WebWindowInner>,
) -> Closure<dyn FnMut(js_sys::Array)> {
Closure::new(move |entries: js_sys::Array| {
let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() {
Some(entry) => entry,
None => return,
};
let dpr = inner.browser_window.device_pixel_ratio();
let dpr_f32 = dpr as f32;
let (physical_width, physical_height, logical_width, logical_height) =
if inner.has_device_pixel_support {
let size: web_sys::ResizeObserverSize = entry
.device_pixel_content_box_size()
.get(0)
.unchecked_into();
let pw = size.inline_size() as u32;
let ph = size.block_size() as u32;
let lw = pw as f64 / dpr;
let lh = ph as f64 / dpr;
(pw, ph, lw as f32, lh as f32)
} else {
// Safari fallback: use contentRect (always CSS px).
let rect = entry.content_rect();
let lw = rect.width() as f32;
let lh = rect.height() as f32;
let pw = (lw as f64 * dpr).round() as u32;
let ph = (lh as f64 * dpr).round() as u32;
(pw, ph, lw, lh)
};
let scale_changed = inner.notify_scale.replace(false);
let prev = inner.last_physical_size.get();
let size_changed = prev != (physical_width, physical_height);
if !scale_changed && !size_changed {
return;
}
inner
.last_physical_size
.set((physical_width, physical_height));
// Skip rendering to a zero-size canvas (e.g. display:none).
if physical_width == 0 || physical_height == 0 {
let mut s = inner.state.borrow_mut();
s.bounds.size = Size::default();
s.scale_factor = dpr_f32;
// Still fire the callback so GPUI knows the window is gone.
drop(s);
let mut cbs = inner.callbacks.borrow_mut();
if let Some(ref mut callback) = cbs.resize {
callback(Size::default(), dpr_f32);
}
return;
}
let max_texture_dimension = inner.state.borrow().max_texture_dimension;
let clamped_width = physical_width.min(max_texture_dimension);
let clamped_height = physical_height.min(max_texture_dimension);
inner
.pending_physical_size
.set(Some((clamped_width, clamped_height)));
{
let mut s = inner.state.borrow_mut();
s.bounds.size = Size {
width: px(logical_width),
height: px(logical_height),
};
s.scale_factor = dpr_f32;
}
let new_size = Size {
width: px(logical_width),
height: px(logical_height),
};
let mut cbs = inner.callbacks.borrow_mut();
if let Some(ref mut callback) = cbs.resize {
callback(new_size, dpr_f32);
}
})
}
}
impl WebWindowInner {
fn create_raf_closure(self: &Rc<Self>) -> Closure<dyn FnMut()> {
let raf_handle: Rc<RefCell<Option<js_sys::Function>>> = Rc::new(RefCell::new(None));
let raf_handle_inner = Rc::clone(&raf_handle);
let this = Rc::clone(self);
let closure = Closure::new(move || {
{
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.request_frame {
callback(RequestFrameOptions {
require_presentation: true,
force_render: false,
});
}
}
// Re-schedule for the next frame
if let Some(ref func) = *raf_handle_inner.borrow() {
this.browser_window.request_animation_frame(func).ok();
}
});
let js_func: js_sys::Function =
closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
*raf_handle.borrow_mut() = Some(js_func);
closure
}
fn schedule_raf(&self, closure: &Closure<dyn FnMut()>) {
self.browser_window
.request_animation_frame(closure.as_ref().unchecked_ref())
.ok();
}
fn observe_canvas(&self, observer: &web_sys::ResizeObserver) {
observer.unobserve(&self.canvas);
if self.has_device_pixel_support {
let options = web_sys::ResizeObserverOptions::new();
options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox);
observer.observe_with_options(&self.canvas, &options);
} else {
observer.observe(&self.canvas);
}
}
fn watch_dpr_changes(self: &Rc<Self>, observer: &web_sys::ResizeObserver) {
let current_dpr = self.browser_window.device_pixel_ratio();
let media_query =
format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})");
let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else {
return;
};
let this = Rc::clone(self);
let observer = observer.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
this.notify_scale.set(true);
this.observe_canvas(&observer);
this.watch_dpr_changes(&observer);
});
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
.ok();
*self.mql_handle.borrow_mut() = Some(MqlHandle {
mql,
_closure: closure,
});
}
pub(crate) fn register_visibility_change(
self: &Rc<Self>,
) -> Option<Closure<dyn FnMut(JsValue)>> {
let document = self.browser_window.document()?;
let this = Rc::clone(self);
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
let is_visible = this
.browser_window
.document()
.map(|doc| {
let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into())
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
state_str == "visible"
})
.unwrap_or(true);
{
let mut state = this.state.borrow_mut();
state.is_active = is_visible;
}
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.active_status_change {
callback(is_visible);
}
});
document
.add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref())
.ok();
Some(closure)
}
pub(crate) fn with_input_handler<R>(
&self,
f: impl FnOnce(&mut PlatformInputHandler) -> R,
) -> Option<R> {
let mut handler = self.state.borrow_mut().input_handler.take()?;
let result = f(&mut handler);
self.state.borrow_mut().input_handler = Some(handler);
Some(result)
}
pub(crate) fn register_appearance_change(
self: &Rc<Self>,
) -> Option<Closure<dyn FnMut(JsValue)>> {
let mql = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
.ok()??;
let this = Rc::clone(self);
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
let mut callbacks = this.callbacks.borrow_mut();
if let Some(ref mut callback) = callbacks.appearance_changed {
callback();
}
});
mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref())
.ok();
Some(closure)
}
}
fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance {
let is_dark = browser_window
.match_media("(prefers-color-scheme: dark)")
.ok()
.flatten()
.map(|mql| mql.matches())
.unwrap_or(false);
if is_dark {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
struct MqlHandle {
mql: web_sys::MediaQueryList,
_closure: Closure<dyn FnMut(JsValue)>,
}
impl Drop for MqlHandle {
fn drop(&mut self) {
self.mql
.remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref())
.ok();
}
}
// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available.
fn check_device_pixel_support() -> bool {
let global: JsValue = js_sys::global().into();
let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else {
return false;
};
let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else {
return false;
};
let descriptor = js_sys::Object::get_own_property_descriptor(
&prototype.unchecked_into::<js_sys::Object>(),
&"devicePixelContentBoxSize".into(),
);
!descriptor.is_undefined()
}
impl raw_window_handle::HasWindowHandle for WebWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
let canvas_ref: &JsValue = self.inner.canvas.as_ref();
let obj = std::ptr::NonNull::from(canvas_ref).cast::<std::ffi::c_void>();
let handle = raw_window_handle::WebCanvasWindowHandle::new(obj);
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) })
}
}
impl raw_window_handle::HasDisplayHandle for WebWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
Ok(raw_window_handle::DisplayHandle::web())
}
}
impl PlatformWindow for WebWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.inner.state.borrow().bounds
}
fn is_maximized(&self) -> bool {
false
}
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Windowed(self.bounds())
}
fn content_size(&self) -> Size<Pixels> {
self.inner.state.borrow().bounds.size
}
fn resize(&mut self, size: Size<Pixels>) {
let style = self.inner.canvas.style();
style
.set_property("width", &format!("{}px", f32::from(size.width)))
.ok();
style
.set_property("height", &format!("{}px", f32::from(size.height)))
.ok();
}
fn scale_factor(&self) -> f32 {
self.inner.state.borrow().scale_factor
}
fn appearance(&self) -> WindowAppearance {
current_appearance(&self.inner.browser_window)
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.display.clone())
}
fn mouse_position(&self) -> Point<Pixels> {
self.inner.state.borrow().mouse_position
}
fn modifiers(&self) -> Modifiers {
self.inner.state.borrow().modifiers
}
fn capslock(&self) -> Capslock {
self.inner.state.borrow().capslock
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.inner.state.borrow_mut().input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.inner.state.borrow_mut().input_handler.take()
}
fn prompt(
&self,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
None
}
fn activate(&self) {
self.inner.state.borrow_mut().is_active = true;
}
fn is_active(&self) -> bool {
self.inner.state.borrow().is_active
}
fn is_hovered(&self) -> bool {
self.inner.state.borrow().is_hovered
}
fn background_appearance(&self) -> WindowBackgroundAppearance {
WindowBackgroundAppearance::Opaque
}
fn set_title(&mut self, title: &str) {
self.inner.state.borrow_mut().title = title.to_owned();
if let Some(document) = self.inner.browser_window.document() {
document.set_title(title);
}
}
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
fn minimize(&self) {
log::warn!("WebWindow::minimize is not supported in the browser");
}
fn zoom(&self) {
log::warn!("WebWindow::zoom is not supported in the browser");
}
fn toggle_fullscreen(&self) {
let mut state = self.inner.state.borrow_mut();
state.is_fullscreen = !state.is_fullscreen;
if state.is_fullscreen {
let canvas: &web_sys::Element = self.inner.canvas.as_ref();
canvas.request_fullscreen().ok();
} else {
if let Some(document) = self.inner.browser_window.document() {
document.exit_fullscreen();
}
}
}
fn is_fullscreen(&self) -> bool {
self.inner.state.borrow().is_fullscreen
}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
self.inner.callbacks.borrow_mut().request_frame = Some(callback);
}
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
self.inner.callbacks.borrow_mut().input = Some(callback);
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.inner.callbacks.borrow_mut().hover_status_change = Some(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.inner.callbacks.borrow_mut().resize = Some(callback);
}
fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.inner.callbacks.borrow_mut().moved = Some(callback);
}
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
self.inner.callbacks.borrow_mut().should_close = Some(callback);
}
fn on_close(&self, callback: Box<dyn FnOnce()>) {
self.inner.callbacks.borrow_mut().close = Some(callback);
}
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback);
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
}
fn draw(&self, scene: &Scene) {
if let Some((width, height)) = self.inner.pending_physical_size.take() {
if self.inner.canvas.width() != width || self.inner.canvas.height() != height {
self.inner.canvas.set_width(width);
self.inner.canvas.set_height(height);
}
let mut state = self.inner.state.borrow_mut();
state.renderer.update_drawable_size(Size {
width: DevicePixels(width as i32),
height: DevicePixels(height as i32),
});
drop(state);
}
self.inner.state.borrow_mut().renderer.draw(scene);
}
fn completed_frame(&self) {
// On web, presentation happens automatically via wgpu surface present
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.inner.state.borrow().renderer.sprite_atlas().clone()
}
fn is_subpixel_rendering_supported(&self) -> bool {
self.inner
.state
.borrow()
.renderer
.supports_dual_source_blending()
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
Some(self.inner.state.borrow().renderer.gpu_specs())
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
fn request_decorations(&self, _decorations: WindowDecorations) {}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server
}
fn set_app_id(&mut self, _app_id: &str) {}
fn window_controls(&self) -> WindowControls {
WindowControls {
fullscreen: true,
maximize: false,
minimize: false,
window_menu: false,
}
}
fn set_client_inset(&self, _inset: Pixels) {}
}