Something went wrong. Try again.
a servo-backed webview component for gpui
Something went wrong. Try again.
13 kB · 453 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454use 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<Pixels>, }, MouseUp { button: MouseButton, pos: Point<Pixels>, }, MouseMove { pos: Point<Pixels>, }, Scroll { delta: ScrollDelta, pos: Point<Pixels>, }, 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<CVPixelBuffer>; /// 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<String>; /// Resize the rendering surface. fn resize(&mut self, size: dpi::PhysicalSize<u32>, 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<T: WebViewSource = crate::ServoEngine> { engine: Entity<T>, focus_handle: FocusHandle, keyboard: bool,}
impl<T: WebViewSource> WebView<T> { /// 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<T>, cx: &mut Context<Self>) -> 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<T> { &self.engine }
fn forward_key(&mut self, keystroke: &Keystroke, pressed: bool, cx: &mut Context<Self>) { self.engine.update(cx, |engine, _| { engine.send_input(WebInput::Key { keystroke: keystroke.clone(), pressed, }); }); }}
impl<T: WebViewSource> Focusable for WebView<T> { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() }}
impl<T: WebViewSource> Render for WebView<T> { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let theme = cx.try_global::<WebViewTheme>().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<T: WebViewSource> { engine: Entity<T>, frame: Option<CVPixelBuffer>,}
impl<T: WebViewSource> IntoElement for WebSurface<T> { type Element = Self;
fn into_element(self) -> Self::Element { self }}
impl<T: WebViewSource> Element for WebSurface<T> { type RequestLayoutState = (); type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> { 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<Pixels>, _: &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<Pixels>, _: &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<CVPixelBuffer>,}
impl ThumbSurface { pub fn new(frame: Option<CVPixelBuffer>) -> 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<ElementId> { 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<Pixels>, _: &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<Pixels>, _: &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<T: WebViewSource> WebSurface<T> { fn on_mouse_down(&self, window: &mut Window, hitbox: &Hitbox, origin: Point<Pixels>) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::<MouseDownEvent>(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<Pixels>) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::<MouseUpEvent>(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<Pixels>) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::<MouseMoveEvent>(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<Pixels>) { let engine = self.engine.clone(); let hitbox = hitbox.clone(); window.on_mouse_event::<ScrollWheelEvent>(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<Pixels>, scale: f32) -> dpi::PhysicalSize<u32> { 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); }}