use core_video::pixel_buffer::CVPixelBuffer; use gpui::{ App, Bounds, Context, CursorStyle, DispatchPhase, Element, ElementId, Entity, FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, KeyDownEvent, KeyUpEvent, Keystroke, LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Size, Style, Window, div, prelude::*, px, relative, }; use crate::theme::WebViewTheme; /// Input forwarded from a [`WebView`] element to its [`WebViewSource`]. #[derive(Debug)] pub enum WebInput { MouseDown { button: MouseButton, pos: Point, }, MouseUp { button: MouseButton, pos: Point, }, MouseMove { pos: Point, }, Scroll { delta: ScrollDelta, pos: Point, }, Key { keystroke: Keystroke, pressed: bool, }, ModifiersChanged { modifiers: gpui::Modifiers, }, EditingAction { action: servo::EditingActionEvent, }, } /// The surface-facing half of a webview: anything a [`WebView`] element can /// render and forward input to. /// /// Implemented for [`crate::ServoEngine`]. Implement it for your own engine if /// you need a different lifecycle (e.g. multiple tabs sharing one component). pub trait WebViewSource: 'static { /// The latest rendered frame, or `None` before the first paint. fn frame(&self) -> Option; /// The CSS cursor the page wants the embedding surface to use. fn cursor(&self) -> CursorStyle; /// Link hover status text, shown as an overlay. fn status_text(&self) -> Option; /// Resize the rendering surface. fn resize(&mut self, size: dpi::PhysicalSize, scale: f32); /// Feed a raw input event to the page. fn send_input(&mut self, input: WebInput); } /// A gpui element that renders a [`WebViewSource`]'s frames and forwards input /// to it. Fill it with `.size_full()` in the parent layout. pub struct WebView { engine: Entity, focus_handle: FocusHandle, keyboard: bool, } impl WebView { /// Create the view. /// /// By default the host owns keyboard routing: forward keys to the source /// yourself. Call [`WebView::keyboard`] to have the element take focus on /// click and forward key/modifier input for you. pub fn new(engine: Entity, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); cx.observe(&engine, |_, _, cx| cx.notify()).detach(); Self { engine, focus_handle, keyboard: false, } } /// Let the element grab focus on click and forward keyboard input to the /// page. Use this for a standalone webview; leave it off when a parent view /// routes keys. pub fn keyboard(mut self, enabled: bool) -> Self { self.keyboard = enabled; self } pub fn engine(&self) -> &Entity { &self.engine } fn forward_key(&mut self, keystroke: &Keystroke, pressed: bool, cx: &mut Context) { self.engine.update(cx, |engine, _| { engine.send_input(WebInput::Key { keystroke: keystroke.clone(), pressed, }); }); } } impl Focusable for WebView { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for WebView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.try_global::().copied().unwrap_or_default(); let (frame, cursor, status) = { let engine = self.engine.read(cx); (engine.frame(), engine.cursor(), engine.status_text()) }; let mut el = div() .size_full() .relative() .bg(theme.background) .cursor(cursor); if self.keyboard { el = el .track_focus(&self.focus_handle) .on_mouse_down( MouseButton::Left, cx.listener(|this, _, window, cx| { window.focus(&this.focus_handle, cx); }), ) .on_key_down(cx.listener(|this, ev: &KeyDownEvent, _window, cx| { this.forward_key(&ev.keystroke, true, cx); })) .on_key_up(cx.listener(|this, ev: &KeyUpEvent, _window, cx| { this.forward_key(&ev.keystroke, false, cx); })) .on_modifiers_changed(cx.listener( |this, ev: &ModifiersChangedEvent, _window, cx| { this.engine.update(cx, |engine, _| { engine.send_input(WebInput::ModifiersChanged { modifiers: ev.modifiers, }); }); }, )); } el.child(WebSurface { engine: self.engine.clone(), frame, }) .when_some(status, |el, text| { // link/status preview pinned to the bottom-left corner el.child( div() .absolute() .bottom_0() .left_0() .line_clamp(1) .max_w(px(600.)) .overflow_hidden() .px_2() .py_0p5() .bg(theme.status_background) .border_1() .border_color(theme.status_border) .text_color(theme.status_foreground) .text_xs() .child(text), ) }) } } struct WebSurface { engine: Entity, frame: Option, } impl IntoElement for WebSurface { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for WebSurface { type RequestLayoutState = (); type PrepaintState = Hitbox; fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::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::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); (window.request_layout(style, [], cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, _cx: &mut App, ) -> Self::PrepaintState { window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let scale = window.scale_factor(); let size = physical_size(bounds.size, scale); self.engine .update(cx, |engine, _| engine.resize(size, scale)); if let Some(frame) = self.frame.take() { window.paint_surface(bounds, frame); } let origin = bounds.origin; self.on_mouse_down(window, hitbox, origin); self.on_mouse_up(window, hitbox, origin); self.on_mouse_move(window, hitbox, origin); self.on_scroll_wheel(window, hitbox, origin); } } /// Renders a cached webview frame as a purely visual thumbnail; no hitbox is /// inserted, so mouse events pass through to whatever is underneath. pub struct ThumbSurface { frame: Option, } impl ThumbSurface { pub fn new(frame: Option) -> Self { Self { frame } } } impl IntoElement for ThumbSurface { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for ThumbSurface { type RequestLayoutState = (); type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::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::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); (window.request_layout(style, [], cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, _bounds: Bounds, _: &mut Self::RequestLayoutState, _window: &mut Window, _cx: &mut App, ) { // No hitbox is inserted, so the thumbnail is purely visual and mouse // events pass through to whatever is underneath. } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, window: &mut Window, _cx: &mut App, ) { if let Some(frame) = self.frame.take() { window.paint_surface(bounds, frame); } } } impl WebSurface { fn on_mouse_down(&self, window: &mut Window, hitbox: &Hitbox, origin: Point) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::(move |ev, phase, window, cx| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { engine.update(cx, |engine, _| { engine.send_input(WebInput::MouseDown { button: ev.button, pos: ev.position - origin, }) }); } }); } fn on_mouse_up(&self, window: &mut Window, hitbox: &Hitbox, origin: Point) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::(move |ev, phase, window, cx| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { engine.update(cx, |engine, _| { engine.send_input(WebInput::MouseUp { button: ev.button, pos: ev.position - origin, }) }); } }); } fn on_mouse_move(&self, window: &mut Window, hitbox: &Hitbox, origin: Point) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::(move |ev, phase, window, cx| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { engine.update(cx, |engine, _| { engine.send_input(WebInput::MouseMove { pos: ev.position - origin, }) }); } }); } fn on_scroll_wheel(&self, window: &mut Window, hitbox: &Hitbox, origin: Point) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::(move |ev, phase, window, cx| { if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { engine.update(cx, |engine, _| { engine.send_input(WebInput::Scroll { delta: ev.delta, pos: ev.position - origin, }) }); } }); } } fn physical_size(logical: Size, scale: f32) -> dpi::PhysicalSize { let width = (f32::from(logical.width) * scale).round().max(1.0) as u32; let height = (f32::from(logical.height) * scale).round().max(1.0) as u32; dpi::PhysicalSize::new(width, height) } #[cfg(test)] mod test { use gpui::{px, size}; use super::*; #[test] fn scales_to_physical() { let physical = physical_size(size(px(100.), px(50.)), 2.0); assert_eq!(physical.width, 200); assert_eq!(physical.height, 100); } #[test] fn rounds_fractional() { let physical = physical_size(size(px(10.), px(10.)), 1.25); assert_eq!(physical.width, 13); assert_eq!(physical.height, 13); } #[test] fn clamps_to_one_pixel() { let physical = physical_size(size(px(0.), px(0.)), 1.0); assert_eq!(physical.width, 1); assert_eq!(physical.height, 1); } }