mirror of
https://github.com/mileswolfallen2/browser.git
synced 2026-09-08 11:23:18 +00:00
Compare commits
4
Commits
3ccb31cfd1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6cd7ec94e | ||
|
|
f2a1898039 | ||
|
|
014a8cf8b7 | ||
|
|
9446a7d0ab |
@@ -0,0 +1,5 @@
|
||||
# CEF symbols are resolved at runtime from the dlopen'd framework
|
||||
# (see cef_host::load_framework_if_bundled), so the linker must tolerate
|
||||
# undefined cef_* symbols.
|
||||
[target.'cfg(target_os = "macos")']
|
||||
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+10867
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "browser"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
cef = "151"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
egui = "0.36.1"
|
||||
egui-wgpu = "0.36.1"
|
||||
egui-winit = "0.36.1"
|
||||
env_logger = "0.11.11"
|
||||
euclid = "0.22"
|
||||
image = "0.25"
|
||||
keyboard-types = "0.8"
|
||||
log = "0.4.33"
|
||||
objc2 = "0.6"
|
||||
objc2-quartz-core = "0.3"
|
||||
pollster = "0.4"
|
||||
rustls = "0.23"
|
||||
servo = "0.5.0"
|
||||
url = "2"
|
||||
wgpu = "30.0.0"
|
||||
winit = "0.30.13"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
# Optimize dependencies (Servo, WebRender, wgpu…) even in dev builds —
|
||||
# unoptimized engines are unusably slow, while keeping our crate debuggable.
|
||||
opt-level = 2
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
@@ -1,2 +1,77 @@
|
||||
# browser
|
||||
idk
|
||||
|
||||
A minimal Firefox-style web browser built on [Servo](https://servo.org), the Rust web engine — **the newest actively maintained browser built on Servo**, written against the [`servo` crate](https://crates.io/crates/servo) embedding API (v0.1+, released April 2026).
|
||||
|
||||
> **Context:** [Verso](https://github.com/versotile-org/verso), the pioneering third-party Servo browser, was archived in October 2025 and is no longer maintained. Servo's own servoshell remains a demo mini-browser for testing the engine. This project picks up the embedder-side baton on the new public `servo` crates.io API — no engine fork required.
|
||||
|
||||
## What works
|
||||
|
||||
- Tab strip with create/close/switch
|
||||
- Back / forward / reload navigation with real history state
|
||||
- URL bar with address parsing and DuckDuckGo search fallback (`localhost` handled too)
|
||||
- Pages render via Servo into offscreen surfaces and composite under the native chrome
|
||||
- Mouse, scroll wheel, and keyboard input forwarded to the page
|
||||
- Live page titles, loading spinners, and hover status bar
|
||||
- `window.open()` / target=_blank opens a new tab
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Chrome UI: egui 0.36 + wgpu │ tabs · toolbar · status bar
|
||||
├──────────────────────────────────────────┤
|
||||
│ Frame bridge │ WebView::paint() → read_to_image()
|
||||
├──────────────────────────────────────────┤
|
||||
│ Engine: servo 0.5 crate │ Stylo · SpiderMonkey · WebRender
|
||||
│ one WebView per tab, each with its own │
|
||||
│ OffscreenRenderingContext │
|
||||
└──────────────────────────────────────────┘
|
||||
windowing: winit · rendering: wgpu (Metal/Vulkan/DX12)
|
||||
```
|
||||
|
||||
The embedder owns everything above the engine: windowing, chrome, input routing,
|
||||
and history UI. Servo owns layout, JS, networking, and painting. The two talk through
|
||||
`WebView::notify_input_event` (down) and a `WebViewDelegate` message queue (up).
|
||||
|
||||
## Building
|
||||
|
||||
Requires a stable Rust toolchain (1.85+ recommended):
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
First build takes a while — the servo crate compiles SpiderMonkey (C++) and the full
|
||||
WebRender pipeline. Incremental rebuilds after that are fast.
|
||||
|
||||
Tested on macOS (Apple Silicon). Windows and Linux should work in principle since all
|
||||
dependencies are cross-platform, but they haven't been exercised yet.
|
||||
|
||||
## Known limitations
|
||||
|
||||
This is an early-days project on top of an engine that is itself pre-1.0:
|
||||
|
||||
- No DRM video (Widevine), and some sites will simply not render correctly yet —
|
||||
that's Servo's web-compat surface area, not something the embedder can fix
|
||||
- Frames are composited through CPU readback, so heavy pages cost more than a
|
||||
zero-copy GPU path would (fine for basic browsing)
|
||||
- No bookmarks/history persistence, downloads, devtools, or preferences UI yet
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Zero-copy frame path (share GL textures instead of readback)
|
||||
- [ ] Bookmarks and browsing history
|
||||
- [ ] Page context menus
|
||||
- [ ] Dark mode following system theme
|
||||
- [ ] Linux CI builds
|
||||
|
||||
## Credits
|
||||
|
||||
Built on the shoulders of the [Servo project](https://servo.org) (Linux Foundation Europe),
|
||||
with the chrome rendered by [egui](https://github.com/emilk/egui). Design cues from Firefox.
|
||||
See also [Verso](https://github.com/versotile-org/verso) (archived) — the pioneering Servo embedder whose contributions live on upstream.
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh
|
||||
# Build Fox and assemble Fox.app with the CEF framework + helper bundles.
|
||||
#
|
||||
# scripts/bundle.sh [debug|release]
|
||||
#
|
||||
# The CEF distribution must be exported first:
|
||||
# cargo install export-cef-dir --version 151.7.0+151.3.23 && export-cef-dir
|
||||
set -e
|
||||
|
||||
PROFILE="${1:-debug}"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
export CEF_PATH="${CEF_PATH:-$HOME/.local/share/cef}"
|
||||
DIST="$CEF_PATH"
|
||||
FRAMEWORK="Chromium Embedded Framework.framework"
|
||||
|
||||
if [ "$PROFILE" = "release" ]; then
|
||||
cargo build --release
|
||||
BIN="target/release/browser"
|
||||
else
|
||||
cargo build
|
||||
BIN="target/debug/browser"
|
||||
fi
|
||||
|
||||
APP="target/bundle/Fox.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
rm -rf "$APP"
|
||||
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Frameworks"
|
||||
|
||||
cp "$BIN" "$CONTENTS/MacOS/browser"
|
||||
ditto "$DIST/$FRAMEWORK" "$CONTENTS/Frameworks/$FRAMEWORK"
|
||||
|
||||
cat > "$CONTENTS/Info.plist" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key><string>en</string>
|
||||
<key>CFBundleExecutable</key><string>browser</string>
|
||||
<key>CFBundleIconFile</key><string></string>
|
||||
<key>CFBundleIdentifier</key><string>dev.fox.browser</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
<key>CFBundleName</key><string>Fox</string>
|
||||
<key>CFBundleDisplayName</key><string>Fox</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.2.0</string>
|
||||
<key>CFBundleVersion</key><string>0.2.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
<key>NSPrincipalClass</key><string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# Chromium on macOS launches child processes (GPU/renderer/network/utility)
|
||||
# from dedicated .app bundles. We reuse the Fox binary itself as the helper
|
||||
# executable — CEF child processes are dispatched via --type= arguments, which
|
||||
# our bootstrap() already understands.
|
||||
make_helper () {
|
||||
HELPER_NAME="$1"
|
||||
HELPER_ID="$2"
|
||||
HELPER_DIR="$CONTENTS/Frameworks/$HELPER_NAME.app"
|
||||
mkdir -p "$HELPER_DIR/Contents/MacOS"
|
||||
cp "$BIN" "$HELPER_DIR/Contents/MacOS/$HELPER_NAME"
|
||||
cat > "$HELPER_DIR/Contents/Info.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key><string>en</string>
|
||||
<key>CFBundleExecutable</key><string>$HELPER_NAME</string>
|
||||
<key>CFBundleIdentifier</key><string>$HELPER_ID</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
<key>CFBundleName</key><string>$HELPER_NAME</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.2.0</string>
|
||||
<key>CFBundleVersion</key><string>0.2.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
}
|
||||
|
||||
make_helper "Fox Helper" "dev.fox.browser.helper"
|
||||
make_helper "Fox Helper (Renderer)" "dev.fox.browser.helper.renderer"
|
||||
make_helper "Fox Helper (GPU)" "dev.fox.browser.helper.gpu"
|
||||
make_helper "Fox Helper (Plugin)" "dev.fox.browser.helper.plugin"
|
||||
make_helper "Fox Helper (Alerts)" "dev.fox.browser.helper.alerts"
|
||||
|
||||
codesign --force --deep --sign - "$APP"
|
||||
|
||||
echo "Built $APP"
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Build and run Fox in dev mode.
|
||||
#
|
||||
# scripts/run-dev.sh [extra cargo args...]
|
||||
#
|
||||
# On macOS the app must run from a .app bundle (CEF needs its framework
|
||||
# resources, so a bare target/debug binary cannot start). This script
|
||||
# therefore bundles first, then launches Fox.app with console output visible.
|
||||
#
|
||||
# The CEF distribution must be exported once:
|
||||
# cargo install export-cef-dir --version 151.7.0+151.3.23
|
||||
# export-cef-dir # installs to ~/.local/share/cef
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
export CEF_PATH="${CEF_PATH:-$HOME/.local/share/cef}"
|
||||
|
||||
scripts/bundle.sh "${FOX_PROFILE:-debug}"
|
||||
|
||||
exec ./target/bundle/Fox.app/Contents/MacOS/browser "$@"
|
||||
+1500
File diff suppressed because it is too large
Load Diff
+1107
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
mod app;
|
||||
mod cef_host;
|
||||
mod servo_host;
|
||||
mod settings;
|
||||
|
||||
use std::error::Error;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use app::BrowserApp;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserEvent {
|
||||
RepaintAfter(std::time::Duration),
|
||||
/// CEF asked for a message-loop pass after this delay.
|
||||
PumpCef(std::time::Duration),
|
||||
}
|
||||
|
||||
fn main() -> Result<ExitCode, Box<dyn Error>> {
|
||||
// Note: logging is initialized by `Servo::setup_logging()` — do not install
|
||||
// another logger here or it will panic with `SetLoggerError`.
|
||||
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install crypto provider");
|
||||
|
||||
// The CEF handshake must happen before winit creates an NSApplication:
|
||||
// when this binary is re-executed as a CEF helper process, bootstrap()
|
||||
// runs the child-process loop to completion and returns false.
|
||||
if !cef_host::bootstrap() {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if cef_host::run_probe() {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
let event_loop = winit::event_loop::EventLoop::<UserEvent>::with_user_event()
|
||||
.build()
|
||||
.expect("Failed to create event loop");
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
cef_host::set_pump_proxy(proxy.clone());
|
||||
|
||||
let settings = settings::Settings::load();
|
||||
let mut app = BrowserApp::new(proxy, settings);
|
||||
|
||||
event_loop.run_app(&mut app)?;
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
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) -> ServoPage {
|
||||
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);
|
||||
ServoPage::new(webview)
|
||||
}
|
||||
}
|
||||
|
||||
/// Engine-side handle for one Servo tab. UI state (title, texture, …) lives in
|
||||
/// `app::Tab`; this only carries what Servo needs.
|
||||
pub struct ServoPage {
|
||||
pub webview: WebView,
|
||||
pub id: WebViewId,
|
||||
}
|
||||
|
||||
impl ServoPage {
|
||||
fn new(webview: WebView) -> ServoPage {
|
||||
ServoPage {
|
||||
id: webview.id(),
|
||||
webview,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adopt(webview: WebView) -> ServoPage {
|
||||
Self::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(),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Which browser engine renders a tab.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum EngineKind {
|
||||
#[default]
|
||||
Chromium,
|
||||
Servo,
|
||||
}
|
||||
|
||||
impl EngineKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
EngineKind::Chromium => "Chromium",
|
||||
EngineKind::Servo => "Servo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub default_engine: EngineKind,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load() -> Settings {
|
||||
let Some(text) = std::fs::read_to_string(Self::path()).ok() else {
|
||||
return Settings::default();
|
||||
};
|
||||
serde_json::from_str(&text).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let path = Self::path();
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(json) = serde_json::to_string_pretty(self) {
|
||||
let _ = std::fs::write(path, json);
|
||||
}
|
||||
}
|
||||
|
||||
fn path() -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_owned());
|
||||
PathBuf::from(home)
|
||||
.join("Library/Application Support/Fox/settings.json")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user