extern crate gpui_kit as gpui;
#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
mod composition;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
use composition::NativeWebViewSurface;
#[cfg(all(feature = "gpui-fast", target_os = "linux"))]
use linux::NativeWebViewSurface;
use std::{cell::Cell, ops::Deref, rc::Rc};
use wry::Rect;
use gpui::{
App, Bounds, ContentMask, DismissEvent, Element, ElementId, Entity, EventEmitter, FocusHandle,
Focusable, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, MouseDownEvent,
ParentElement as _, Pixels, Render, Size, Style, Styled as _, Window, canvas, div,
};
const _: () = {
assert!(
!gpui::GPUI_FAST || cfg!(feature = "gpui-fast"),
"gpui-kit is built with `gpui-fast`, but gpui-webview is not: enable the \
`gpui-webview/gpui-fast` feature so the WebView uses GPUI Fast's window composition"
);
assert!(
gpui::GPUI_FAST || !cfg!(feature = "gpui-fast"),
"gpui-webview is built with `gpui-fast`, but gpui-kit is not: enable the \
`gpui-kit/gpui-fast` feature, which selects the GPUI backend"
);
};
#[derive(Clone)]
pub struct WebViewHandle(Rc<wry::WebView>);
impl WebViewHandle {
pub fn raw(&self) -> &wry::WebView {
&self.0
}
}
pub struct WebView {
focus_handle: FocusHandle,
webview: Rc<wry::WebView>,
visible: Cell<bool>,
bounds: Bounds<Pixels>,
zoom: Cell<f64>,
geometry_failed: Cell<bool>,
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
composition: Option<NativeWebViewSurface>,
#[cfg(target_os = "linux")]
scale_factor: Cell<f32>,
#[cfg(target_os = "linux")]
origin: Rc<Cell<gpui::Point<Pixels>>>,
}
impl Drop for WebView {
fn drop(&mut self) {
self.hide();
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = self.composition.take() {
surface.release(self.webview.clone());
}
}
}
impl WebView {
pub fn new(webview: wry::WebView, window: &mut Window, cx: &mut App) -> Self {
let _ = webview.set_bounds(Rect::default());
#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
let composition = match NativeWebViewSurface::new(&webview, window, cx) {
Ok(surface) => Some(surface),
Err(error) => {
log::warn!("WebView composition unavailable: {error:#}");
None
}
};
#[cfg(target_os = "linux")]
let origin = Rc::new(Cell::new(gpui::Point::default()));
#[cfg(target_os = "linux")]
linux::forward_mouse_down(&webview, origin.clone(), window, cx);
#[cfg(target_os = "linux")]
linux::match_scale_factor(&webview, window.scale_factor(), 1.0);
#[cfg(not(any(all(feature = "gpui-fast", target_os = "macos"), target_os = "linux")))]
let _ = window;
Self {
focus_handle: cx.focus_handle(),
visible: Cell::new(true),
bounds: Bounds::default(),
zoom: Cell::new(1.0),
geometry_failed: Cell::new(false),
webview: Rc::new(webview),
#[cfg(all(feature = "gpui-fast", target_os = "macos"))]
composition,
#[cfg(all(feature = "gpui-fast", target_os = "linux"))]
composition: None,
#[cfg(target_os = "linux")]
scale_factor: Cell::new(window.scale_factor()),
#[cfg(target_os = "linux")]
origin,
}
}
pub fn build(
builder: wry::WebViewBuilder,
window: &mut Window,
cx: &mut App,
) -> wry::Result<Self> {
#[cfg(target_os = "linux")]
let webview = {
let parent = linux::X11Parent::new(window)?;
linux::ensure_gtk(cx)?;
#[cfg(feature = "gpui-fast")]
match NativeWebViewSurface::new(window, cx) {
Ok(surface) => {
let webview = builder.build_as_child(&surface.parent()?)?;
let mut this = Self::new(webview, window, cx);
this.composition = Some(surface);
return Ok(this);
}
Err(error) => log::warn!("WebView composition unavailable: {error:#}"),
}
builder.build_as_child(&parent)?
};
#[cfg(not(target_os = "linux"))]
let webview = builder.build_as_child(&*window)?;
Ok(Self::new(webview, window, cx))
}
pub fn set_bounds(&self, bounds: Rect) -> wry::Result<()> {
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = &self.composition {
return surface
.set_bounds(&self.webview, bounds)
.map_err(|error| wry::Error::Io(std::io::Error::other(error)));
}
self.webview.set_bounds(bounds)
}
pub fn set_geometry(
&self,
document: Bounds<Pixels>,
clip: Bounds<Pixels>,
) -> anyhow::Result<()> {
let visible = document.intersect(&clip);
if visible.is_empty() || !self.visible.get() {
self.set_native_visible(false)?;
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = &self.composition {
if self.visible.get() && !document.is_empty() {
surface.set_geometry(&self.webview, document, document)?;
}
}
return Ok(());
}
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = &self.composition {
surface.set_geometry(&self.webview, document, visible)?;
surface.set_visible(true)?;
self.webview.set_visible(true)?;
return Ok(());
}
#[cfg(target_os = "linux")]
let rect = linux::device_bounds(document, self.scale_factor.get());
#[cfg(not(target_os = "linux"))]
let rect = Rect {
position: wry::dpi::LogicalPosition::new(
f64::from(document.origin.x),
f64::from(document.origin.y),
)
.into(),
size: wry::dpi::LogicalSize::new(
f64::from(document.size.width),
f64::from(document.size.height),
)
.into(),
};
self.set_bounds(rect)?;
self.set_native_visible(true)?;
Ok(())
}
fn set_native_visible(&self, visible: bool) -> wry::Result<()> {
self.webview.set_visible(visible)?;
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = &self.composition {
surface
.set_visible(visible)
.map_err(|error| wry::Error::Io(std::io::Error::other(error)))?;
}
Ok(())
}
pub fn set_visible(&self, visible: bool) -> wry::Result<()> {
let focus_result = if !visible && self.visible.get() {
self.webview.focus_parent()
} else {
Ok(())
};
self.set_native_visible(visible)?;
self.visible.set(visible);
focus_result
}
pub fn show(&mut self) {
let _ = self.set_visible(true);
}
pub fn hide(&mut self) {
let _ = self.set_visible(false);
}
pub fn visible(&self) -> bool {
self.visible.get()
}
pub fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
pub fn back(&mut self) -> anyhow::Result<()> {
Ok(self.webview.evaluate_script("history.back();")?)
}
pub fn forward(&mut self) -> anyhow::Result<()> {
Ok(self.webview.evaluate_script("history.forward();")?)
}
pub fn load_url(&mut self, url: &str) {
let _ = self.webview.load_url(url);
}
pub fn set_theme(&self, theme: wry::Theme) -> wry::Result<()> {
#[cfg(target_os = "linux")]
linux::set_theme(theme);
#[cfg(target_os = "macos")]
macos::set_theme(&self.webview, theme);
#[cfg(target_os = "windows")]
wry::WebViewExtWindows::set_theme(&*self.webview, theme)?;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let _ = theme;
Ok(())
}
pub fn set_zoom(&self, zoom: f64) -> wry::Result<()> {
self.zoom.set(zoom);
#[cfg(target_os = "linux")]
linux::match_scale_factor(&self.webview, self.scale_factor.get(), zoom);
#[cfg(not(target_os = "linux"))]
self.webview.zoom(zoom)?;
Ok(())
}
pub fn zoom(&self) -> f64 {
self.zoom.get()
}
pub fn raw(&self) -> &wry::WebView {
&self.webview
}
pub fn handle(&self) -> WebViewHandle {
WebViewHandle(self.webview.clone())
}
}
impl Deref for WebView {
type Target = wry::WebView;
fn deref(&self) -> &Self::Target {
&self.webview
}
}
impl Focusable for WebView {
fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for WebView {}
impl Render for WebView {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let view = cx.entity().clone();
div()
.track_focus(&self.focus_handle)
.size_full()
.child({
let view = cx.entity().clone();
canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.absolute()
.size_full()
})
.child(WebViewElement::new(self.webview.clone(), view, window, cx))
}
}
pub struct WebViewElement {
parent: Entity<WebView>,
view: Rc<wry::WebView>,
}
impl WebViewElement {
pub fn new(
view: Rc<wry::WebView>,
parent: Entity<WebView>,
_window: &mut Window,
_cx: &mut App,
) -> Self {
Self { view, parent }
}
}
impl IntoElement for WebViewElement {
type Element = WebViewElement;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for WebViewElement {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let style = Style {
size: Size::full(),
flex_shrink: 1.,
..Default::default()
};
let id = window.request_layout(style, [], cx);
(id, ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
if !self.parent.read(cx).visible() {
return None;
}
let parent = self.parent.read(cx);
#[cfg(all(feature = "gpui-fast", any(target_os = "macos", target_os = "linux")))]
if let Some(surface) = &parent.composition {
surface.set_scale_factor(window.scale_factor());
}
#[cfg(target_os = "linux")]
if parent.scale_factor.replace(window.scale_factor()) != window.scale_factor() {
linux::match_scale_factor(&parent.webview, window.scale_factor(), parent.zoom.get());
}
#[cfg(target_os = "linux")]
parent.origin.set(bounds.origin);
let clip = window.content_mask().bounds;
if let Err(error) = parent.set_geometry(bounds, clip) {
if !parent.geometry_failed.replace(true) {
log::warn!("WebView geometry update failed: {error:#}");
}
let _ = parent.set_native_visible(false);
return None;
}
parent.geometry_failed.set(false);
let visible = bounds.intersect(&clip);
if visible.is_empty() {
return None;
}
Some(window.insert_hitbox(visible, gpui::HitboxBehavior::BlockMouse))
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
_: &mut App,
) {
let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let webview = self.view.clone();
window.on_mouse_event(move |event: &MouseDownEvent, _, _, _| {
if !bounds.contains(&event.position) {
let _ = webview.focus_parent();
}
});
});
}
}