mirror of
https://github.com/mileswolfallen2/browser.git
synced 2026-09-08 11:23:18 +00:00
304 lines
9.7 KiB
Rust
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(),
|
|
)))
|
|
}
|