use std::{cell::Cell, rc::Rc};
use anyhow::{Context as _, Result};
use gpui::{AnyWindowHandle, AsyncApp, Bounds, Window, WindowCompositionSurface, point, px, size};
use objc2::rc::Retained;
use objc2_app_kit::NSView;
use objc2_foundation::{NSPoint, NSRect, NSSize};
use wry::WebViewExtMacOS as _;
pub(super) struct NativeWebViewSurface {
surface: WindowCompositionSurface,
original_parent: Retained<NSView>,
scale_factor: Cell<f32>,
window: AnyWindowHandle,
cx: AsyncApp,
}
impl NativeWebViewSurface {
pub(super) fn new(webview: &wry::WebView, window: &Window, cx: &gpui::App) -> Result<Self> {
let composition = window.enable_window_composition()?;
let surface = composition.create_native_surface()?;
let attached = (|| {
let handle = surface.platform_surface()?.platform_handle()?;
let parent = *handle
.downcast::<usize>()
.map_err(|_| anyhow::anyhow!("native surface is not an AppKit view"))?;
anyhow::ensure!(parent != 0, "native surface has no AppKit view");
let view = webview.webview();
unsafe {
let original_parent = view.superview().context("WebView has no parent")?;
(&*(parent as *const NSView)).addSubview(&view);
Ok(original_parent)
}
})();
match attached {
Ok(original_parent) => Ok(Self {
surface,
original_parent,
scale_factor: Cell::new(window.scale_factor()),
window: window.window_handle(),
cx: cx.to_async(),
}),
Err(error) => {
composition.remove_surface(surface.id())?;
Err(error)
}
}
}
pub(super) fn set_scale_factor(&self, scale_factor: f32) {
self.scale_factor.set(scale_factor);
}
pub(super) fn set_bounds(&self, webview: &wry::WebView, rect: wry::Rect) -> Result<()> {
let scale = self.scale_factor.get();
let (position, extent) = logical_bounds(rect, scale);
let bounds = Bounds::new(
point(px(position.x as f32), px(position.y as f32)),
size(px(extent.width as f32), px(extent.height as f32)),
);
self.surface
.platform_surface()?
.set_bounds(bounds.to_device_pixels(scale))?;
webview.webview().setFrame(NSRect::new(
NSPoint::new(0., 0.),
NSSize::new(extent.width, extent.height),
));
Ok(())
}
pub(super) fn set_geometry(
&self,
webview: &wry::WebView,
document: Bounds<gpui::Pixels>,
visible: Bounds<gpui::Pixels>,
) -> Result<()> {
let scale = self.scale_factor.get();
self.surface
.platform_surface()?
.set_bounds(visible.to_device_pixels(scale))?;
let offset = document.origin - visible.origin;
webview.webview().setFrame(NSRect::new(
NSPoint::new(
f64::from(offset.x),
f64::from(visible.size.height - offset.y - document.size.height),
),
NSSize::new(
f64::from(document.size.width),
f64::from(document.size.height),
),
));
Ok(())
}
pub(super) fn set_visible(&self, visible: bool) -> Result<()> {
self.surface.platform_surface()?.set_visible(visible)
}
pub(super) fn release(self, webview: Rc<wry::WebView>) {
self.cx
.clone()
.spawn(async move |cx| {
self.original_parent.addSubview(&webview.webview());
let result = self.window.update(cx, |_, window, _| {
window
.enable_window_composition()?
.remove_surface(self.surface.id())
});
if let Ok(Err(error)) = result {
log::warn!("cannot release WebView composition surface: {error:#}");
}
})
.detach();
}
}
fn logical_bounds(
rect: wry::Rect,
scale: f32,
) -> (wry::dpi::LogicalPosition<f64>, wry::dpi::LogicalSize<f64>) {
(
rect.position.to_logical(f64::from(scale)),
rect.size.to_logical(f64::from(scale)),
)
}
#[cfg(test)]
mod tests {
use super::*;
use wry::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
#[test]
fn retina_physical_bounds_keep_the_child_inside_its_container() {
let (position, extent) = logical_bounds(
wry::Rect {
position: PhysicalPosition::new(400, 160).into(),
size: PhysicalSize::new(1280, 960).into(),
},
2.,
);
assert_eq!(position, LogicalPosition::new(200., 80.));
assert_eq!(extent, LogicalSize::new(640., 480.));
}
#[test]
fn offscreen_logical_loading_bounds_do_not_shift_on_retina() {
let rect = wry::Rect {
position: LogicalPosition::new(-20000., -20000.).into(),
size: LogicalSize::new(640., 480.).into(),
};
for scale in [1., 2.] {
let (position, extent) = logical_bounds(rect, scale);
assert_eq!(position, LogicalPosition::new(-20000., -20000.));
assert_eq!(extent, LogicalSize::new(640., 480.));
}
}
}