Files
browser/src/servo_host.rs
T
2026-08-21 19:02:37 -05:00

304 lines
9.7 KiB
Rust

use std::cell::RefCell;
use std::rc::Rc;
use egui::TextureHandle;
use keyboard_types::{Code, Key, KeyState, Location, Modifiers, NamedKey};
use servo::{
DevicePoint, EventLoopWaker, InputEvent, KeyboardEvent as ServoKeyboardEvent, LoadStatus,
MouseButton, MouseButtonAction, OffscreenRenderingContext, RenderingContext, Servo,
ServoBuilder, Theme, WebView, WebViewBuilder, WebViewId, WheelDelta, WheelEvent, WheelMode,
WindowRenderingContext,
};
use url::Url;
pub type SharedQueue = Rc<RefCell<Vec<EngineMsg>>>;
pub enum EngineMsg {
UrlChanged(WebViewId, String),
TitleChanged(WebViewId, Option<String>),
StatusText(WebViewId, Option<String>),
LoadStatus(WebViewId, bool),
NewFrame(WebViewId),
Animating(WebViewId, bool),
Crashed(WebViewId, String),
Closed(WebViewId),
NewWebView(WebView),
}
#[derive(Clone)]
pub struct EguiWaker(pub egui::Context);
impl EventLoopWaker for EguiWaker {
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
Box::new(self.clone())
}
fn wake(&self) {
self.0.request_repaint();
}
}
pub struct BrowserDelegate {
pub queue: SharedQueue,
}
impl servo::WebViewDelegate for BrowserDelegate {
fn notify_url_changed(&self, webview: WebView, url: Url) {
self.queue
.borrow_mut()
.push(EngineMsg::UrlChanged(webview.id(), url.to_string()));
}
fn notify_page_title_changed(&self, webview: WebView, title: Option<String>) {
self.queue
.borrow_mut()
.push(EngineMsg::TitleChanged(webview.id(), title));
}
fn notify_status_text_changed(&self, webview: WebView, status: Option<String>) {
self.queue
.borrow_mut()
.push(EngineMsg::StatusText(webview.id(), status));
}
fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) {
let loading = !matches!(status, LoadStatus::Complete);
self.queue
.borrow_mut()
.push(EngineMsg::LoadStatus(webview.id(), loading));
}
fn notify_new_frame_ready(&self, webview: WebView) {
self.queue
.borrow_mut()
.push(EngineMsg::NewFrame(webview.id()));
}
fn notify_animating_changed(&self, webview: WebView, animating: bool) {
self.queue
.borrow_mut()
.push(EngineMsg::Animating(webview.id(), animating));
}
fn notify_crashed(&self, webview: WebView, reason: String, _backtrace: Option<String>) {
self.queue
.borrow_mut()
.push(EngineMsg::Crashed(webview.id(), reason));
}
fn notify_closed(&self, webview: WebView) {
self.queue.borrow_mut().push(EngineMsg::Closed(webview.id()));
}
fn request_create_new(&self, parent_webview: WebView, request: servo::CreateNewWebViewRequest) {
let webview = request
.builder(parent_webview.rendering_context())
.delegate(parent_webview.delegate())
.build();
webview.notify_theme_change(Theme::Light);
self.queue.borrow_mut().push(EngineMsg::NewWebView(webview));
}
}
pub struct Engine {
pub servo: Servo,
pub window_context: Rc<WindowRenderingContext>,
pub queue: SharedQueue,
}
impl Engine {
pub fn new(waker: EguiWaker, window_context: Rc<WindowRenderingContext>) -> Engine {
let queue: SharedQueue = Rc::new(RefCell::new(Vec::new()));
let servo = ServoBuilder::default()
.event_loop_waker(Box::new(waker))
.build();
servo.setup_logging();
Engine {
servo,
window_context,
queue,
}
}
pub fn spin(&self) {
self.servo.spin_event_loop();
}
pub fn create_tab(&self, url: Url, hidpi_scale: f32) -> Tab {
let size = self.window_context.size();
let offscreen: Rc<OffscreenRenderingContext> =
Rc::new(self.window_context.offscreen_context(size));
let delegate = Rc::new(BrowserDelegate {
queue: self.queue.clone(),
});
let webview = WebViewBuilder::new(&self.servo, offscreen as Rc<dyn RenderingContext>)
.url(url)
.hidpi_scale_factor(euclid::Scale::new(hidpi_scale))
.delegate(delegate)
.build();
webview.notify_theme_change(Theme::Light);
Tab::new(webview)
}
}
pub struct Tab {
pub webview: WebView,
pub id: WebViewId,
pub title: String,
pub url_string: String,
pub status_text: Option<String>,
pub loading: bool,
pub animating: bool,
pub dirty: bool,
pub texture: Option<TextureHandle>,
/// Hash of the last uploaded frame — skips redundant GPU texture uploads.
pub last_frame_hash: u64,
/// Time of the last GPU->CPU readback — throttles readback frequency.
pub last_paint: std::time::Instant,
}
impl Tab {
fn new(webview: WebView) -> Tab {
Tab {
id: webview.id(),
webview,
title: String::from("New Tab"),
url_string: String::new(),
status_text: None,
loading: true,
animating: false,
dirty: true,
texture: None,
last_frame_hash: 0,
last_paint: std::time::Instant::now() - std::time::Duration::from_secs(1),
}
}
pub fn adopt(webview: WebView) -> Tab {
Tab::new(webview)
}
}
fn winit_named_key_to_kt(key: winit::keyboard::NamedKey) -> Key {
use winit::keyboard::NamedKey as W;
match key {
W::Enter => Key::Named(NamedKey::Enter),
W::Tab => Key::Named(NamedKey::Tab),
W::Backspace => Key::Named(NamedKey::Backspace),
W::Delete => Key::Named(NamedKey::Delete),
W::Escape => Key::Named(NamedKey::Escape),
W::Space => Key::Character(" ".into()),
W::ArrowUp => Key::Named(NamedKey::ArrowUp),
W::ArrowDown => Key::Named(NamedKey::ArrowDown),
W::ArrowLeft => Key::Named(NamedKey::ArrowLeft),
W::ArrowRight => Key::Named(NamedKey::ArrowRight),
W::Home => Key::Named(NamedKey::Home),
W::End => Key::Named(NamedKey::End),
W::PageUp => Key::Named(NamedKey::PageUp),
W::PageDown => Key::Named(NamedKey::PageDown),
W::Insert => Key::Named(NamedKey::Insert),
W::F1 => Key::Named(NamedKey::F1),
W::F2 => Key::Named(NamedKey::F2),
W::F3 => Key::Named(NamedKey::F3),
W::F4 => Key::Named(NamedKey::F4),
W::F5 => Key::Named(NamedKey::F5),
W::F6 => Key::Named(NamedKey::F6),
W::F7 => Key::Named(NamedKey::F7),
W::F8 => Key::Named(NamedKey::F8),
W::F9 => Key::Named(NamedKey::F9),
W::F10 => Key::Named(NamedKey::F10),
W::F11 => Key::Named(NamedKey::F11),
W::F12 => Key::Named(NamedKey::F12),
_ => Key::Named(NamedKey::Unidentified),
}
}
pub fn winit_key_to_kt_key(key: &winit::keyboard::Key) -> Key {
match key {
winit::keyboard::Key::Character(s) => Key::Character(s.as_str().into()),
winit::keyboard::Key::Named(n) => winit_named_key_to_kt(*n),
_ => Key::Named(NamedKey::Unidentified),
}
}
pub struct KeyEventParts {
pub state: KeyState,
pub key: Key,
pub code: Code,
pub repeat: bool,
}
pub fn winit_key_event_to_parts(event: &winit::event::KeyEvent) -> KeyEventParts {
let state = if event.state == winit::event::ElementState::Pressed {
KeyState::Down
} else {
KeyState::Up
};
let code = match event.physical_key {
winit::keyboard::PhysicalKey::Code(code) => {
// winit's `KeyCode` variants share their names with the W3C
// `KeyboardEvent.code` values that `keyboard_types::Code` parses.
format!("{code:?}")
.parse::<Code>()
.unwrap_or(Code::Unidentified)
}
winit::keyboard::PhysicalKey::Unidentified(_native) => Code::Unidentified,
};
KeyEventParts {
state,
key: winit_key_to_kt_key(&event.logical_key),
code,
repeat: event.repeat,
}
}
pub fn make_keyboard_input(parts: KeyEventParts, modifiers: Modifiers) -> Option<InputEvent> {
if parts.key == Key::Named(NamedKey::Unidentified) && parts.code == Code::Unidentified {
return None;
}
let event = ServoKeyboardEvent::new_without_event(
parts.state,
parts.key,
parts.code,
Location::Standard,
modifiers,
parts.repeat,
false,
);
Some(InputEvent::Keyboard(event))
}
pub fn winit_button_to_servo(button: winit::event::MouseButton) -> Option<MouseButton> {
match button {
winit::event::MouseButton::Left => Some(MouseButton::Left),
winit::event::MouseButton::Right => Some(MouseButton::Right),
winit::event::MouseButton::Middle => Some(MouseButton::Middle),
winit::event::MouseButton::Back => Some(MouseButton::Back),
winit::event::MouseButton::Forward => Some(MouseButton::Forward),
winit::event::MouseButton::Other(_) => None,
}
}
pub fn winit_button_action_to_servo(state: winit::event::ElementState) -> MouseButtonAction {
match state {
winit::event::ElementState::Pressed => MouseButtonAction::Down,
winit::event::ElementState::Released => MouseButtonAction::Up,
}
}
pub fn wheel_input(
delta: winit::event::MouseScrollDelta,
point: DevicePoint,
) -> Option<InputEvent> {
let (x, y, mode) = match delta {
winit::event::MouseScrollDelta::LineDelta(dx, dy) => {
((dx * 76.0) as f64, (dy * 76.0) as f64, WheelMode::DeltaLine)
}
winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.x, pos.y, WheelMode::DeltaPixel),
};
Some(InputEvent::Wheel(WheelEvent::new(
WheelDelta { x, y, z: 0.0, mode },
point.into(),
)))
}