use std::rc::{Rc, Weak};
use gpui::{
App, Bounds, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
IntoElement, LayoutId, Pixels, Refineable as _, Style, StyleRefinement, Styled, Window,
};
use tauri::{LogicalPosition, LogicalSize, Manager as _, Rect, Webview, Wry};
use crate::{platform::window::WindowInner, runtime};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux;
pub(crate) struct EmbeddedWebview {
webview: Webview<Wry>,
desired: Option<Bounds<Pixels>>,
applied: Option<Option<Bounds<Pixels>>>,
frames: usize,
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
impl Drop for EmbeddedWebview {
fn drop(&mut self) {
linux::forget(self.webview.label());
}
}
struct FrameState {
window: Weak<WindowInner>,
label: String,
}
impl FrameState {
fn new(inner: &Rc<WindowInner>, webview: &Webview<Wry>) -> Self {
let label = webview.label().to_string();
inner
.webviews
.borrow_mut()
.entry(label.clone())
.or_insert_with(|| EmbeddedWebview {
webview: webview.clone(),
desired: None,
applied: None,
frames: 0,
})
.frames += 1;
Self {
window: Rc::downgrade(inner),
label,
}
}
fn place(&self, bounds: Option<Bounds<Pixels>>) {
if let Some(inner) = self.window.upgrade()
&& let Some(embedded) = inner.webviews.borrow_mut().get_mut(&self.label)
{
embedded.desired = bounds;
}
}
}
impl Drop for FrameState {
fn drop(&mut self) {
let Some(inner) = self.window.upgrade() else {
return;
};
let Ok(mut webviews) = inner.webviews.try_borrow_mut() else {
return;
};
if let Some(embedded) = webviews.get_mut(&self.label) {
embedded.frames -= 1;
if embedded.frames == 0 {
embedded.desired = None;
}
}
}
}
pub fn webview_frame(webview: &Webview<Wry>) -> WebviewFrame {
WebviewFrame {
id: ElementId::Name(format!("webview-frame:{}", webview.label()).into()),
webview: webview.clone(),
style: StyleRefinement::default(),
}
}
pub fn focus_webview(webview: &Webview<Wry>) -> tauri::Result<()> {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if linux::focus(webview.label()) {
return Ok(());
}
webview.set_focus()
}
pub struct WebviewFrame {
id: ElementId,
webview: Webview<Wry>,
style: StyleRefinement,
}
impl Styled for WebviewFrame {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl IntoElement for WebviewFrame {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for WebviewFrame {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style {
size: gpui::Size::full(),
..Style::default()
};
style.refine(&self.style);
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
_: &mut App,
) -> Self::PrepaintState {
let visible = bounds.intersect(&window.content_mask().bounds);
let visible = (visible.size.width > Pixels::ZERO && visible.size.height > Pixels::ZERO)
.then_some(visible);
let id = id.expect("webview_frame has an element id");
let placed = window.with_element_state::<Option<FrameState>, _>(id, |state, _| {
let state = state.flatten().or_else(|| {
let window = self.webview.window();
match runtime::with(|runtime| Ok(runtime.surface(window.label()))) {
Ok(Some(inner)) => Some(FrameState::new(&inner, &self.webview)),
_ => {
log::warn!(
"webview_frame: `{}` is not a child of a GPUI-backed window",
self.webview.label()
);
None
}
}
});
if let Some(state) = &state {
state.place(visible);
}
(state.is_some(), state)
});
visible
.filter(|_| placed)
.map(|visible| window.insert_hitbox(visible, HitboxBehavior::BlockMouse))
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
_: &mut App,
) {
if hitbox.is_none() {
return;
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
let label = self.webview.label().to_string();
window.on_mouse_event(move |_: &gpui::MouseDownEvent, phase, _, _| {
if phase == gpui::DispatchPhase::Bubble {
linux::focus_parent(&label);
}
});
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let _ = window;
}
}
pub(crate) fn sync(inner: &WindowInner) {
let mut changes = Vec::new();
let mut webviews = inner.webviews.borrow_mut();
for embedded in webviews.values_mut() {
let painted = embedded.desired;
if embedded.applied != Some(painted) {
let was_shown = matches!(embedded.applied, Some(Some(_)));
embedded.applied = Some(painted);
changes.push((embedded.webview.clone(), painted, was_shown));
}
}
if changes.is_empty() {
return;
}
webviews.retain(|_, embedded| {
embedded.frames > 0
|| embedded.applied != Some(None)
|| embedded
.webview
.get_webview(embedded.webview.label())
.is_some()
});
drop(webviews);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let x11_parent = crate::platform::x11::xid(inner.raw.get().window);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let toplevel = inner.x11.as_ref().map(|x11| x11.toplevel_xid);
(inner.defer)(Box::new(move || {
for (webview, painted, was_shown) in changes {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(parent) = x11_parent {
linux::apply(&webview, parent, toplevel, painted);
continue;
}
let result = match painted {
Some(bounds) => webview
.set_bounds(rect(bounds))
.and_then(|()| if was_shown { Ok(()) } else { webview.show() }),
None => webview.hide(),
};
if let Err(error) = result {
log::warn!("failed to update webview `{}`: {error}", webview.label());
}
}
}));
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) fn invalidate(inner: &WindowInner) {
for embedded in inner.webviews.borrow_mut().values_mut() {
embedded.applied = None;
}
}
fn rect(bounds: Bounds<Pixels>) -> Rect {
Rect {
position: LogicalPosition::new(f32::from(bounds.origin.x), f32::from(bounds.origin.y))
.into(),
size: LogicalSize::new(f32::from(bounds.size.width), f32::from(bounds.size.height)).into(),
}
}