Remove CEF: the web content crate, ElyMark's page, their packaging, licences and tests (C14.5)

PRD and agents.md name the native route; ElyMail's body will not use
CEF or Electron. ElyMark's package carries the KaTeX fonts' OFL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 03:15:30 -04:00
1 parent 2e8379b0b1
commit 8a942e6d80
77 files changed
+105 -10718

No files matched your search

+1 -49
View File
@@ -83,34 +83,11 @@ pub fn open(app: AppId, files: &[PathBuf]) -> io::Result<()> {
}
let mut command = Command::new(&found);
command.args(files);
let child = fresh_signals(&mut command).spawn()?;
let child = command.spawn()?;
log::info!("apps: started {} as {}", app.name(), child.id());
Ok(())
}
/// The started app takes signals as from a shell.
#[cfg(unix)]
fn fresh_signals(command: &mut Command) -> &mut Command {
use std::os::unix::process::CommandExt as _;
// Chromium's ignored or blocked signals would carry over.
unsafe {
command.pre_exec(|| {
for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP, libc::SIGQUIT] {
libc::signal(signal, libc::SIG_DFL);
}
let mut none: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut none);
libc::pthread_sigmask(libc::SIG_SETMASK, &none, std::ptr::null_mut());
Ok(())
})
}
}
#[cfg(not(unix))]
fn fresh_signals(command: &mut Command) -> &mut Command {
command
}
/// Opens `path` in the default app; a refusal says why.
pub fn open_default(path: &Path) -> io::Result<()> {
let mut command = if cfg!(target_os = "macos") {
@@ -170,28 +147,3 @@ mod tests {
))));
}
}
#[cfg(all(test, unix))]
mod signals {
use std::os::unix::process::ExitStatusExt as _;
/// A starter that ignores and blocks SIGTERM, as Chromium may.
#[test]
fn a_started_app_still_stops_on_sigterm() {
unsafe {
libc::signal(libc::SIGTERM, libc::SIG_IGN);
let mut term: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut term);
libc::sigaddset(&mut term, libc::SIGTERM);
libc::pthread_sigmask(libc::SIG_BLOCK, &term, std::ptr::null_mut());
}
let mut command = std::process::Command::new("/bin/sleep");
command.arg("30");
let mut child = super::fresh_signals(&mut command).spawn().unwrap();
std::thread::sleep(std::time::Duration::from_millis(200));
unsafe { libc::kill(child.id() as i32, libc::SIGTERM) };
let status = child.wait().unwrap();
unsafe { libc::signal(libc::SIGTERM, libc::SIG_DFL) };
assert_eq!(status.signal(), Some(libc::SIGTERM), "{status:?}");
}
}
+8 -12
View File
@@ -11,18 +11,15 @@ use crate::AppId;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Log,
/// The web content cache (ElyMark).
Cache,
Recovery,
}
impl Kind {
pub const ALL: [Kind; 3] = [Kind::Log, Kind::Cache, Kind::Recovery];
pub const ALL: [Kind; 2] = [Kind::Log, Kind::Recovery];
fn place(self) -> &'static str {
match self {
Kind::Log => "ely.log",
Kind::Cache => "web",
Kind::Recovery => "recovery",
}
}
@@ -152,15 +149,14 @@ mod tests {
#[test]
fn marked_kinds_go_at_start_and_others_stay() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("web/Cache")).unwrap();
std::fs::write(dir.path().join("web/Cache/blob"), vec![0u8; 4096]).unwrap();
std::fs::create_dir_all(dir.path().join("recovery/a")).unwrap();
std::fs::write(dir.path().join("recovery/a/point"), b"text").unwrap();
assert_eq!(size(&dir.path().join("web")).unwrap(), 4096);
std::fs::write(dir.path().join(Kind::Cache.mark()), b"").unwrap();
std::fs::write(dir.path().join("recovery/a/point"), vec![0u8; 4096]).unwrap();
std::fs::write(dir.path().join("ely.log"), b"line").unwrap();
assert_eq!(size(&dir.path().join("recovery")).unwrap(), 4096);
std::fs::write(dir.path().join(Kind::Recovery.mark()), b"").unwrap();
clear_marked_in(dir.path()).unwrap();
assert!(!dir.path().join("web").exists(), "the cache went");
assert!(dir.path().join("recovery/a/point").exists(), "unmarked kinds stay");
assert!(!dir.path().join(Kind::Cache.mark()).exists(), "the mark is spent");
assert!(!dir.path().join("recovery").exists(), "the copies went");
assert!(dir.path().join("ely.log").exists(), "the log stays");
assert!(!dir.path().join(Kind::Recovery.mark()).exists(), "the mark is spent");
}
}
-2
View File
@@ -32,7 +32,6 @@ pub(super) const EN: &[(&str, &str)] = &[
("settings.git.bad", "{path} is not Git 2.40 or later: {error}"),
("settings.storage", "Storage"),
("settings.storage.log", "This app's log"),
("settings.storage.cache", "Web content cache"),
("settings.storage.recovery", "Recovery copies"),
("settings.storage.clear", "Clear"),
("settings.storage.clear_later", "Clear at next start"),
@@ -92,7 +91,6 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("settings.git.bad", "{path} 不是 2.40 或更新的 Git:{error}"),
("settings.storage", "存储"),
("settings.storage.log", "本应用的日志"),
("settings.storage.cache", "网页内容缓存"),
("settings.storage.recovery", "恢复副本"),
("settings.storage.clear", "清除"),
("settings.storage.clear_later", "下次启动时清除"),
+1 -4
View File
@@ -74,9 +74,7 @@ impl SettingsView {
None => {}
Some(Err(error)) => section = section.row(Banner::new("storage-unread", Severity::Danger, error.clone())),
Some(Ok(usage)) => {
// Only ElyMark keeps web content.
let shown = usage.iter().filter(|(kind, ..)| *kind != Kind::Cache || self.app == ely_platform::AppId::Mark);
for (kind, bytes, clearing) in shown {
for (kind, bytes, clearing) in usage {
section = section.row(row(*kind, *bytes, *clearing, this.clone(), cx));
}
}
@@ -188,7 +186,6 @@ impl SettingsView {
fn name(kind: Kind, cx: &App) -> SharedString {
match kind {
Kind::Log => text(cx, "settings.storage.log", &[]),
Kind::Cache => text(cx, "settings.storage.cache", &[]),
Kind::Recovery => text(cx, "settings.storage.recovery", &[]),
}
}
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "ely-web-content"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
anyhow.workspace = true
cef.workspace = true
ely-gpui-component.workspace = true
ely-ui.workspace = true
futures.workspace = true
gpui.workspace = true
image.workspace = true
libc.workspace = true
log.workspace = true
serde_json.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2.workspace = true
[target.'cfg(windows)'.dependencies]
windows-sys.workspace = true
[lints]
workspace = true
[dev-dependencies]
ely-platform.workspace = true
tempfile.workspace = true
-317
View File
@@ -1,317 +0,0 @@
//! A live WebView: input, fence, failures, release, paint.
use std::{
net::{TcpListener, UdpSocket},
path::{Path, PathBuf},
time::Duration,
};
use ely_platform::AppId;
use ely_web_content::{Event, WebView};
use gpui::{
App, AppContext as _, AsyncWindowContext, Bounds, Context, ElementInputHandler, Entity,
Focusable as _, InputHandler as _, IntoElement, KeyUpEvent, Keystroke, ParentElement as _,
PlatformInput, Render, Styled as _, WeakEntity, Window, div,
};
/// How many views open and close to show their release.
const CHURN: usize = 20;
/// Views dropped before the pump makes their browsers.
const UNMADE: usize = 5;
/// Views dropped within 3 ms steps of opening.
const EARLY: u64 = 10;
struct Probe {
view: Entity<WebView>,
/// Pages beside the main one, each with its own test.
others: Vec<Entity<WebView>>,
/// The ratio before zoom, to compare after.
before: Option<f64>,
/// Fenced off: nothing may reach it.
server: TcpListener,
/// WebRTC's way out, by UDP; nothing may reach it either.
udp: UdpSocket,
churned: usize,
crashed: bool,
fixtures: PathBuf,
work: PathBuf,
}
fn typed(text: &str) -> Keystroke {
Keystroke {
key_char: Some(text.into()),
..Keystroke::parse(text).expect("a keystroke")
}
}
/// Waits, then runs `then` with the probe and its window.
fn later(
this: WeakEntity<Probe>,
ms: u64,
window: &mut Window,
cx: &mut Context<Probe>,
then: impl FnOnce(&mut Probe, &mut Window, &mut Context<Probe>) + 'static,
) {
cx.spawn_in(window, async move |_, cx: &mut AsyncWindowContext| {
cx.background_executor()
.timer(Duration::from_millis(ms))
.await;
let _ = this.update_in(cx, then);
})
.detach();
}
impl Probe {
fn main_heard(
&mut self,
_: &Entity<WebView>,
event: &Event,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
// Held so the gate sees the page drawn again.
Event::Loaded if self.crashed => {
println!("probe: recovered");
let this = cx.weak_entity();
later(this, 4000, window, cx, |_, _, cx| cx.quit());
}
Event::Loaded => {
let port = self.server.local_addr().expect("an address").port();
let image = format!(
"new Image().src = 'http://127.0.0.1:{port}/pixel?x=1'; \
try {{ new WebSocket('ws://127.0.0.1:{port}/socket'); }} catch (e) {{}} \
try {{ const peer = new RTCPeerConnection({{ iceServers: \
[{{ urls: 'stun:127.0.0.1:{udp}' }}] }}); peer.createDataChannel('x'); \
peer.createOffer().then((offer) => peer.setLocalDescription(offer)); \
}} catch (e) {{}}",
udp = self.udp.local_addr().expect("an address").port()
);
self.view.read(cx).run_js(&image);
self.view.read(cx).run_js("report('loaded')");
}
Event::Query(said, reply) => {
reply.ok("ok");
let read: serde_json::Value = serde_json::from_str(said).expect("json");
self.step(&read, window, cx);
}
Event::Leave(url) => println!("probe: leave {url}"),
Event::Printed(path, ok) => println!("probe: printed {} {ok}", path.display()),
// Recovery comes from the shown Reload button.
Event::Failed(why) => {
println!("probe: main failed");
eprintln!("probe: because {why}");
self.crashed = true;
}
}
}
fn step(&mut self, read: &serde_json::Value, window: &mut Window, cx: &mut Context<Self>) {
let this = cx.weak_entity();
match read["step"].as_str() {
Some("loaded") => {
self.before = read["ratio"].as_f64();
println!("probe: outside={}", read["outside"]);
self.edit(window, cx);
}
Some("edited") => {
println!("probe: value={}", read["value"].as_str().unwrap_or("?"));
println!(
"probe: focused={}",
read["focused"].as_str().unwrap_or("none")
);
println!("probe: fg={}", read["fg"].as_str().unwrap_or(""));
let reached = self.server.accept().is_ok();
println!(
"probe: network={}",
if reached { "reached" } else { "refused" }
);
let heard = self.udp.recv(&mut [0u8; 2048]).is_ok();
println!("probe: udp={}", if heard { "reached" } else { "silent" });
println!("probe: rtc={}", read["rtc"].as_str().unwrap_or("?"));
println!(
"probe: network={}",
if reached { "reached" } else { "refused" }
);
self.view.read(cx).set_zoom(1.25);
later(this, 800, window, cx, |probe, _, cx| {
probe.view.read(cx).run_js("report('zoomed')");
});
}
Some("zoomed") => {
let zoom = read["ratio"].as_f64().unwrap_or(0.0) / self.before.unwrap_or(1.0);
println!("probe: zoom={zoom:.2}");
// A page cannot leave its bundled folder.
self.view
.read(cx)
.run_js("location.href = 'https://example.com/'");
later(this, 800, window, cx, |probe, _, cx| {
probe.view.read(cx).run_js("report('stayed')");
});
}
Some("stayed") => {
println!("probe: stayed");
self.churn(window, cx);
}
other => println!("probe: unknown step {other:?}"),
}
}
/// Compose 你好, commit it, then type A.
fn edit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let view = self.view.clone();
// Input runs outside this update; GPUI redraws during it.
cx.spawn_in(window, async move |_, cx| {
let typed_in = cx.update(|window, cx| {
let focus = view.read(cx).focus_handle(cx);
window.focus(&focus, cx);
let mut ime = ElementInputHandler::new(Bounds::default(), view.clone());
ime.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
ime.replace_and_mark_text_in_range(Some(0..2), "你好", None, window, cx);
ime.replace_text_in_range(None, "你好", window, cx);
let stroke = typed("a");
window.dispatch_keystroke(stroke.clone(), cx);
window.dispatch_event(PlatformInput::KeyUp(KeyUpEvent { keystroke: stroke }), cx);
});
if typed_in.is_err() {
return;
}
cx.background_executor()
.timer(Duration::from_millis(500))
.await;
view.update(cx, |view, _| view.run_js("report('edited')"));
})
.detach();
}
/// Opens and drops pages; each must free its memory.
fn churn(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let page = self.fixtures.join("probe.html");
if self.churned == CHURN {
println!("probe: closed {CHURN}");
for _ in 0..UNMADE {
drop(WebView::open(&page, &self.fixtures, window, cx).expect("a view"));
}
for step in 0..EARLY {
let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view");
self.others.push(view.clone());
later(cx.weak_entity(), step * 3, window, cx, move |probe, _, _| {
probe.others.retain(|other| *other != view);
});
}
println!("probe: dropped {UNMADE} unmade, {EARLY} early");
let this = cx.weak_entity();
later(this, 2000, window, cx, |_, _, _| {
// The log marks what freed before the hold.
log::info!("probe: hold");
println!("probe: hold");
});
return;
}
self.churned += 1;
let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view");
cx.subscribe_in(&view, window, |probe, view, event, window, cx| {
if matches!(event, Event::Loaded) {
probe.others.retain(|other| other != view);
probe.churn(window, cx);
}
})
.detach();
self.others.push(view);
}
/// A page named like a URL, one gone, one inert.
fn edges(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let page = self.fixtures.join("inert.html");
let inert = WebView::open_inert(&page, &self.fixtures, window, cx).expect("a view");
cx.subscribe_in(&inert, window, |_, _, event, _, _| match event {
Event::Loaded => println!("probe: inert loaded"),
Event::Query(_, reply) => {
println!("probe: inert ran");
reply.ok("");
}
_ => {}
})
.detach();
// The same page with scripts: it must run.
let scripted = WebView::open(&page, &self.fixtures, window, cx).expect("a view");
cx.subscribe_in(&scripted, window, |_, _, event, window, cx| {
if let Event::Query(_, reply) = event {
println!("probe: scripted ran");
reply.ok("");
later(cx.weak_entity(), 2000, window, cx, |_, _, _| println!("probe: inert checked"));
}
})
.detach();
self.others.extend([inert, scripted]);
let hash = self.work.join("hash");
std::fs::create_dir_all(&hash).expect("makes");
std::fs::copy(
self.fixtures.join("probe.html"),
hash.join("name#part.html"),
)
.expect("copies");
let named = WebView::open(&hash.join("name#part.html"), &hash, window, cx).expect("a view");
cx.subscribe_in(&named, window, |_, _, event, _, _| match event {
Event::Loaded => println!("probe: hash loaded"),
Event::Failed(_) => println!("probe: hash failed"),
_ => {}
})
.detach();
let gone = self.work.join("gone");
std::fs::create_dir_all(&gone).expect("makes");
let page = gone.join("page.html");
std::fs::copy(self.fixtures.join("probe.html"), &page).expect("copies");
let missing = WebView::open(&page, &gone, window, cx).expect("a view");
std::fs::remove_file(&page).expect("removes");
cx.subscribe_in(&missing, window, |_, _, event, _, _| match event {
Event::Loaded => println!("probe: missing loaded"),
Event::Failed(_) => println!("probe: missing failed"),
_ => {}
})
.detach();
self.others.extend([named, missing]);
}
}
impl Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.view.clone())
}
}
fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity<ely_ui::Root> {
let work = tempfile::tempdir().expect("a temp dir").keep();
ely_web_content::start(&work, cx).expect("CEF starts");
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/web");
let fixtures = std::fs::canonicalize(fixtures).expect("fixtures");
let view = WebView::open(&fixtures.join("probe.html"), &fixtures, window, cx).expect("a view");
let server = TcpListener::bind("127.0.0.1:0").expect("a port");
server.set_nonblocking(true).expect("nonblocking");
let udp = UdpSocket::bind("127.0.0.1:0").expect("a port");
udp.set_nonblocking(true).expect("nonblocking");
let probe = cx.new(|cx| {
cx.subscribe_in(&view, window, Probe::main_heard).detach();
let mut probe = Probe {
view,
others: Vec::new(),
before: None,
server,
udp,
churned: 0,
crashed: false,
fixtures,
work,
};
probe.edges(window, cx);
probe
});
ely_ui::root(app, Some(probe.into()), window, cx)
}
fn main() {
if let Some(code) = ely_web_content::subprocess() {
std::process::exit(code);
}
ely_ui::run(AppId::Mark, open);
}
@@ -1,9 +0,0 @@
//! CEF's helper processes inside an app bundle.
fn main() {
let Some(code) = ely_web_content::subprocess() else {
eprintln!("ely-web-helper: started without --type; CEF starts it");
std::process::exit(2);
};
std::process::exit(code);
}
-92
View File
@@ -1,92 +0,0 @@
//! AppKit's Command edit keys; off-screen Blink never gets them.
use cef::*;
const SHIFT: u32 = 1 << 1;
const CONTROL: u32 = 1 << 2;
const ALT: u32 = 1 << 3;
const COMMAND: u32 = 1 << 7;
/// What AppKit's standard bindings do, as page script.
pub(crate) fn script(windows_key: i32, modifiers: u32) -> Option<String> {
if modifiers & COMMAND == 0 || modifiers & (CONTROL | ALT) != 0 {
return None;
}
let alter = if modifiers & SHIFT != 0 {
"extend"
} else {
"move"
};
let (direction, unit) = match windows_key {
0x25 => ("backward", "lineboundary"),
0x27 => ("forward", "lineboundary"),
0x26 => ("backward", "documentboundary"),
0x28 => ("forward", "documentboundary"),
// Delete to the line's start or end.
0x08 | 0x2E if modifiers & SHIFT == 0 => {
let (direction, command) = match windows_key {
0x08 => ("backward", "delete"),
_ => ("forward", "forwardDelete"),
};
return Some(format!(
"{{const s=getSelection();if(s.isCollapsed)s.modify('extend','{direction}','lineboundary');document.execCommand('{command}')}}"
));
}
_ => return None,
};
Some(format!(
"{{const s=getSelection();s.modify('{alter}','{direction}','{unit}');s.focusNode?.parentElement?.scrollIntoView({{block:'nearest'}})}}"
))
}
wrap_keyboard_handler! {
pub(crate) struct Keys;
impl KeyboardHandler {
fn on_key_event(
&self,
browser: Option<&mut Browser>,
event: Option<&KeyEvent>,
_os_event: *mut u8,
) -> ::std::os::raw::c_int {
let (Some(browser), Some(event)) = (browser, event) else { return 0 };
let down = matches!(event.type_, KeyEventType::RAWKEYDOWN | KeyEventType::KEYDOWN);
if !down || event.focus_on_editable_field == 0 {
return 0;
}
let Some(script) = script(event.windows_key_code, event.modifiers) else { return 0 };
let Some(frame) = browser.focused_frame() else { return 0 };
log::debug!("web: AppKit edit key {}", event.windows_key_code);
frame.execute_java_script(Some(&CefString::from(script.as_str())), None, 0);
1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_keys_only() {
assert!(
script(0x28, COMMAND)
.unwrap()
.contains("'move','forward','documentboundary'")
);
assert!(
script(0x25, COMMAND | SHIFT)
.unwrap()
.contains("'extend','backward','lineboundary'")
);
assert!(
script(0x08, COMMAND)
.unwrap()
.contains("execCommand('delete')")
);
assert!(script(0x2E, COMMAND).unwrap().contains("forwardDelete"));
assert_eq!(script(0x28, 0), None);
assert_eq!(script(0x28, COMMAND | ALT), None);
assert_eq!(script(0x41, COMMAND), None);
}
}
-356
View File
@@ -1,356 +0,0 @@
//! CEF itself: the framework, its processes, its message pump.
use std::{
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
time::Duration,
};
use anyhow::Result;
use cef::{args::Args, *};
use futures::{FutureExt as _, StreamExt as _, channel::mpsc};
use gpui::{App, AsyncApp, Global, WeakEntity};
use crate::{handlers, surface::Shared, view::WebView};
/// The framework beside the app, else the fetched one.
#[cfg(target_os = "macos")]
fn framework_dir() -> Result<PathBuf> {
use anyhow::Context as _;
const NAME: &str = "Chromium Embedded Framework.framework";
let exe = std::env::current_exe()?;
let mut places: Vec<PathBuf> = Vec::new();
if let Some(dir) = std::env::var_os("ELY_CEF") {
places.push(PathBuf::from(dir).join(NAME));
}
if let Some(contents) = exe.parent().and_then(Path::parent) {
places.push(contents.join("Frameworks").join(NAME));
// A helper sits in Frameworks/<App> Helper.app/Contents/MacOS.
if let Some(frameworks) = contents.parent().and_then(Path::parent) {
places.push(frameworks.join(NAME));
}
}
if let Some(built) = option_env!("CEF_PATH") {
places.push(Path::new(built).join(NAME));
}
places
.into_iter()
.find(|place| place.is_dir())
.with_context(|| format!("no {NAME} beside {} or in engines/cef", exe.display()))
}
static LOADED: OnceLock<std::result::Result<PathBuf, String>> = OnceLock::new();
/// Loads CEF once and sets its API version.
fn load() -> Result<PathBuf> {
LOADED
.get_or_init(|| {
let dir = open()?;
// Versioned calls exit the process until this runs.
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
log::info!("web: CEF loaded from {}", dir.display());
Ok(dir)
})
.clone()
.map_err(anyhow::Error::msg)
}
/// Loads the framework; its folder.
#[cfg(target_os = "macos")]
fn open() -> std::result::Result<PathBuf, String> {
let dir = framework_dir().map_err(|error| format!("{error:#}"))?;
let library = dir.join("Chromium Embedded Framework");
let name = std::ffi::CString::new(library.as_os_str().as_encoded_bytes()).map_err(|error| error.to_string())?;
if unsafe { load_library(Some(&*name.as_ptr().cast())) } != 1 {
return Err(format!("CEF did not load from {}", library.display()));
}
Ok(dir)
}
/// The folder of the libcef.dll this process linked.
#[cfg(windows)]
fn open() -> std::result::Result<PathBuf, String> {
use std::os::windows::ffi::OsStringExt as _;
use windows_sys::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
let name: Vec<u16> = "libcef.dll".encode_utf16().chain([0]).collect();
let module = unsafe { GetModuleHandleW(name.as_ptr()) };
if module.is_null() {
return Err(format!("libcef.dll is not loaded: {}", std::io::Error::last_os_error()));
}
let mut path = vec![0u16; 32_768];
let len = unsafe { GetModuleFileNameW(module, path.as_mut_ptr(), path.len() as u32) } as usize;
if len == 0 || len == path.len() {
return Err(format!("libcef.dll has no path: {}", std::io::Error::last_os_error()));
}
let file = PathBuf::from(std::ffi::OsString::from_wide(&path[..len]));
file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display()))
}
/// The folder of the libcef.so this process linked.
#[cfg(target_os = "linux")]
fn open() -> std::result::Result<PathBuf, String> {
use std::os::unix::ffi::OsStrExt as _;
let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
let entry = sys::cef_api_hash as *const libc::c_void;
if unsafe { libc::dladdr(entry, &mut info) } == 0 || info.dli_fname.is_null() {
return Err("libcef.so is not loaded".into());
}
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
let file = PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()));
file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display()))
}
/// In a CEF helper process, runs it to its end.
pub fn subprocess() -> Option<i32> {
// CEF's own parser needs the framework; plain args do not.
if !std::env::args().any(|arg| arg.starts_with("--type=")) {
return None;
}
if let Err(error) = load() {
eprintln!("web: {error:#}");
return Some(1);
}
let args = Args::new();
let mut app = handlers::app();
let code = execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
Some(code)
}
/// The longest the pump waits between turns, as in cefclient.
const FALLBACK: Duration = Duration::from_millis(1000 / 30);
/// Pump work CEF asked for, from any thread.
static WAKE: Mutex<Option<mpsc::UnboundedSender<i64>>> = Mutex::new(None);
/// CEF wants its loop run within `delay_ms`.
pub(crate) fn schedule(delay_ms: i64) {
if let Some(wake) = WAKE.lock().expect("the pump lock").as_ref() {
let _ = wake.unbounded_send(delay_ms);
}
}
/// The live views the pump hands frames to.
#[derive(Default)]
pub(crate) struct Engine {
pub views: Vec<WeakEntity<WebView>>,
}
impl Global for Engine {}
thread_local! {
/// CEF calls asked inside GPUI's updates, run after them.
static LATER: std::cell::RefCell<Vec<Box<dyn FnOnce()>>> = Default::default();
}
/// Runs `work` on the pump's next turn.
pub(crate) fn later(work: impl FnOnce() + 'static) {
LATER.with_borrow_mut(|queue| queue.push(Box::new(work)));
schedule(0);
}
/// In asked order; work may ask for more.
fn run_later() {
loop {
let batch = LATER.with_borrow_mut(std::mem::take);
if batch.is_empty() {
return;
}
for work in batch {
work();
}
}
}
/// Browsers asked for, made on the pump's next turn.
static PENDING: Mutex<Vec<(Shared, String)>> = Mutex::new(Vec::new());
/// Why CEF could not start; every view is told.
static FAILED: Mutex<Option<String>> = Mutex::new(None);
pub(crate) fn failed() -> Option<String> {
FAILED.lock().expect("the start lock").clone()
}
/// Queues a browser for `surface` at `url`.
pub(crate) fn ask_browser(surface: Shared, url: String) {
PENDING
.lock()
.expect("the pending lock")
.push((surface, url));
schedule(0);
}
/// Outside GPUI's updates: CEF may spin the run loop.
fn make_browsers() {
let pending = std::mem::take(&mut *PENDING.lock().expect("the pending lock"));
for (surface, url) in pending {
if surface.lock().expect("the surface lock").closed {
continue;
}
// Off-screen pages work only in CEF's Alloy style.
let info = WindowInfo {
windowless_rendering_enabled: true as _,
runtime_style: RuntimeStyle::ALLOY,
..Default::default()
};
let scripts = match surface.lock().expect("the surface lock").scripts {
true => State::ENABLED,
false => State::DISABLED,
};
let settings = BrowserSettings {
windowless_frame_rate: 60,
javascript: scripts,
..Default::default()
};
// Made at once: no view ever waits on a browser.
let made = browser_host_create_browser_sync(
Some(&info),
Some(&mut handlers::client(&surface)),
Some(&url.as_str().into()),
Some(&settings),
None,
None,
);
match made {
Some(browser) => {
handlers::adopt(&browser, surface.clone());
surface.lock().expect("the surface lock").browser = Some(browser);
}
None => {
surface.lock().expect("the surface lock").failed =
Some(format!("CEF made no browser for {url}"))
}
}
}
}
/// Starts CEF in this process; once per app.
pub fn start(data: &Path, cx: &mut App) -> Result<()> {
if cx.has_global::<Engine>() {
return Ok(());
}
crate::input::bind(cx);
let framework = load()?;
#[cfg(target_os = "macos")]
crate::mac::adopt_app_protocol()?;
let exe = std::env::current_exe()?;
// Bundled, CEF finds its helper apps; else this binary helps.
let bundled = cfg!(target_os = "macos")
&& framework.parent().is_some_and(|dir| {
let name = exe.file_name().unwrap_or_default().to_string_lossy();
dir.join(format!("{name} Helper.app")).is_dir()
});
let helper = if bundled {
String::new()
} else {
exe.to_string_lossy().into_owned()
};
let cache = data.join("web");
std::fs::create_dir_all(&cache)?;
let settings = Settings {
windowless_rendering_enabled: true as _,
external_message_pump: true as _,
no_sandbox: true as _,
browser_subprocess_path: helper.as_str().into(),
#[cfg(target_os = "macos")]
framework_dir_path: framework.to_string_lossy().as_ref().into(),
#[cfg(not(target_os = "macos"))]
resources_dir_path: framework.to_string_lossy().as_ref().into(),
#[cfg(not(target_os = "macos"))]
locales_dir_path: framework.join("locales").to_string_lossy().as_ref().into(),
root_cache_path: cache.to_string_lossy().as_ref().into(),
persist_session_cookies: false as _,
log_severity: LogSeverity::WARNING,
..Default::default()
};
let (wake, mut asked) = mpsc::unbounded();
*WAKE.lock().expect("the pump lock") = Some(wake);
cx.set_global(Engine::default());
cx.spawn(async move |cx: &mut AsyncApp| {
// Not inside an update: CEF may run AppKit's loop here.
let args = Args::new();
let mut app = handlers::app();
let started = initialize(
Some(args.as_main_args()),
Some(&settings),
Some(&mut app),
std::ptr::null_mut(),
);
if started != 1 {
let why = held(&cache).unwrap_or_else(|| "CEF did not initialize".into());
log::error!("web: {why}");
*FAILED.lock().expect("the start lock") = Some(why);
cx.update(crate::view::deliver);
return;
}
log::info!("web: CEF started, cache {}", cache.display());
let quitting = cx.update(|cx| cx.on_app_quit(|_| async { stop() }));
quitting.detach();
let mut due: Option<Duration> = Some(Duration::ZERO);
loop {
let timer = match due {
Some(delay) => cx.background_executor().timer(delay).boxed_local(),
None => futures::future::pending().boxed_local(),
};
futures::select_biased! {
delay = asked.next() => match delay {
Some(delay) => {
let delay = Duration::from_millis(delay.max(0) as u64);
due = Some(due.map_or(delay, |now| now.min(delay)));
continue;
}
None => return,
},
_ = timer.fuse() => {}
}
run_later();
make_browsers();
do_message_loop_work();
cx.update(crate::view::deliver);
// As cefclient: CEF does not ask for all work.
due = Some(FALLBACK);
}
})
.detach();
Ok(())
}
/// Stops CEF as the app quits.
fn stop() {
WAKE.lock().expect("the pump lock").take();
shutdown();
log::info!("web: CEF stopped");
}
/// Why CEF would refuse this cache: another app holds it.
#[cfg(unix)]
fn held(cache: &Path) -> Option<String> {
let lock = std::fs::read_link(cache.join("SingletonLock")).ok()?;
let lock = lock.to_string_lossy();
let pid: i32 = lock.rsplit('-').next()?.parse().ok()?;
// Only a live process holds it.
let alive = unsafe { libc::kill(pid, 0) } == 0;
alive.then(|| format!("another copy (process {pid}) is using {}", cache.display()))
}
/// Chromium's Windows lock names no process.
#[cfg(windows)]
fn held(_cache: &Path) -> Option<String> {
None
}
#[cfg(test)]
mod tests {
#[test]
fn cef_loads_with_its_api_version() {
let dir = super::load().expect("CEF loads");
#[cfg(not(target_os = "macos"))]
assert!(dir.join("locales").is_dir(), "{} holds CEF's files", dir.display());
// Without the version this call exits the process.
assert!(cef::command_line_create().is_some());
}
}
-88
View File
@@ -1,88 +0,0 @@
//! The fence: where pages go, what they load.
use cef::{wrapper::message_router::MessageRouterBrowserSideHandlerCallbacks, *};
use crate::{handlers::router, surface::Shared};
wrap_request_handler! {
pub(crate) struct Requests {
surface: Shared,
}
impl RequestHandler {
fn on_before_browse(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
request: Option<&mut Request>,
user_gesture: ::std::os::raw::c_int,
_is_redirect: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int {
let url = request.map(|r| CefString::from(&r.url()).to_string()).unwrap_or_default();
let mut surface = self.surface.lock().expect("the surface lock");
// Content stays bundled; a clicked link leaves for the system.
if !surface.navigable(&url) {
if user_gesture == 1 {
surface.leaving.push(url);
}
return 1;
}
drop(surface);
router().on_before_browse(browser.map(|b| b.clone()), frame.map(|f| f.clone()));
0
}
fn resource_request_handler(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_request: Option<&mut Request>,
_is_navigation: ::std::os::raw::c_int,
_is_download: ::std::os::raw::c_int,
_request_initiator: Option<&CefString>,
_disable_default_handling: Option<&mut ::std::os::raw::c_int>,
) -> Option<ResourceRequestHandler> {
Some(Resources::new(self.surface.clone()))
}
fn on_render_process_terminated(
&self,
browser: Option<&mut Browser>,
status: TerminationStatus,
error_code: ::std::os::raw::c_int,
_error_string: Option<&CefString>,
) {
router().on_render_process_terminated(browser.map(|b| b.clone()));
self.surface.lock().expect("the surface lock").failed =
Some(format!("the page stopped: {status:?} ({error_code})"));
}
}
}
wrap_resource_request_handler! {
struct Resources {
surface: Shared,
}
impl ResourceRequestHandler {
// Every request, scripts and images too, meets the fence.
fn on_before_resource_load(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
request: Option<&mut Request>,
_callback: Option<&mut Callback>,
) -> ReturnValue {
let Some(request) = request else {
return ReturnValue::CANCEL;
};
let url = CefString::from(&request.url()).to_string();
let image = request.resource_type() == ResourceType::IMAGE;
if self.surface.lock().expect("the surface lock").loadable(&url, image) {
return ReturnValue::CONTINUE;
}
log::warn!("web: a request outside the allowed folders was refused");
ReturnValue::CANCEL
}
}
}
-482
View File
@@ -1,482 +0,0 @@
//! What CEF calls back: per process, and per browser.
use std::{
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
};
use cef::{
wrapper::message_router::{
BrowserSideCallback, BrowserSideHandler, BrowserSideRouter, MessageRouterBrowserSide,
MessageRouterBrowserSideHandlerCallbacks, MessageRouterConfig, MessageRouterRendererSide,
MessageRouterRendererSideHandlerCallbacks, RendererSideRouter,
},
*,
};
use crate::{fence::Requests, surface::Shared};
/// The page calls `window.elyQuery({request, onSuccess, onFailure})`.
fn config() -> MessageRouterConfig {
MessageRouterConfig {
js_query_function: "elyQuery".into(),
js_cancel_function: "elyQueryCancel".into(),
..Default::default()
}
}
/// Each browser's surface, by CEF's browser id.
fn surfaces() -> &'static Mutex<HashMap<i32, Shared>> {
static SURFACES: OnceLock<Mutex<HashMap<i32, Shared>>> = OnceLock::new();
SURFACES.get_or_init(Default::default)
}
pub(crate) fn adopt(browser: &Browser, surface: Shared) {
surfaces()
.lock()
.expect("the surface lock")
.insert(browser.identifier(), surface);
}
fn surface_of(browser: Option<&Browser>) -> Option<Shared> {
let id = browser?.identifier();
surfaces()
.lock()
.expect("the surface lock")
.get(&id)
.cloned()
}
/// Queries go to the view whose page asked.
struct Queries;
impl BrowserSideHandler for Queries {
fn on_query_str(
&self,
browser: Option<Browser>,
_frame: Option<Frame>,
_query_id: i64,
request: &str,
_persistent: bool,
callback: Arc<Mutex<dyn BrowserSideCallback>>,
) -> bool {
let Some(surface) = surface_of(browser.as_ref()) else {
return false;
};
surface
.lock()
.expect("the surface lock")
.queries
.push((request.to_string(), callback));
true
}
}
pub(crate) fn router() -> &'static Arc<BrowserSideRouter> {
static ROUTER: OnceLock<Arc<BrowserSideRouter>> = OnceLock::new();
ROUTER.get_or_init(|| {
let router = BrowserSideRouter::new(config());
router.add_handler(Arc::new(Queries), true);
router
})
}
fn renderer_router() -> &'static Arc<RendererSideRouter> {
static ROUTER: OnceLock<Arc<RendererSideRouter>> = OnceLock::new();
ROUTER.get_or_init(|| RendererSideRouter::new(config()))
}
/// WebRTC reaches the network by UDP; pages lose it.
const NO_PEERS: &str = "for (const name of ['RTCPeerConnection', 'webkitRTCPeerConnection', \
'RTCDataChannel', 'RTCSessionDescription', 'RTCIceCandidate', 'RTCRtpSender', \
'RTCRtpReceiver', 'RTCRtpTransceiver']) delete globalThis[name];";
wrap_app! {
struct ElyApp;
impl App {
fn on_before_command_line_processing(
&self,
_process_type: Option<&CefString>,
command_line: Option<&mut CommandLine>,
) {
let Some(line) = command_line else { return };
// No keychain prompt; the content keeps no secrets.
line.append_switch(Some(&"use-mock-keychain".into()));
line.append_switch(Some(&"disable-extensions".into()));
// Offline pages: all traffic meets a dead proxy.
line.append_switch_with_value(Some(&"proxy-server".into()), Some(&"127.0.0.1:9".into()));
line.append_switch_with_value(
Some(&"proxy-bypass-list".into()),
Some(&"<-loopback>".into()),
);
// WebRTC sends UDP past proxies; it may use proxies only.
line.append_switch_with_value(
Some(&"force-webrtc-ip-handling-policy".into()),
Some(&"disable_non_proxied_udp".into()),
);
}
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(Pump::new())
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(Renderer::new())
}
}
}
pub(crate) fn app() -> cef::App {
ElyApp::new()
}
wrap_browser_process_handler! {
struct Pump;
impl BrowserProcessHandler {
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
crate::engine::schedule(delay_ms);
}
}
}
wrap_render_process_handler! {
struct Renderer;
impl RenderProcessHandler {
fn on_context_created(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
// Before any page script: no peer connections at all.
if let Some(context) = context.as_ref() {
let code = CefString::from(NO_PEERS);
let (mut value, mut thrown) = (None, None);
if context.eval(Some(&code), None, 0, Some(&mut value), Some(&mut thrown)) != 1 {
eprintln!("web: WebRTC could not be removed from a page");
}
}
renderer_router().on_context_created(
browser.map(|b| b.clone()),
frame.map(|f| f.clone()),
context.map(|c| c.clone()),
);
}
fn on_context_released(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
renderer_router().on_context_released(
browser.map(|b| b.clone()),
frame.map(|f| f.clone()),
context.map(|c| c.clone()),
);
}
fn on_process_message_received(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
source_process: ProcessId,
message: Option<&mut ProcessMessage>,
) -> ::std::os::raw::c_int {
renderer_router().on_process_message_received(
browser.map(|b| b.clone()),
frame.map(|f| f.clone()),
Some(source_process),
message.map(|m| m.clone()),
) as _
}
}
}
wrap_render_handler! {
struct Paint {
surface: Shared,
}
impl RenderHandler {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
let Some(rect) = rect else { return };
let surface = self.surface.lock().expect("the surface lock");
// CEF needs a page at least one point square.
rect.width = (surface.size.0.round() as i32).max(1);
rect.height = (surface.size.1.round() as i32).max(1);
}
fn screen_info(
&self,
_browser: Option<&mut Browser>,
screen_info: Option<&mut ScreenInfo>,
) -> ::std::os::raw::c_int {
let Some(info) = screen_info else { return 0 };
info.device_scale_factor = self.surface.lock().expect("the surface lock").scale;
1
}
fn on_paint(
&self,
_browser: Option<&mut Browser>,
type_: PaintElementType,
_dirty_rects: Option<&[Rect]>,
buffer: *const u8,
width: ::std::os::raw::c_int,
height: ::std::os::raw::c_int,
) {
// Popups (select menus) are not drawn; pages avoid them.
if type_ != PaintElementType::VIEW || buffer.is_null() || width <= 0 || height <= 0 {
return;
}
let length = width as usize * height as usize * 4;
let bgra = unsafe { std::slice::from_raw_parts(buffer, length) }.to_vec();
self.surface.lock().expect("the surface lock").frame =
Some((width as u32, height as u32, bgra));
}
fn on_ime_composition_range_changed(
&self,
_browser: Option<&mut Browser>,
_selected_range: Option<&Range>,
character_bounds: Option<&[Rect]>,
) {
let bounds = character_bounds.unwrap_or_default();
self.surface.lock().expect("the surface lock").ime = bounds
.iter()
.map(|r| (r.x as f32, r.y as f32, r.width as f32, r.height as f32))
.collect();
}
}
}
wrap_display_handler! {
struct Display {
surface: Shared,
}
impl DisplayHandler {
fn on_console_message(
&self,
_browser: Option<&mut Browser>,
level: LogSeverity,
_message: Option<&CefString>,
_source: Option<&CefString>,
line: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int {
// Words and sources a document may forge stay out.
if level.get_raw() >= LogSeverity::WARNING.get_raw() {
log::warn!("web: the page warned at line {line}");
}
0
}
fn on_cursor_change(
&self,
_browser: Option<&mut Browser>,
_cursor: CursorHandle,
type_: CursorType,
_custom_cursor_info: Option<&CursorInfo>,
) -> ::std::os::raw::c_int {
self.surface.lock().expect("the surface lock").cursor = type_;
1
}
}
}
wrap_load_handler! {
struct Load {
surface: Shared,
}
impl LoadHandler {
fn on_load_start(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_transition_type: TransitionType,
) {
if frame.is_some_and(|frame| frame.is_main() == 1) {
self.surface.lock().expect("the surface lock").errored = false;
}
}
fn on_load_error(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
error_code: Errorcode,
error_text: Option<&CefString>,
failed_url: Option<&CefString>,
) {
// A stopped load is no failure; a fenced one is.
if frame.is_none_or(|frame| frame.is_main() != 1) || error_code == Errorcode::ABORTED {
return;
}
let text = error_text.map(CefString::to_string).unwrap_or_default();
let url = failed_url.map(CefString::to_string).unwrap_or_default();
let mut surface = self.surface.lock().expect("the surface lock");
surface.errored = true;
surface.failed = Some(format!("{text} ({}) {url}", error_code.get_raw()));
}
fn on_load_end(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_http_status_code: ::std::os::raw::c_int,
) {
if frame.is_some_and(|frame| frame.is_main() == 1) {
let mut surface = self.surface.lock().expect("the surface lock");
surface.loaded = !surface.errored;
}
}
}
}
wrap_life_span_handler! {
struct Life {
surface: Shared,
}
impl LifeSpanHandler {
fn on_after_created(&self, browser: Option<&mut Browser>) {
if let Some(browser) = browser {
log::info!("web: browser {} made", browser.identifier());
}
}
fn on_before_close(&self, browser: Option<&mut Browser>) {
let browser = browser.map(|b| b.clone());
router().on_before_close(browser.clone());
if let Some(browser) = browser {
surfaces().lock().expect("the surface lock").remove(&browser.identifier());
}
// The browser holds these handlers; they hold the surface.
self.surface.lock().expect("the surface lock").browser = None;
}
}
}
wrap_client! {
struct Handlers {
paint: RenderHandler,
display: DisplayHandler,
load: LoadHandler,
life: LifeSpanHandler,
requests: RequestHandler,
keys: Option<KeyboardHandler>,
}
impl Client {
fn keyboard_handler(&self) -> Option<KeyboardHandler> {
self.keys.clone()
}
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.paint.clone())
}
fn display_handler(&self) -> Option<DisplayHandler> {
Some(self.display.clone())
}
fn load_handler(&self) -> Option<LoadHandler> {
Some(self.load.clone())
}
fn life_span_handler(&self) -> Option<LifeSpanHandler> {
Some(self.life.clone())
}
fn request_handler(&self) -> Option<RequestHandler> {
Some(self.requests.clone())
}
fn on_process_message_received(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
source_process: ProcessId,
message: Option<&mut ProcessMessage>,
) -> ::std::os::raw::c_int {
router().on_process_message_received(
browser.map(|b| b.clone()),
frame.map(|f| f.clone()),
source_process,
message.map(|m| m.clone()),
) as _
}
}
}
/// A browser's handlers, all sharing its surface.
pub(crate) fn client(surface: &Shared) -> cef::Client {
Handlers::new(
Paint::new(surface.clone()),
Display::new(surface.clone()),
Load::new(surface.clone()),
Life::new(surface.clone()),
Requests::new(surface.clone()),
keys(),
)
}
#[cfg(target_os = "macos")]
fn keys() -> Option<KeyboardHandler> {
Some(crate::edit::Keys::new())
}
/// Elsewhere Chromium binds these keys itself.
#[cfg(not(target_os = "macos"))]
fn keys() -> Option<KeyboardHandler> {
None
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex, OnceLock};
use cef::{CefString, ImplDisplayHandler, LogSeverity};
struct Kept;
fn kept() -> &'static Mutex<Vec<String>> {
static KEPT: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
KEPT.get_or_init(Mutex::default)
}
impl log::Log for Kept {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
kept().lock().expect("the kept lock").push(record.args().to_string());
}
fn flush(&self) {}
}
#[test]
fn a_pages_words_and_sources_never_reach_the_log() {
log::set_boxed_logger(Box::new(Kept)).expect("the only logger");
log::set_max_level(log::LevelFilter::Trace);
let surface = crate::surface::Surface::new(std::env::temp_dir(), 1.0);
let display = super::Display::new(Arc::new(Mutex::new(surface)));
let said = CefString::from("ely: M11_SECRET");
let at = CefString::from("file:///content/M11_SECRET.js");
for level in [LogSeverity::WARNING, LogSeverity::ERROR, LogSeverity::INFO] {
display.on_console_message(None, level, Some(&said), Some(&at), 7);
}
let records = kept().lock().expect("the kept lock").clone();
let warned = records.iter().filter(|said| *said == "web: the page warned at line 7");
assert_eq!(warned.count(), 2, "warnings and errors stay seen: {records:#?}");
assert!(!records.iter().any(|said| said.contains("M11_SECRET")), "{records:#?}");
}
}
-344
View File
@@ -1,344 +0,0 @@
//! GPUI's mouse, keys, focus and IME, as CEF events.
use std::ops::Range;
use cef::*;
use gpui::{
App, Bounds, Context, Div, Entity, EntityInputHandler, InteractiveElement as _, KeyDownEvent,
KeyUpEvent, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollDelta, ScrollWheelEvent, UTF16Selection, Window, point, px, size,
};
use crate::{keys, view::WebView};
gpui::actions!(web, [PageTab, PageBackTab]);
/// The page's key context: Tab stays in the page.
const CONTEXT: &str = "WebView";
/// Tab and Shift-Tab go to the page, not to focus.
pub(crate) fn bind(cx: &mut App) {
cx.bind_keys([
gpui::KeyBinding::new("tab", PageTab, Some(CONTEXT)),
gpui::KeyBinding::new("shift-tab", PageBackTab, Some(CONTEXT)),
]);
}
/// CEF's `cef_event_flags_t` for these modifiers.
pub(crate) fn flags(modifiers: &Modifiers) -> u32 {
let mut flags = 0;
for (on, bit) in [
(modifiers.shift, 1 << 1),
(modifiers.control, 1 << 2),
(modifiers.alt, 1 << 3),
(modifiers.platform, 1 << 7),
] {
if on {
flags |= bit;
}
}
flags
}
fn button(button: MouseButton) -> Option<(MouseButtonType, u32)> {
match button {
MouseButton::Left => Some((MouseButtonType::LEFT, 1 << 4)),
MouseButton::Middle => Some((MouseButtonType::MIDDLE, 1 << 5)),
MouseButton::Right => Some((MouseButtonType::RIGHT, 1 << 6)),
_ => None,
}
}
impl WebView {
/// A window point as a page point.
fn at(&self, position: Point<Pixels>, modifiers: u32) -> MouseEvent {
let local = position - self.bounds.origin;
MouseEvent {
x: f32::from(local.x).round() as i32,
y: f32::from(local.y).round() as i32,
modifiers,
}
}
fn mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
window.focus(&self.focus, cx);
let Some((kind, held)) = button(event.button) else {
return;
};
let at = self.at(event.position, flags(&event.modifiers) | held);
let count = event.click_count as i32;
self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, false as _, count));
}
fn mouse_up(&mut self, event: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
let Some((kind, _)) = button(event.button) else {
return;
};
let at = self.at(event.position, flags(&event.modifiers));
let count = event.click_count as i32;
self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, true as _, count));
}
fn mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, _: &mut Context<Self>) {
let held = event
.pressed_button
.and_then(button)
.map_or(0, |(_, bit)| bit);
let at = self.at(event.position, flags(&event.modifiers) | held);
self.on_host(move |host| host.send_mouse_move_event(Some(&at), false as _));
}
fn scroll(&mut self, event: &ScrollWheelEvent, _: &mut Window, _: &mut Context<Self>) {
// Lines as Chromium counts them: forty points.
let delta = match event.delta {
ScrollDelta::Pixels(delta) => (f32::from(delta.x), f32::from(delta.y)),
ScrollDelta::Lines(delta) => (delta.x * 40.0, delta.y * 40.0),
};
let at = self.at(event.position, flags(&event.modifiers));
let (x, y) = (delta.0.round() as i32, delta.1.round() as i32);
self.on_host(move |host| host.send_mouse_wheel_event(Some(&at), x, y));
}
fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
let stroke = &event.keystroke;
// Edit commands Chromium only takes from its own menu.
let edit = stroke.key.clone();
if keys::edit_chord(&stroke.modifiers) && matches!(edit.as_str(), "c" | "x" | "v" | "a") {
// The owner may take a paste, such as an image.
if edit == "v" && self.paste.clone().is_some_and(|take| take(window, cx)) {
return cx.stop_propagation();
}
self.on_browser(move |browser| {
let Some(frame) = browser.main_frame() else {
return;
};
match edit.as_str() {
"c" => frame.copy(),
"x" => frame.cut(),
"v" => frame.paste(),
_ => frame.select_all(),
}
});
return cx.stop_propagation();
}
let Some(code) = keys::code(&stroke.key) else {
return log::debug!("web: no key code for {}", stroke.key);
};
let event = KeyEvent {
modifiers: flags(&stroke.modifiers),
windows_key_code: code.windows,
native_key_code: code.native,
..Default::default()
};
let chord = stroke.modifiers.control || stroke.modifiers.platform;
match (keys::character(&stroke.key), &stroke.key_char) {
// Typed text arrives by the input handler, which sends it.
(None, Some(_)) if !chord => self.typing = Some(event),
(character, typed) => {
let character = character
.or_else(|| typed.as_deref().and_then(|t| t.encode_utf16().next()))
.unwrap_or(0);
self.press(event, character);
}
}
}
/// Tab, with Shift when `back`, down and up.
fn tab(&mut self, back: bool) {
let code = keys::code("tab").expect("tab has a key code");
let event = KeyEvent {
modifiers: if back { 1 << 1 } else { 0 },
windows_key_code: code.windows,
native_key_code: code.native,
..Default::default()
};
self.press(event.clone(), 0x09);
let up = KeyEvent {
type_: KeyEventType::KEYUP,
character: 0x09,
unmodified_character: 0x09,
..event
};
self.on_host(move |host| host.send_key_event(Some(&up)));
}
/// A key down and its character, as cefclient sends them.
fn press(&self, event: KeyEvent, character: u16) {
let mut event = KeyEvent {
type_: KeyEventType::KEYDOWN,
character,
unmodified_character: character,
..event
};
self.on_host(move |host| {
host.send_key_event(Some(&event));
event.type_ = KeyEventType::CHAR;
host.send_key_event(Some(&event));
});
}
fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, _: &mut Context<Self>) {
let stroke = &event.keystroke;
let Some(code) = keys::code(&stroke.key) else {
return;
};
// An up without its character reads as another down.
let character = keys::character(&stroke.key)
.or_else(|| {
stroke
.key_char
.as_deref()
.and_then(|t| t.encode_utf16().next())
})
.or_else(|| stroke.key.encode_utf16().next())
.unwrap_or(0);
let up = KeyEvent {
type_: KeyEventType::KEYUP,
modifiers: flags(&stroke.modifiers),
windows_key_code: code.windows,
native_key_code: code.native,
character,
unmodified_character: character,
..Default::default()
};
self.on_host(move |host| host.send_key_event(Some(&up)));
}
}
/// The view's listeners on its outer element.
pub(crate) fn listen(element: Div, cx: &mut Context<WebView>) -> Div {
element
.key_context(CONTEXT)
.on_action(cx.listener(|view, _: &PageTab, _, _| view.tab(false)))
.on_action(cx.listener(|view, _: &PageBackTab, _, _| view.tab(true)))
.on_mouse_down(MouseButton::Left, cx.listener(WebView::mouse_down))
.on_mouse_down(MouseButton::Right, cx.listener(WebView::mouse_down))
.on_mouse_down(MouseButton::Middle, cx.listener(WebView::mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(WebView::mouse_up))
.on_mouse_up(MouseButton::Right, cx.listener(WebView::mouse_up))
.on_mouse_up(MouseButton::Middle, cx.listener(WebView::mouse_up))
.on_mouse_move(cx.listener(WebView::mouse_move))
.on_scroll_wheel(cx.listener(WebView::scroll))
.on_key_down(cx.listener(WebView::key_down))
.on_key_up(cx.listener(WebView::key_up))
}
/// CEF's page focus follows the view's.
pub(crate) fn follow_focus(view: &Entity<WebView>, window: &mut Window, cx: &mut App) {
view.update(cx, |this, cx| {
let focus = this.focus.clone();
cx.on_focus(&focus, window, |view, _, _| {
view.on_host(|host| host.set_focus(true as _));
})
.detach();
cx.on_blur(&focus, window, |view, _, _| {
view.on_host(|host| host.set_focus(false as _));
})
.detach();
});
}
/// CEF's "no range": both ends at the maximum.
const NO_RANGE: cef::Range = cef::Range {
from: u32::MAX,
to: u32::MAX,
};
fn utf16(text: &str) -> Vec<u16> {
text.encode_utf16().collect()
}
impl EntityInputHandler for WebView {
/// The IME sees none of the page's text.
fn text_for_range(
&mut self,
_: Range<usize>,
_: &mut Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<String> {
None
}
fn selected_text_range(
&mut self,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<UTF16Selection> {
None
}
fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
self.composing.clone()
}
fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
if self.composing.take().is_some() {
self.on_host(|host| host.ime_finish_composing_text(false as _));
}
}
fn replace_text_in_range(
&mut self,
_: Option<Range<usize>>,
text: &str,
_: &mut Window,
_: &mut Context<Self>,
) {
let typed = self.typing.take();
let was = self.composing.take();
let units = utf16(text);
// One character, no composition: a key press, as cefclient.
if let (Some(event), None, [character]) = (typed, was, units.as_slice()) {
return self.press(event, *character);
}
let text = CefString::from(text);
self.on_host(move |host| host.ime_commit_text(Some(&text), Some(&NO_RANGE), 0));
}
fn replace_and_mark_text_in_range(
&mut self,
_: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) {
let length = utf16(new_text).len();
self.composing = (length > 0).then_some(0..length);
let selection = new_selected_range.unwrap_or(length..length);
let range = cef::Range {
from: selection.start as _,
to: selection.end as _,
};
let text = CefString::from(new_text);
self.on_host(move |host| {
host.ime_set_composition(Some(&text), None, Some(&NO_RANGE), Some(&range))
});
}
fn bounds_for_range(
&mut self,
range: Range<usize>,
element: Bounds<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let surface = self.surface.lock().expect("the surface lock");
let (x, y, w, h) = *surface.ime.get(range.start).or(surface.ime.last())?;
Some(Bounds::new(
element.origin + point(px(x), px(y)),
size(px(w), px(h)),
))
}
fn character_index_for_point(
&mut self,
_: Point<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<usize> {
None
}
}
-189
View File
@@ -1,189 +0,0 @@
//! GPUI key names as the codes Chromium reads.
use gpui::Modifiers;
/// Windows' virtual key, and macOS's hardware key.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Code {
pub windows: i32,
pub native: i32,
}
/// Letters, then digits, as macOS numbers its keys.
const MAC_LETTERS: [i32; 26] = [
0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6,
];
const MAC_DIGITS: [i32; 10] = [29, 18, 19, 20, 21, 23, 22, 26, 28, 25];
const MAC_F: [i32; 12] = [122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111];
/// Named keys: GPUI's name, Windows, macOS.
const NAMED: &[(&str, i32, i32)] = &[
("backspace", 0x08, 51),
("tab", 0x09, 48),
("enter", 0x0D, 36),
("escape", 0x1B, 53),
("space", 0x20, 49),
("pageup", 0x21, 116),
("pagedown", 0x22, 121),
("end", 0x23, 119),
("home", 0x24, 115),
("left", 0x25, 123),
("up", 0x26, 126),
("right", 0x27, 124),
("down", 0x28, 125),
("delete", 0x2E, 117),
(";", 0xBA, 41),
("=", 0xBB, 24),
(",", 0xBC, 43),
("-", 0xBD, 27),
(".", 0xBE, 47),
("/", 0xBF, 44),
("`", 0xC0, 50),
("[", 0xDB, 33),
("\\", 0xDC, 42),
("]", 0xDD, 30),
("'", 0xDE, 39),
];
pub fn code(key: &str) -> Option<Code> {
if let Some(&(_, windows, native)) = NAMED.iter().find(|(name, _, _)| *name == key) {
return Some(Code { windows, native });
}
let mut chars = key.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
let n: usize = key.strip_prefix('f')?.parse().ok()?;
let native = *MAC_F.get(n.checked_sub(1)?)?;
return Some(Code {
windows: 0x6F + n as i32,
native,
});
};
match c {
'a'..='z' => Some(Code {
windows: c.to_ascii_uppercase() as i32,
native: MAC_LETTERS[(c as u8 - b'a') as usize],
}),
'0'..='9' => Some(Code {
windows: c as i32,
native: MAC_DIGITS[(c as u8 - b'0') as usize],
}),
_ => None,
}
}
/// The character AppKit gives keys that type none.
#[cfg(target_os = "macos")]
pub fn character(key: &str) -> Option<u16> {
let n: Option<u16> = key.strip_prefix('f').and_then(|n| n.parse().ok());
Some(match key {
"backspace" => 0x7F,
"tab" => 0x09,
"enter" => 0x0D,
"escape" => 0x1B,
"up" => 0xF700,
"down" => 0xF701,
"left" => 0xF702,
"right" => 0xF703,
"delete" => 0xF728,
"home" => 0xF729,
"end" => 0xF72B,
"pageup" => 0xF72C,
"pagedown" => 0xF72D,
_ => match n {
Some(n @ 1..=12) => 0xF703 + n,
_ => return None,
},
})
}
/// The control character other systems type, if any.
#[cfg(not(target_os = "macos"))]
pub fn character(key: &str) -> Option<u16> {
Some(match key {
"backspace" => 0x08,
"tab" => 0x09,
"enter" => 0x0D,
"escape" => 0x1B,
_ => return None,
})
}
/// The chord for cut, copy, paste and select-all here.
pub fn edit_chord(modifiers: &Modifiers) -> bool {
let primary = if cfg!(target_os = "macos") {
modifiers.platform && !modifiers.control
} else {
modifiers.control && !modifiers.platform
};
primary && !modifiers.alt && !modifiers.shift
}
#[cfg(test)]
mod tests {
use super::{Code, code};
#[test]
fn keys_carry_both_codes() {
assert_eq!(
code("a"),
Some(Code {
windows: 0x41,
native: 0
})
);
assert_eq!(
code("z"),
Some(Code {
windows: 0x5A,
native: 6
})
);
assert_eq!(
code("0"),
Some(Code {
windows: 0x30,
native: 29
})
);
assert_eq!(
code("enter"),
Some(Code {
windows: 0x0D,
native: 36
})
);
assert_eq!(
code("f1"),
Some(Code {
windows: 0x70,
native: 122
})
);
assert_eq!(
code("f12"),
Some(Code {
windows: 0x7B,
native: 111
})
);
assert_eq!(
code("["),
Some(Code {
windows: 0xDB,
native: 33
})
);
assert_eq!(code("f13"), None);
assert_eq!(code("é"), None);
}
#[cfg(target_os = "macos")]
#[test]
fn keys_without_text_carry_appkit_characters() {
assert_eq!(super::character("backspace"), Some(0x7F));
assert_eq!(super::character("up"), Some(0xF700));
assert_eq!(super::character("f1"), Some(0xF704));
assert_eq!(super::character("f12"), Some(0xF70F));
assert_eq!(super::character("a"), None);
}
}
-19
View File
@@ -1,19 +0,0 @@
//! Bundled pages: CEF draws off screen, GPUI shows.
#[cfg(target_os = "macos")]
mod edit;
mod engine;
mod fence;
mod handlers;
mod input;
mod keys;
#[cfg(target_os = "macos")]
mod mac;
mod print;
mod surface;
mod tokens;
mod view;
pub use engine::{start, subprocess};
pub use surface::file_url;
pub use view::{Event, Reply, WebView};
-105
View File
@@ -1,105 +0,0 @@
//! Chromium's app protocol on GPUI's own `NSApplication` class.
use std::{cell::Cell, ffi::CStr};
use anyhow::{Result, bail};
use objc2::{
ffi,
runtime::{AnyClass, AnyObject, Bool, Imp, Sel},
sel,
};
thread_local! {
/// NSApp lives on the main thread; one flag serves it.
static SENDING: Cell<bool> = const { Cell::new(false) };
}
extern "C-unwind" fn is_handling(_this: *mut AnyObject, _cmd: Sel) -> Bool {
Bool::new(SENDING.get())
}
extern "C-unwind" fn set_handling(_this: *mut AnyObject, _cmd: Sel, on: Bool) {
SENDING.set(on.as_bool());
}
extern "C-unwind" fn send_event(this: *mut AnyObject, _cmd: Sel, event: *mut AnyObject) {
let was = SENDING.replace(true);
let base = AnyClass::get(c"NSApplication").expect("AppKit has NSApplication");
unsafe {
let _: () = objc2::msg_send![super(&*this, base), sendEvent: event];
}
SENDING.set(was);
}
fn add(class: &AnyClass, name: Sel, imp: Imp, types: &CStr) -> Result<()> {
let added = unsafe {
ffi::class_addMethod(
class as *const AnyClass as *mut AnyClass,
name,
imp,
types.as_ptr(),
)
};
if !added.as_bool() {
bail!("{} already has -{name}", class.name().to_string_lossy());
}
Ok(())
}
/// Gives NSApp's class what CEF checks before it runs.
pub fn adopt_app_protocol() -> Result<()> {
let Some(app) = (unsafe { objc2_app_kit_nsapp() }) else {
bail!("no NSApp yet");
};
let class = unsafe { &*ffi::object_getClass(app.cast()) };
if class.responds_to(sel!(isHandlingSendEvent)) {
return Ok(());
}
unsafe {
add(
class,
sel!(isHandlingSendEvent),
std::mem::transmute(is_handling as *const ()),
c"c@:",
)?;
add(
class,
sel!(setHandlingSendEvent:),
std::mem::transmute(set_handling as *const ()),
c"v@:c",
)?;
add(
class,
sel!(sendEvent:),
std::mem::transmute(send_event as *const ()),
c"v@:@",
)?;
}
// Chromium checks the two `Cr` protocols; CEF's own is header-only.
for (name, needed) in [
(c"CrAppProtocol", true),
(c"CrAppControlProtocol", true),
(c"CefAppProtocol", false),
] {
let protocol = unsafe { ffi::objc_getProtocol(name.as_ptr()) };
match (protocol.is_null(), needed) {
(true, true) => bail!("CEF did not register {}", name.to_string_lossy()),
(true, false) => log::info!("web: {} is not registered", name.to_string_lossy()),
(false, _) => unsafe {
ffi::class_addProtocol(class as *const AnyClass as *mut AnyClass, protocol);
},
}
}
log::info!(
"web: {} takes CefAppProtocol",
class.name().to_string_lossy()
);
Ok(())
}
/// The shared application, if made.
unsafe fn objc2_app_kit_nsapp() -> Option<*mut AnyObject> {
let class = AnyClass::get(c"NSApplication")?;
let app: *mut AnyObject = unsafe { objc2::msg_send![class, sharedApplication] };
(!app.is_null()).then_some(app)
}
-39
View File
@@ -1,39 +0,0 @@
//! A page as PDF, printed by Chromium itself (MARK-11).
use std::path::{Path, PathBuf};
use cef::*;
use crate::{surface::Shared, view::WebView};
wrap_pdf_print_callback! {
struct Printed {
surface: Shared,
}
impl PdfPrintCallback {
fn on_pdf_print_finished(&self, path: Option<&CefString>, ok: ::std::os::raw::c_int) {
let path = PathBuf::from(path.map(CefString::to_string).unwrap_or_default());
log::info!("web: printed {} ok {}", path.display(), ok != 0);
self.surface.lock().expect("the surface lock").printed.push((path, ok != 0));
}
}
}
impl WebView {
/// Prints the page to `path`; `Event::Printed` tells how.
pub fn print_pdf(&self, path: &Path) {
let (path, surface) = (
CefString::from(path.to_string_lossy().as_ref()),
self.surface.clone(),
);
self.on_host(move |host| {
let settings = PdfPrintSettings {
print_background: 1,
..Default::default()
};
let mut done = Printed::new(surface);
host.print_to_pdf(Some(&path), Some(&settings), Some(&mut done));
});
}
}
-278
View File
@@ -1,278 +0,0 @@
//! What CEF's handlers leave the view; the page fence.
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use cef::{wrapper::message_router::BrowserSideCallback, *};
pub(crate) type Shared = Arc<Mutex<Surface>>;
pub(crate) struct Surface {
/// Points, as GPUI lays the view out.
pub size: (f32, f32),
pub scale: f32,
pub frame: Option<(u32, u32, Vec<u8>)>,
/// Composing characters' boxes, in points.
pub ime: Vec<(f32, f32, f32, f32)>,
pub cursor: CursorType,
pub loaded: bool,
/// The main frame's load failed since it started.
pub errored: bool,
pub queries: Vec<(String, Arc<Mutex<dyn BrowserSideCallback>>)>,
pub leaving: Vec<String>,
/// PDFs printed: where, and whether it worked.
pub printed: Vec<(PathBuf, bool)>,
/// Why the page cannot show: load, renderer or start.
pub failed: Option<String>,
/// Made on the pump's turn, after the view.
pub browser: Option<Browser>,
/// The view went before its browser came.
pub closed: bool,
/// Whether the page's scripts run.
pub scripts: bool,
/// The only folder pages load from, real path.
root: PathBuf,
/// A folder images alone may also come from.
pub images: Option<PathBuf>,
}
impl Surface {
pub fn new(root: PathBuf, scale: f32) -> Self {
Surface {
size: (1.0, 1.0),
scale,
frame: None,
ime: Vec::new(),
cursor: CursorType::POINTER,
loaded: false,
errored: false,
queries: Vec::new(),
leaving: Vec::new(),
printed: Vec::new(),
failed: None,
browser: None,
closed: false,
scripts: true,
root,
images: None,
}
}
/// A file under `root`, by its real path.
fn holds(root: &Path, url: &str) -> bool {
let Some(path) = url.strip_prefix("file://") else {
return false;
};
let path = path.split(['?', '#']).next().unwrap_or_default();
let Ok(path) = String::from_utf8(decoded(path)) else {
return false;
};
#[cfg(windows)]
let path = windows_path(&path);
std::fs::canonicalize(path).is_ok_and(|real| real.starts_with(root))
}
/// Where the page may go: its own files, or blank.
pub fn navigable(&self, url: &str) -> bool {
url == "about:blank" || Self::holds(&self.root, url)
}
/// What a page loads: its files, data, allowed images.
pub fn loadable(&self, url: &str, image: bool) -> bool {
let pictured = image
&& self
.images
.as_deref()
.is_some_and(|dir| Self::holds(dir, url));
self.navigable(url)
|| url.starts_with("data:")
|| url.starts_with("blob:file://")
|| pictured
}
}
impl Drop for Surface {
fn drop(&mut self) {
log::info!("web: a page's memory is freed");
}
}
/// `%XX` decoded; a file URL's path.
fn decoded(text: &str) -> Vec<u8> {
let bytes = text.as_bytes();
let (mut out, mut at) = (Vec::with_capacity(bytes.len()), 0);
while at < bytes.len() {
let hex = bytes
.get(at + 1..at + 3)
.and_then(|pair| u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok());
match (bytes[at], hex) {
(b'%', Some(byte)) => {
out.push(byte);
at += 3;
}
(byte, _) => {
out.push(byte);
at += 1;
}
}
}
out
}
/// Bytes as URL text, reserved ones escaped.
fn escaped(bytes: &[u8]) -> String {
let mut out = String::new();
for &byte in bytes {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => {
out.push(byte as char)
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
/// A path as a file URL, every reserved byte escaped.
pub fn file_url(path: &Path) -> String {
#[cfg(windows)]
return windows_url(&path.to_string_lossy());
#[cfg(not(windows))]
format!("file://{}", escaped(path.as_os_str().as_encoded_bytes()))
}
/// A drive or UNC path as `file:///C:/…` or `file://host/…`.
#[cfg(any(windows, test))]
fn windows_url(path: &str) -> String {
let path = match path.strip_prefix(r"\\?\UNC\") {
Some(rest) => format!(r"\\{rest}"),
None => path.strip_prefix(r"\\?\").unwrap_or(path).to_owned(),
};
let slashed = |text: &str| escaped(text.replace('\\', "/").as_bytes());
match path.strip_prefix(r"\\") {
Some(unc) => {
let (host, rest) = unc.split_once('\\').unwrap_or((unc, ""));
format!("file://{}/{}", slashed(host), slashed(rest))
}
None if path.get(1..2) == Some(":") => {
format!("file:///{}{}", &path[..2], slashed(&path[2..]))
}
None => format!("file:///{}", slashed(&path)),
}
}
/// A decoded file URL's rest as a Windows path.
#[cfg(any(windows, test))]
fn windows_path(rest: &str) -> String {
match rest.strip_prefix('/') {
Some(drive) => drive.replace('/', "\\"),
None => format!(r"\\{}", rest.replace('/', "\\")),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
fn surface(root: &Path) -> super::Surface {
super::Surface::new(std::fs::canonicalize(root).expect("real"), 1.0)
}
#[test]
fn only_real_files_under_the_root_load() {
let dir = tempfile::tempdir().expect("a temp dir");
let root = dir.path().join("content");
let outside = dir.path().join("outside");
std::fs::create_dir_all(&root).expect("makes");
std::fs::create_dir_all(&outside).expect("makes");
std::fs::write(root.join("index.html"), "x").expect("writes");
std::fs::write(root.join("a #b?.html"), "x").expect("writes");
std::fs::write(outside.join("o.html"), "x").expect("writes");
let surface = surface(&root);
let url = |path: &Path| super::file_url(&std::fs::canonicalize(path).expect("real"));
assert!(surface.navigable(&url(&root.join("index.html"))));
assert!(
surface.navigable(&url(&root.join("a #b?.html"))),
"reserved bytes"
);
assert!(surface.navigable("about:blank"));
assert!(!surface.navigable(&url(&outside.join("o.html"))));
let up = format!("{}/../outside/o.html", super::file_url(&surface.root));
assert!(!surface.navigable(&up));
assert!(!surface.navigable(&format!("{}/missing.html", super::file_url(&surface.root))));
assert!(!surface.navigable("https://example.com/"));
assert!(
!surface.navigable("data:text/html,x")
&& surface.loadable("data:image/png;base64,x", true)
);
assert!(!surface.loadable("http://127.0.0.1:9/pixel", true));
}
#[test]
fn an_image_folder_admits_images_alone() {
let dir = tempfile::tempdir().expect("a temp dir");
let (root, docs) = (dir.path().join("content"), dir.path().join("docs"));
std::fs::create_dir_all(&root).expect("makes");
std::fs::create_dir_all(&docs).expect("makes");
std::fs::write(docs.join("a.png"), "x").expect("writes");
let mut surface = surface(&root);
let picture = super::file_url(&std::fs::canonicalize(docs.join("a.png")).expect("real"));
assert!(
!surface.loadable(&picture, true),
"not before it is allowed"
);
surface.images = Some(std::fs::canonicalize(&docs).expect("real"));
assert!(surface.loadable(&picture, true));
assert!(!surface.loadable(&picture, false), "a script from there");
assert!(!surface.navigable(&picture));
}
#[cfg(unix)]
#[test]
fn a_link_out_of_the_root_is_outside() {
let dir = tempfile::tempdir().expect("a temp dir");
let root = dir.path().join("content");
let outside = dir.path().join("outside");
std::fs::create_dir_all(&root).expect("makes");
std::fs::create_dir_all(&outside).expect("makes");
std::fs::write(outside.join("o.html"), "x").expect("writes");
std::os::unix::fs::symlink(&outside, root.join("link")).expect("links");
let surface = surface(&root);
let through = format!("{}/link/o.html", super::file_url(&surface.root));
assert!(!surface.navigable(&through));
}
#[test]
fn windows_paths_make_valid_file_urls_and_back() {
let cases = [
(r"C:\Users\A b\doc.md", "file:///C:/Users/A%20b/doc.md"),
(r"\\?\C:\notes\doc.md", "file:///C:/notes/doc.md"),
(r"\\server\share\doc.md", "file://server/share/doc.md"),
(r"\\?\UNC\server\share\doc.md", "file://server/share/doc.md"),
];
for (path, url) in cases {
assert_eq!(super::windows_url(path), url, "{path}");
}
assert_eq!(
super::windows_path("/C:/Users/A b/doc.md"),
r"C:\Users\A b\doc.md"
);
assert_eq!(
super::windows_path("server/share/doc.md"),
r"\\server\share\doc.md"
);
}
#[test]
fn percent_escapes_decode_and_bad_ones_stay() {
assert_eq!(super::decoded("a%20b%e4%b8%ad"), "a b中".as_bytes());
assert_eq!(super::decoded("100%"), b"100%");
assert_eq!(super::decoded("%zz"), b"%zz");
assert_eq!(
super::file_url(Path::new("/a b/中#?.html")),
"file:///a%20b/%E4%B8%AD%23%3F.html"
);
}
}
-77
View File
@@ -1,77 +0,0 @@
//! The app's theme as CSS variables on the page's root.
use ely_gpui_component::theme::{ActiveTheme as _, Palette};
use gpui::{App, Hsla, Rgba};
fn css(color: Hsla) -> String {
let rgba = Rgba::from(color);
let byte = |part: f32| (part.clamp(0.0, 1.0) * 255.0).round() as u8;
format!(
"rgba({}, {}, {}, {:.3})",
byte(rgba.r),
byte(rgba.g),
byte(rgba.b),
rgba.a
)
}
/// `--ely-*` names and values, palette then type.
pub fn tokens(cx: &App) -> Vec<(String, String)> {
let theme = cx.theme();
let colors: &Palette = &theme.colors;
let mut out: Vec<(String, String)> = [
("bg", colors.bg),
("surface", colors.surface),
("sunken", colors.sunken),
("hover", colors.hover),
("border", colors.border),
("fg", colors.fg),
("fg-muted", colors.fg_muted),
("fg-subtle", colors.fg_subtle),
("accent", colors.accent),
("on-accent", colors.on_accent),
("focus", colors.focus),
("link", colors.link),
("selection", colors.selection),
("danger", colors.danger),
("warning", colors.warning),
("success", colors.success),
("keyword", colors.syntax.keyword),
("string", colors.syntax.string),
("number", colors.syntax.number),
("comment", colors.syntax.comment),
("function", colors.syntax.function),
("type", colors.syntax.type_name),
("tag", colors.syntax.tag),
("attribute", colors.syntax.attribute),
("punctuation", colors.syntax.punctuation),
]
.into_iter()
.map(|(name, color)| (format!("--ely-{name}"), css(color)))
.collect();
out.push(("--ely-font".into(), format!("\"{}\"", theme.font_family)));
out.push(("--ely-mono".into(), format!("\"{}\"", theme.mono_family)));
out.push(("--ely-font-scale".into(), theme.font_scale.to_string()));
let scheme = if theme.is_dark() { "dark" } else { "light" };
out.push(("color-scheme".into(), scheme.into()));
out
}
/// Script that sets `tokens` on `:root`.
pub fn script(tokens: &[(String, String)]) -> String {
let pairs = serde_json::to_string(tokens).expect("strings serialize");
format!("for (const [k, v] of {pairs}) document.documentElement.style.setProperty(k, v);")
}
#[cfg(test)]
mod tests {
#[test]
fn a_script_sets_each_token_as_json() {
let tokens = vec![("--ely-fg".to_string(), "rgba(1, 2, 3, 1.000)".to_string())];
let script = super::script(&tokens);
assert!(
script.contains(r#"[["--ely-fg","rgba(1, 2, 3, 1.000)"]]"#),
"{script}"
);
}
}
-381
View File
@@ -1,381 +0,0 @@
//! A CEF page drawn by GPUI: frames in, events out.
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use anyhow::{Result, bail};
use cef::{wrapper::message_router::BrowserSideCallback, *};
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
feedback::ErrorView,
primitives::IconName,
};
use gpui::{
AnyElement, App, AppContext as _, Bounds, Context, CursorStyle, ElementInputHandler, Entity,
EventEmitter, FocusHandle, Focusable, Hitbox, HitboxBehavior, InteractiveElement as _,
IntoElement, ParentElement as _, Pixels, Render, RenderImage, Styled as _, Window, canvas, div,
};
use image::{Frame, ImageBuffer, Rgba};
use crate::{
engine::Engine,
surface::{Shared, Surface, file_url},
};
/// A page's answer to one `elyQuery`.
#[derive(Clone)]
pub struct Reply(Arc<Mutex<dyn BrowserSideCallback>>);
impl Reply {
pub fn ok(&self, text: &str) {
self.0.lock().expect("the reply lock").success_str(text);
}
pub fn fail(&self, message: &str) {
self.0.lock().expect("the reply lock").failure(1, message);
}
}
/// What the page tells its owner.
pub enum Event {
Loaded,
Query(String, Reply),
/// A link to leave by; the owner decides.
Leave(String),
/// The page cannot show: load, start or run failed.
Failed(String),
/// A PDF print ended: where, and whether it worked.
Printed(PathBuf, bool),
}
pub struct WebView {
pub(crate) surface: Shared,
image: Option<Arc<RenderImage>>,
/// Replaced frames, freed from GPUI's atlas next paint.
stale: Vec<Arc<RenderImage>>,
pub(crate) focus: FocusHandle,
pub(crate) bounds: Bounds<Pixels>,
pub(crate) composing: Option<std::ops::Range<usize>>,
/// The key whose text the input handler brings next.
pub(crate) typing: Option<cef::KeyEvent>,
url: String,
/// Shown instead of the page until reloaded.
fault: Option<String>,
/// Takes a paste before the page; true when it did.
pub(crate) paste: Option<Paste>,
}
/// Takes a paste before the page; true when it did.
pub(crate) type Paste = std::rc::Rc<dyn Fn(&mut Window, &mut App) -> bool>;
impl EventEmitter<Event> for WebView {}
impl Focusable for WebView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus.clone()
}
}
impl WebView {
/// Its frames, for a closing owner to free.
pub fn retire_all(&mut self) -> ely_gpui_component::primitives::Retired {
let mut images = ely_gpui_component::primitives::Retired::default();
for image in self.image.take().into_iter().chain(self.stale.drain(..)) {
images.push(image);
}
images
}
/// Opens `page`, a file in `root`, in a new browser.
pub fn open(page: &Path, root: &Path, window: &mut Window, cx: &mut App) -> Result<Entity<Self>> {
Self::opened(page, root, true, window, cx)
}
/// Opens `page` with its scripts off: shown, never run.
pub fn open_inert(page: &Path, root: &Path, window: &mut Window, cx: &mut App) -> Result<Entity<Self>> {
Self::opened(page, root, false, window, cx)
}
fn opened(
page: &Path,
root: &Path,
scripts: bool,
window: &mut Window,
cx: &mut App,
) -> Result<Entity<Self>> {
if !cx.has_global::<Engine>() {
bail!("the web engine has not started");
}
let root = std::fs::canonicalize(root)?;
let page = std::fs::canonicalize(page)?;
let mut surface = Surface::new(root, window.scale_factor());
surface.scripts = scripts;
let surface = Arc::new(Mutex::new(surface));
let url = file_url(&page);
if !surface.lock().expect("the surface lock").navigable(&url) {
bail!("{} is outside its content folder", page.display());
}
log::info!("web: a browser asked for {url}");
crate::engine::ask_browser(surface.clone(), url.clone());
let view = cx.new(|cx| WebView {
surface,
image: None,
stale: Vec::new(),
focus: cx.focus_handle(),
bounds: Bounds::default(),
composing: None,
typing: None,
url,
fault: None,
paste: None,
});
crate::input::follow_focus(&view, window, cx);
view.update(cx, |_, cx| {
cx.observe_global::<ely_gpui_component::theme::Theme>(|view, cx| view.theme(cx))
.detach();
});
cx.global_mut::<Engine>().views.push(view.downgrade());
Ok(view)
}
/// The browser, on the pump's next turn, outside any update.
pub(crate) fn on_browser(&self, work: impl FnOnce(&Browser) + 'static) {
let surface = self.surface.clone();
crate::engine::later(move || {
// Not held while CEF runs: its callbacks lock it too.
let browser = surface.lock().expect("the surface lock").browser.clone();
if let Some(browser) = browser {
work(&browser);
}
});
}
pub(crate) fn on_host(&self, work: impl FnOnce(&BrowserHost) + 'static) {
self.on_browser(move |browser| {
if let Some(host) = browser.host() {
work(&host);
}
});
}
/// Lets images under `dir` load; one folder at a time.
pub fn allow_images(&self, dir: &Path) -> Result<()> {
let dir = std::fs::canonicalize(dir)?;
log::info!("web: images may come from {}", dir.display());
self.surface.lock().expect("the surface lock").images = Some(dir);
Ok(())
}
/// `take` sees pastes first; true keeps them from the page.
pub fn intercept_paste(&mut self, take: impl Fn(&mut Window, &mut App) -> bool + 'static) {
self.paste = Some(std::rc::Rc::new(take));
}
/// Runs script in the page's main frame.
pub fn run_js(&self, code: &str) {
let code = CefString::from(code);
self.on_browser(move |browser| {
if let Some(frame) = browser.main_frame() {
frame.execute_java_script(Some(&code), None, 0);
}
});
}
/// The app's colors and fonts, into the page.
fn theme(&mut self, cx: &mut Context<Self>) {
self.run_js(&crate::tokens::script(&crate::tokens::tokens(cx)));
}
/// Zoom as a factor; CEF counts in 1.2 steps.
pub fn set_zoom(&self, factor: f64) {
let level = factor.ln() / 1.2f64.ln();
self.on_host(move |host| host.set_zoom_level(level));
}
/// Takes what the handlers left; says if anything came.
fn take(&mut self, cx: &mut Context<Self>) {
let (frame, events) = {
let mut surface = self.surface.lock().expect("the surface lock");
let mut events = Vec::new();
if std::mem::take(&mut surface.loaded) {
events.push(Event::Loaded);
}
for (request, callback) in surface.queries.drain(..) {
events.push(Event::Query(request, Reply(callback)));
}
events.extend(surface.leaving.drain(..).map(Event::Leave));
events.extend(
surface
.printed
.drain(..)
.map(|(path, ok)| Event::Printed(path, ok)),
);
events.extend(surface.failed.take().map(Event::Failed));
if let Some(why) = crate::engine::failed().filter(|_| !surface.closed) {
surface.closed = true;
events.push(Event::Failed(why));
}
(surface.frame.take(), events)
};
if let Some((width, height, bgra)) = frame {
match ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, bgra) {
Some(picture) => {
let image = Arc::new(RenderImage::new([Frame::new(picture)]));
self.stale.extend(self.image.replace(image));
cx.notify();
}
None => log::error!("web: a {width}×{height} frame was short"),
}
}
for event in events {
match &event {
// Each load starts unthemed; the tokens go in first.
Event::Loaded => {
self.fault = None;
self.theme(cx);
}
Event::Failed(why) => {
log::error!("web: {} failed: {why}", self.url);
self.fault = Some(why.clone());
}
_ => {}
}
cx.notify();
cx.emit(event);
}
}
/// Follows the room GPUI gave; CEF resizes to it.
fn place(&mut self, bounds: Bounds<Pixels>, scale: f32) {
self.bounds = bounds;
let size = (f32::from(bounds.size.width), f32::from(bounds.size.height));
let (resized, rescaled) = {
let mut surface = self.surface.lock().expect("the surface lock");
let changed = (surface.size != size, surface.scale != scale);
(surface.size, surface.scale) = (size, scale);
changed
};
if resized || rescaled {
self.on_host(move |host| {
if rescaled {
host.notify_screen_info_changed();
}
host.was_resized();
});
}
}
/// Tries the page again; a lost browser is made anew.
pub fn reload(&mut self, cx: &mut Context<Self>) {
self.fault = None;
let made = self
.surface
.lock()
.expect("the surface lock")
.browser
.is_some();
match made {
true => self.on_browser(|browser| browser.reload()),
false => crate::engine::ask_browser(self.surface.clone(), self.url.clone()),
}
cx.notify();
}
/// The fault, with a way to try again.
fn fault_view(&self, why: &str, cx: &mut Context<Self>) -> AnyElement {
let this = cx.weak_entity();
let title = ely_ui::i18n::text(cx, "web.failed", &[]);
let again = Button::new("web-reload", ely_ui::i18n::text(cx, "web.reload", &[]))
.variant(ButtonVariant::Primary)
.on_click(move |_, window, cx| {
// The page, not the gone button, takes keys.
this.update(cx, |view, cx| {
view.reload(cx);
window.focus(&view.focus, cx);
})
.ok();
});
let view =
ErrorView::new("web-failed", IconName::TriangleAlert, title).detail(why.to_string());
// Without a running engine nothing can try again.
match crate::engine::failed() {
Some(_) => view.into_any_element(),
None => view.action(again).into_any_element(),
}
}
fn cursor(&self) -> CursorStyle {
match self.surface.lock().expect("the surface lock").cursor {
CursorType::HAND => CursorStyle::PointingHand,
CursorType::IBEAM => CursorStyle::IBeam,
CursorType::CROSS => CursorStyle::Crosshair,
CursorType::EASTWESTRESIZE | CursorType::COLUMNRESIZE => CursorStyle::ResizeLeftRight,
CursorType::NORTHSOUTHRESIZE | CursorType::ROWRESIZE => CursorStyle::ResizeUpDown,
CursorType::NOTALLOWED => CursorStyle::OperationNotAllowed,
_ => CursorStyle::Arrow,
}
}
}
impl Drop for WebView {
fn drop(&mut self) {
self.surface.lock().expect("the surface lock").closed = true;
self.on_host(|host| host.close_browser(true as _));
}
}
/// After each pump: every live view takes its news.
pub(crate) fn deliver(cx: &mut App) {
let views = std::mem::take(&mut cx.global_mut::<Engine>().views);
let alive: Vec<_> = views
.into_iter()
.filter(|view| view.update(cx, |view, cx| view.take(cx)).is_ok())
.collect();
cx.global_mut::<Engine>().views.extend(alive);
}
impl Render for WebView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(why) = self.fault.clone() {
return div()
.size_full()
.child(self.fault_view(&why, cx))
.into_any_element();
}
let this = cx.entity();
let paint = this.clone();
let element = canvas(
move |bounds, window, cx| {
let scale = window.scale_factor();
this.update(cx, |view, _| view.place(bounds, scale));
window.insert_hitbox(bounds, HitboxBehavior::Normal)
},
move |bounds, hitbox: Hitbox, window, cx| {
let (image, stale, cursor, focus) = paint.update(cx, |view, _| {
let stale = std::mem::take(&mut view.stale);
(view.image.clone(), stale, view.cursor(), view.focus.clone())
});
for old in stale {
if let Err(error) = window.drop_image(old) {
log::error!("web: an old frame stayed: {error}");
}
}
window.set_cursor_style(cursor, &hitbox);
window.handle_input(&focus, ElementInputHandler::new(bounds, paint.clone()), cx);
let Some(image) = image else { return };
let painted =
window.paint_image(bounds, bounds, Default::default(), image, 0, false);
if let Err(error) = painted {
log::error!("web: a frame was not painted: {error}");
}
},
)
.size_full();
crate::input::listen(div().size_full(), cx)
.track_focus(&self.focus)
.child(element)
.into_any_element()
}
}