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>>; pub enum EngineMsg { UrlChanged(WebViewId, String), TitleChanged(WebViewId, Option), StatusText(WebViewId, Option), 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 { 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) { self.queue .borrow_mut() .push(EngineMsg::TitleChanged(webview.id(), title)); } fn notify_status_text_changed(&self, webview: WebView, status: Option) { 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) { 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, pub queue: SharedQueue, } impl Engine { pub fn new(waker: EguiWaker, window_context: Rc) -> 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 = 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) .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, pub loading: bool, pub animating: bool, pub dirty: bool, pub texture: Option, /// 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::() .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 { 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 { 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 { 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(), ))) }