feat(test): port tests/shared library from CEF

feat: unify bundle utilities under cef build-util feature

feat: reenable sandbox support in cefsimple

feat: add MainMenu.xib on mac

fix: bypass tryToTerminateApplication for Command+Q

fix: enable much smaller linux release builds
This commit is contained in:
Bill Avery
2025-10-19 15:16:36 -07:00
parent 67d9ff7cfe
commit e66a73c60e
83 changed files with 9135 additions and 445 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ cargo-sources.json
Cargo.lock
/.vscode/
**/.DS_Store

View File

@@ -23,18 +23,20 @@ authors = [
repository = "https://github.com/tauri-apps/cef-rs"
[workspace.dependencies]
cef = { path = "cef" }
cef = { path = "cef", default-features = false }
cef-dll-sys = { version = "143.4.0", path = "sys" }
download-cef = { version = "2.2", path = "download-cef" }
anyhow = "1"
ash = "0.38"
bindgen = "0.72"
cargo_metadata = "0.23.1"
clap = { version = "4", features = ["derive"] }
cmake = "0.1.52"
convert_case = "0.10"
git-cliff = "2"
git-cliff-core = "2"
glib = "0.21"
io-surface = "0.16"
libc = "0.2"
libloading = "0.9"
@@ -42,7 +44,8 @@ metal = "0.32"
objc = "0.2"
objc2 = "0.6.3"
objc2-app-kit = { version = "0.3.2", default-features = false }
objc2-io-surface = "0.3"
objc2-foundation = "0.3.2"
objc2-io-surface = "0.3.2"
plist = "1"
proc-macro2 = "1"
quote = "1"
@@ -55,6 +58,8 @@ thiserror = "2"
toml_edit = "0.24"
tracing = "0.1"
wgpu = "27"
winres = "0.1"
x11-dl = "2"
[workspace.dependencies.windows-sys]
version = "0.61"

View File

@@ -52,6 +52,11 @@ $env:PATH="$env:PATH;$env:CEF_PATH"
### Run the `cefsimple` Example
This command should work with each platform:
```sh
cargo run --bin bundle-cef-app -- cefsimple -o target/bundle
```
#### Linux
```sh
@@ -61,15 +66,15 @@ cargo run --bin cefsimple
#### macOS
```sh
cargo run --bin bundle_cefsimple
open target/debug/cefsimple.app
cargo run --bin bundle-cef-app -- cefsimple -o target/bundle
open target/bundle/cefsimple.app
```
#### Windows (using PowerShell)
```pwsh
cp ./examples/cefsimple/src/win/cefsimple.exe.manifest ./target/debug/
cargo run --bin cefsimple
cargo run --bin bundle-cef-app -- cefsimple -o ./target/bundle
./target/bundle/cefsimple.exe
```
## Contributing

View File

@@ -8,23 +8,44 @@ license.workspace = true
authors.workspace = true
repository.workspace = true
[lib]
[[bin]]
name = "bundle-cef-app"
[features]
default = ["sandbox"]
default = ["sandbox", "build-util"]
dox = ["cef-dll-sys/dox"]
sandbox = ["cef-dll-sys/sandbox"]
# Unified texture import system for CEF hardware acceleration
accelerated_osr = [
"ash",
"libc",
"objc",
"objc2-io-surface",
"metal",
"thiserror",
"tracing",
"wgpu",
"windows",
"libc",
"ash",
"tracing",
"thiserror",
"objc2-io-surface",
"objc",
"metal"
]
# Build utilities, including the bundle-cef-app tool.
build-util = [
"anyhow",
"cargo_metadata",
"clap",
"plist",
"semver",
"serde",
"serde_json",
"thiserror",
]
# Linux X11 support in bundle-cef-app.
linux-x11 = []
[package.metadata.docs.rs]
features = ["dox"]
@@ -37,6 +58,13 @@ thiserror = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true }
# bundle
anyhow = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
cargo_metadata = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys.workspace = true
windows = { workspace = true, optional = true }
@@ -47,6 +75,8 @@ objc2.workspace = true
objc2-io-surface = { workspace = true, optional = true }
objc = { workspace = true, optional = true }
metal = { workspace = true, optional = true }
plist = { workspace = true, optional = true }
semver = { workspace = true, optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
libc = { workspace = true, optional = true }

View File

@@ -56,9 +56,7 @@ impl Args {
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn as_cmd_line(&self) -> Option<CommandLine> {
let Some(cmd_line) = command_line_create() else {
return None;
};
let cmd_line = command_line_create()?;
cmd_line.init_from_argv(self.as_main_args().argc, self.as_main_args().argv.cast());
Some(cmd_line)
}
@@ -81,3 +79,12 @@ impl Args {
cmd_line
}
}
impl From<MainArgs> for Args {
fn from(main_args: MainArgs) -> Self {
Args {
main_args,
..Default::default()
}
}
}

View File

@@ -0,0 +1,26 @@
use cef::build_util::linux::*;
use clap::Parser;
use std::{env, path::PathBuf};
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
name: String,
#[arg(long, default_value_t = false)]
release: bool,
#[arg(short, long)]
output: Option<String>,
}
pub fn main() -> anyhow::Result<()> {
let args = Args::parse();
let output = match args.output {
Some(output) => PathBuf::from(output),
None => env::current_dir()?,
};
let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?;
let bundle_path = bundle_path.display();
println!("Run the app from {bundle_path}");
Ok(())
}

View File

@@ -0,0 +1,46 @@
use cef::build_util::mac::*;
use clap::Parser;
use semver::Version;
use std::{env, path::PathBuf};
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
name: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
identifier: Option<String>,
#[arg(short, long)]
display_name: Option<String>,
#[arg(short, long, default_value = "English")]
region: String,
#[arg(short, long, default_value = "1.0.0")]
version: String,
}
pub fn main() -> anyhow::Result<()> {
let args = Args::parse();
let output = match args.output {
Some(output) => PathBuf::from(output),
None => env::current_dir()?,
};
let identifier = args
.identifier
.unwrap_or_else(|| format!("apps.tauri.cef-rs.{}", args.name));
let display_name = args.display_name.unwrap_or_else(|| args.name.clone());
let version = Version::parse(&args.version)?;
let bundle_info = BundleInfo {
name: args.name.clone(),
identifier,
display_name,
development_region: args.region,
version,
};
let bundle_path = build_bundle(output.as_path(), &args.name, bundle_info)?;
let bundle_path = bundle_path.display();
println!("Run the app from {bundle_path}");
Ok(())
}

View File

@@ -0,0 +1,29 @@
#[cfg(not(feature = "build-util"))]
fn main() {
let command = std::env::current_exe().expect("Failed to get current executable path");
eprintln!("Disabled: {command:?} was compiled without the build-util feature.");
}
#[cfg(all(feature = "build-util", target_os = "macos"))]
mod mac;
#[cfg(all(feature = "build-util", target_os = "macos"))]
fn main() -> anyhow::Result<()> {
mac::main()
}
#[cfg(all(feature = "build-util", target_os = "linux"))]
mod linux;
#[cfg(all(feature = "build-util", target_os = "linux"))]
fn main() -> anyhow::Result<()> {
linux::main()
}
#[cfg(all(feature = "build-util", target_os = "windows"))]
mod win;
#[cfg(all(feature = "build-util", target_os = "windows"))]
fn main() -> anyhow::Result<()> {
win::main()
}

View File

@@ -0,0 +1,26 @@
use cef::build_util::win::*;
use clap::Parser;
use std::{env, path::PathBuf};
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
name: String,
#[arg(long, default_value_t = false)]
release: bool,
#[arg(short, long)]
output: Option<String>,
}
pub fn main() -> anyhow::Result<()> {
let args = Args::parse();
let output = match args.output {
Some(output) => PathBuf::from(output),
None => env::current_dir()?,
};
let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?;
let bundle_path = bundle_path.display();
println!("Run the app from {bundle_path}");
Ok(())
}

View File

@@ -0,0 +1,77 @@
use std::{
fs, io,
path::{Path, PathBuf},
process::Command,
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("I/O error: {0:?}")]
Io(#[from] io::Error),
#[error("Metadata error: {0:?}")]
Metadata(#[from] super::metadata::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux
pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result<PathBuf> {
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
copy_directory(&cef_path, &app_path)?;
const LOCALES_DIR: &str = "locales";
copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?;
copy_app(app_path, target_path, executable_name)
}
/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target.
pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result<PathBuf> {
let cargo_metadata = super::metadata::get_cargo_metadata()?;
let target_path =
cargo_metadata
.target_directory()
.join(if release { "release" } else { "debug" });
cargo_build(executable_name, release)?;
bundle(app_path, &target_path, executable_name)
}
fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result<PathBuf> {
let executable_path = app_path.join(executable_name);
let target_executable = target_path.join(executable_name);
fs::copy(&target_executable, &executable_path)?;
Ok(executable_path)
}
fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_file() {
fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}
fn cargo_build(name: &str, release: bool) -> Result<()> {
println!("Building {name}...");
let mut args = vec!["build"];
if release {
args.push("--release");
}
#[cfg(feature = "linux-x11")]
args.extend(["-F", "linux-x11"]);
args.extend(["--bin", name]);
let status = Command::new(super::cargo_path()).args(args).status()?;
if status.success() {
Ok(())
} else {
Err(io::Error::from(io::ErrorKind::Interrupted).into())
}
}

342
cef/src/build_util/mac.rs Normal file
View File

@@ -0,0 +1,342 @@
use semver::Version;
use serde::Serialize;
use std::{
collections::HashMap,
fs, io,
path::{Path, PathBuf},
process::Command,
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("I/O error: {0:?}")]
Io(#[from] io::Error),
#[error("Metadata error: {0:?}")]
Metadata(#[from] super::metadata::Error),
#[error("Plist error: {0:?}")]
Plist(#[from] plist::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// Common bundle information that is shared in the [Info.plist](https://developer.apple.com/documentation/bundleresources/information-property-list) files.
#[derive(Clone, Serialize)]
pub struct BundleInfo {
/// [CFBundleName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundlename)
#[serde(rename = "CFBundleName")]
pub name: String,
/// [CFBundleIdentifier](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier)
#[serde(rename = "CFBundleIdentifier")]
pub identifier: String,
/// [CFBundleDisplayName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledisplayname)
#[serde(rename = "CFBundleDisplayName")]
pub display_name: String,
/// [CFBundleDevelopmentRegion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledevelopmentregion)
#[serde(rename = "CFBundleDevelopmentRegion")]
pub development_region: String,
/// [CFBundleVersion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion)
#[serde(rename = "CFBundleVersion", serialize_with = "serialize_version")]
pub version: Version,
}
impl BundleInfo {
pub fn new(
name: &str,
identifier: &str,
display_name: &str,
development_region: &str,
version: Version,
) -> Self {
Self {
name: name.to_owned(),
identifier: identifier.to_owned(),
display_name: display_name.to_owned(),
development_region: development_region.to_owned(),
version,
}
}
}
/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos
pub fn bundle(
app_path: &Path,
target_path: &Path,
executable_name: &str,
helper_name: &str,
resources_path: Option<PathBuf>,
bundle_info: BundleInfo,
) -> Result<PathBuf> {
let main_app_path = create_app(
app_path,
executable_name,
false,
resources_path.as_deref(),
bundle_info.clone(),
&target_path.join(executable_name),
)?;
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK);
if to.exists() {
fs::remove_dir_all(&to).unwrap();
}
copy_directory(&cef_path.join(FRAMEWORK), &to)?;
for helper in HELPERS {
let helper = format!("{executable_name} {helper}");
create_app(
&main_app_path.join(FRAMEWORKS_PATH),
&helper,
true,
None,
bundle_info.clone(),
&target_path.join(helper_name),
)?;
}
if let Some(resources_path) = resources_path {
let resources_path = resources_path.join("mac");
let target_path = main_app_path.join(RESOURCES_PATH);
copy_app_resources(&resources_path, &target_path)?;
}
Ok(main_app_path)
}
/// Similar to [`bundle`], but this will invoke `cargo build` to build both the main executable and
/// helper executable targets.
pub fn build_bundle(
app_path: &Path,
executable_name: &str,
bundle_info: BundleInfo,
) -> Result<PathBuf> {
let cargo_metadata = super::metadata::get_cargo_metadata()?;
let target_path = cargo_metadata.target_directory().join("debug");
let bundle_metadata = cargo_metadata.parse_bundle_metadata(executable_name)?;
cargo_build(executable_name)?;
cargo_build(&bundle_metadata.helper_name)?;
bundle(
app_path,
&target_path,
executable_name,
&bundle_metadata.helper_name,
bundle_metadata.resources_path,
bundle_info,
)
}
#[derive(Serialize)]
struct InfoPlist {
#[serde(flatten)]
bundle_info: BundleInfo,
#[serde(rename = "CFBundleExecutable")]
executable_name: String,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
bundle_info_dictionary_version: String,
#[serde(rename = "CFBundlePackageType")]
bundle_package_type: String,
#[serde(rename = "CFBundleIconFile", skip_serializing_if = "String::is_empty")]
icon_file: String,
#[serde(rename = "CFBundleSignature")]
bundle_signature: String,
#[serde(
rename = "CFBundleShortVersionString",
serialize_with = "serialize_version"
)]
short_version: Version,
#[serde(rename = "LSEnvironment")]
environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
file_quarantine_enabled: bool,
#[serde(rename = "LSMinimumSystemVersion")]
minimum_system_version: String,
#[serde(
rename = "LSUIElement",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_ui_element"
)]
ui_element: Option<&'static str>,
#[serde(rename = "NSBluetoothAlwaysUsageDescription")]
bluetooth_always_usage_description: String,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
supports_automatic_graphics_switching: bool,
#[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")]
web_browser_publickey_credential_usage_description: String,
#[serde(rename = "NSCameraUsageDescription")]
camera_usage_description: String,
#[serde(rename = "NSMicrophoneUsageDescription")]
microphone_usage_description: String,
}
impl InfoPlist {
fn new(
executable_name: &str,
is_helper: bool,
icon_file: Option<String>,
bundle_info: BundleInfo,
) -> Self {
Self {
executable_name: executable_name.to_owned(),
bundle_info_dictionary_version: "6.0".to_owned(),
bundle_package_type: "APPL".to_owned(),
icon_file: icon_file.unwrap_or_default(),
bundle_signature: "????".to_owned(),
short_version: Version::new(
bundle_info.version.major,
bundle_info.version.minor,
bundle_info.version.patch,
),
environment: [("MallocNanoZone".to_owned(), "0".to_owned())]
.into_iter()
.collect(),
file_quarantine_enabled: true,
minimum_system_version: "11.0".to_owned(),
ui_element: if is_helper { Some("1") } else { None },
bluetooth_always_usage_description: executable_name.to_owned(),
supports_automatic_graphics_switching: true,
web_browser_publickey_credential_usage_description: executable_name.to_owned(),
camera_usage_description: executable_name.to_owned(),
microphone_usage_description: executable_name.to_owned(),
bundle_info,
}
}
}
fn serialize_ui_element<S>(
ui_element: &Option<&'static str>,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match ui_element {
Some(element) => serializer.serialize_str(element),
None => unreachable!("None is skipped"),
}
}
fn serialize_version<S>(version: &Version, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&version.to_string())
}
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const HELPERS: &[&str] = &[
"Helper (GPU)",
"Helper (Renderer)",
"Helper (Plugin)",
"Helper (Alerts)",
"Helper",
];
fn create_app_layout(app_path: &Path) -> Result<PathBuf> {
for path in [EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] {
fs::create_dir_all(app_path.join(path))?;
}
Ok(app_path.join("Contents"))
}
fn create_app(
app_path: &Path,
executable_name: &str,
is_helper: bool,
resources_path: Option<&Path>,
bundle_info: BundleInfo,
bin: &Path,
) -> Result<PathBuf> {
let app_path = app_path.join(executable_name).with_extension("app");
let contents_path = create_app_layout(&app_path)?;
let icon_file = resources_path.and_then(|path| {
let icon_file = format!("{executable_name}.icns");
if path.join("mac").join(&icon_file).exists() {
Some(icon_file)
} else {
None
}
});
create_info_plist(
&contents_path,
executable_name,
bundle_info,
is_helper,
icon_file,
)?;
let executable_path = app_path.join(EXEC_PATH).join(executable_name);
fs::copy(bin, executable_path)?;
Ok(app_path)
}
fn create_info_plist(
contents_path: &Path,
executable_name: &str,
bundle_info: BundleInfo,
is_helper: bool,
icon_file: Option<String>,
) -> Result<()> {
let info_plist = InfoPlist::new(executable_name, is_helper, icon_file, bundle_info);
plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?;
Ok(())
}
fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_directory(&entry.path(), &dst_path)?;
} else {
fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}
fn copy_app_resources(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let mut dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_app_resources(&entry.path(), &dst_path)?;
} else {
let entry = entry.path();
if entry
.extension()
.map(|ext| ext == "xib")
.unwrap_or_default()
{
dst_path.set_extension("nib");
let (Some(dst_path), Some(entry)) = (dst_path.to_str(), entry.to_str()) else {
return Err(io::Error::from(io::ErrorKind::NotFound));
};
let status = Command::new("xcrun")
.args(["ibtool", "--compile", dst_path, entry])
.status()?;
if !status.success() {
return Err(io::Error::from(io::ErrorKind::Interrupted));
}
} else {
fs::copy(entry, &dst_path)?;
}
}
}
Ok(())
}
fn cargo_build(name: &str) -> Result<()> {
println!("Building {name}...");
let status = Command::new(super::cargo_path())
.args(["build", "--bin", name])
.status()?;
if status.success() {
Ok(())
} else {
Err(io::Error::from(io::ErrorKind::Interrupted).into())
}
}

View File

@@ -0,0 +1,86 @@
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Cargo metadata error: {0:?}")]
Metadata(#[from] cargo_metadata::Error),
#[error("Missing package metadata for {0}")]
MissingPackageMetadata(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Deserialize)]
struct PackageMetadata {
cef: Cef,
}
#[derive(Deserialize)]
struct Cef {
bundle: CargoBundleMetadata,
}
#[derive(Deserialize)]
struct CargoBundleMetadata {
#[cfg(target_os = "macos")]
helper_name: String,
resources_path: Option<String>,
}
pub struct BundleMetadata {
#[cfg(target_os = "macos")]
pub helper_name: String,
pub resources_path: Option<PathBuf>,
}
impl BundleMetadata {
pub fn parse(executable: &str, metadata: &cargo_metadata::Metadata) -> Option<Self> {
let package = metadata
.packages
.iter()
.find(|p| p.targets.iter().any(|t| t.name == executable))?;
let package_metadata =
serde_json::from_value::<PackageMetadata>(package.metadata.clone()).ok()?;
let resources_path = package_metadata
.cef
.bundle
.resources_path
.as_deref()
.and_then(|resources_path| {
package
.manifest_path
.clone()
.into_std_path_buf()
.parent()
.map(|manifest_dir| manifest_dir.join(resources_path))
});
Some(Self {
#[cfg(target_os = "macos")]
helper_name: package_metadata.cef.bundle.helper_name,
resources_path,
})
}
}
pub struct CargoMetadata(cargo_metadata::Metadata);
impl CargoMetadata {
pub fn target_directory(&self) -> PathBuf {
PathBuf::from(&self.0.target_directory)
}
pub fn parse_bundle_metadata(&self, executable: &str) -> Result<BundleMetadata> {
BundleMetadata::parse(executable, &self.0)
.ok_or_else(|| Error::MissingPackageMetadata(executable.to_owned()))
}
}
/// Run `cargo metadata` to determine the configuration for the current workspace/package.
pub fn get_cargo_metadata() -> Result<CargoMetadata> {
let metadata = cargo_metadata::MetadataCommand::new()
.no_deps()
.other_options(vec!["--frozen".to_string()])
.exec()?;
Ok(CargoMetadata(metadata))
}

18
cef/src/build_util/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::env;
pub mod metadata;
#[cfg(target_os = "macos")]
pub mod mac;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod win;
/// Prefer the path in the `CARGO` environment variable if specified, otherwise just execute
/// `cargo` from wherever it is found in the `PATH`.
fn cargo_path() -> String {
env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())
}

View File

@@ -0,0 +1,117 @@
use std::{
fs,
io::{self, Write},
path::{Path, PathBuf},
process::Command,
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("I/O error: {0:?}")]
Io(#[from] io::Error),
#[error("Metadata error: {0:?}")]
Metadata(#[from] super::metadata::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux
pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result<PathBuf> {
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
copy_directory(&cef_path, &app_path)?;
const LOCALES_DIR: &str = "locales";
copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?;
copy_app(app_path, target_path, executable_name)
}
/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target.
pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result<PathBuf> {
let cargo_metadata = super::metadata::get_cargo_metadata()?;
let target_path =
cargo_metadata
.target_directory()
.join(if release { "release" } else { "debug" });
cargo_build(executable_name, release)?;
bundle(app_path, &target_path, executable_name)
}
const MANIFEST_CONTENT: &[u8] = include_bytes!("cef-app.exe.manifest");
fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result<PathBuf> {
let mut manifest_file =
fs::File::create(app_path.join(format!("{executable_name}.exe.manifest")))?;
manifest_file.write_all(MANIFEST_CONTENT)?;
#[cfg(feature = "sandbox")]
{
let dll_name = format!("{executable_name}.dll");
let dll_path = app_path.join(&dll_name);
let target_dll = target_path.join(&dll_name);
fs::copy(&target_dll, &dll_path)?;
let pdb_name = format!("{executable_name}.pdb");
let pdb_path = app_path.join(&pdb_name);
let target_pdb = target_path.join(&pdb_name);
fs::copy(&target_pdb, &pdb_path)?;
let executable_name = format!("{executable_name}.exe");
let executable_path = app_path.join(&executable_name);
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
let target_executable = cef_path.join("bootstrap.exe");
fs::copy(&target_executable, &executable_path)?;
Ok(executable_path)
}
#[cfg(not(feature = "sandbox"))]
{
let executable_name = format!("{executable_name}.exe");
let executable_path = app_path.join(&executable_name);
let target_executable = target_path.join(executable_name);
fs::copy(&target_executable, &executable_path)?;
Ok(executable_path)
}
}
fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_file()
&& !entry
.path()
.extension()
.map(|ext| ext == "exe")
.unwrap_or_default()
{
fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}
fn cargo_build(name: &str, release: bool) -> Result<()> {
println!("Building {name}...");
let mut args = vec!["build"];
if release {
args.push("--release");
}
#[cfg(feature = "sandbox")]
args.extend_from_slice(&["-p", name, "--lib"]);
#[cfg(not(feature = "sandbox"))]
args.extend_from_slice(&["--no-default-features", "--bin", name]);
let status = Command::new(super::cargo_path()).args(args).status()?;
if status.success() {
Ok(())
} else {
Err(io::Error::from(io::ErrorKind::Interrupted).into())
}
}

View File

@@ -3,6 +3,8 @@
pub mod args;
pub mod rc;
pub mod string;
pub mod window_info;
pub mod wrapper;
#[cfg(target_os = "macos")]
pub mod application_mac;
@@ -16,10 +18,15 @@ pub mod sandbox;
#[cfg(feature = "accelerated_osr")]
pub mod osr_texture_import;
#[cfg(feature = "build-util")]
pub mod build_util;
#[rustfmt::skip]
mod bindings;
pub use bindings::*;
pub use rc::Rc as _;
pub use cef_dll_sys as sys;
#[cfg(all(
@@ -27,3 +34,7 @@ pub use cef_dll_sys as sys;
feature = "accelerated_osr"
))]
compile_error!("accelerated_osr not supported on this platform");
pub const SEEK_SET: i32 = 0;
pub const SEEK_CUR: i32 = 1;
pub const SEEK_END: i32 = 2;

View File

@@ -43,6 +43,12 @@ impl Sandbox {
}
}
impl Default for Sandbox {
fn default() -> Self {
Self::new()
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
unsafe {

View File

@@ -856,6 +856,7 @@ impl<'a, T> From<&'a mut CefStringCollection<T>> for Option<&'a mut T> {
}
/// See [_cef_string_list_t] for more documentation.
#[derive(Clone)]
pub struct CefStringList(CefStringCollection<_cef_string_list_t>);
impl CefStringList {
@@ -988,6 +989,7 @@ impl Debug for CefStringList {
}
/// See [_cef_string_map_t] for more documentation.
#[derive(Clone)]
pub struct CefStringMap(CefStringCollection<_cef_string_map_t>);
impl CefStringMap {
@@ -1134,6 +1136,7 @@ impl Debug for CefStringMap {
}
/// See [_cef_string_multimap_t] for more documentation.
#[derive(Clone)]
pub struct CefStringMultimap(CefStringCollection<_cef_string_multimap_t>);
impl CefStringMultimap {

60
cef/src/window_info.rs Normal file
View File

@@ -0,0 +1,60 @@
use crate::{sys::cef_window_handle_t, *};
#[cfg(target_os = "windows")]
use windows_sys::Win32::UI::WindowsAndMessaging::*;
impl WindowInfo {
/// Create the browser as a child window.
pub fn set_as_child(self, parent: cef_window_handle_t, bounds: &Rect) -> Self {
Self {
#[cfg(target_os = "windows")]
style: WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_TABSTOP | WS_VISIBLE,
#[cfg(any(target_os = "linux", target_os = "windows"))]
parent_window: parent,
#[cfg(target_os = "macos")]
parent_view: parent,
bounds: bounds.clone(),
#[cfg(target_os = "macos")]
hidden: 0,
..self
}
}
/// Create the browser as a popup window.
#[cfg(target_os = "windows")]
pub fn set_as_popup(self, parent: cef_window_handle_t, title: &str) -> Self {
Self {
window_name: CefString::from(title),
parent_window: parent,
style: WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE,
bounds: Rect {
x: CW_USEDEFAULT,
y: CW_USEDEFAULT,
width: CW_USEDEFAULT,
height: CW_USEDEFAULT,
},
..self
}
}
/// Create the browser using windowless (off-screen) rendering. No window
/// will be created for the browser and all rendering will occur via the
/// CefRenderHandler interface. The |parent| value will be used to identify
/// monitor info and to act as the parent window for dialogs, context menus,
/// etc. If |parent| is not provided then the main screen monitor will be used
/// and some functionality that requires a parent window may not function
/// correctly. In order to create windowless browsers the
/// CefSettings.windowless_rendering_enabled value must be set to true.
/// Transparent painting is enabled by default but can be disabled by setting
/// CefBrowserSettings.background_color to an opaque value.
pub fn set_as_windowless(self, parent: cef_window_handle_t) -> Self {
Self {
windowless_rendering_enabled: 1,
#[cfg(any(target_os = "linux", target_os = "windows"))]
parent_window: parent,
#[cfg(target_os = "macos")]
parent_view: parent,
runtime_style: RuntimeStyle::ALLOY,
..self
}
}
}

View File

@@ -0,0 +1,164 @@
use std::{collections::BTreeMap, ops::ControlFlow};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum BrowserInfoMapVisitorResult {
/// Remove the entry from the map.
RemoveEntry,
/// Keep the entry in the map.
KeepEntry,
}
/// Implement this interface to visit and optionally delete objects in the map.
pub trait BrowserInfoMapVisitor<K: Copy + Ord, V: Clone> {
fn on_next_info(
&self,
browser_id: i32,
key: K,
value: &V,
) -> ControlFlow<BrowserInfoMapVisitorResult, BrowserInfoMapVisitorResult>;
}
#[derive(Default)]
pub struct BrowserInfoMap<K: Clone + Ord, V: Clone> {
map: BTreeMap<i32, BTreeMap<K, V>>,
}
impl<K: Copy + Ord, V: Clone> BrowserInfoMap<K, V> {
/// Add an object associated with the specified ID values.
pub fn insert(&mut self, browser_id: i32, key: K, value: V) {
self.map.entry(browser_id).or_default().insert(key, value);
}
/// Find the object with the specified ID values. |visitor| can optionally be
/// used to evaluate or remove the object at the same time. If the object is
/// removed using the Visitor the caller is responsible for destroying it.
pub fn find(
&mut self,
browser_id: i32,
key: K,
visitor: Option<&dyn BrowserInfoMapVisitor<K, V>>,
) -> Option<V> {
let info_map = self.map.get_mut(&browser_id)?;
let entry = info_map.get(&key)?;
if let Some(visitor) = visitor {
let result = match visitor.on_next_info(browser_id, key, entry) {
ControlFlow::Break(result) => result,
ControlFlow::Continue(result) => result,
};
if result == BrowserInfoMapVisitorResult::RemoveEntry {
let entry = info_map.remove(&key);
if info_map.is_empty() {
self.map.remove(&browser_id);
}
return entry;
}
}
Some(entry.clone())
}
/// Find all objects. If any objects are removed using the Visitor the caller
/// is responsible for destroying them.
pub fn find_all(&mut self, visitor: &dyn BrowserInfoMapVisitor<K, V>) {
let browser_ids: Vec<_> = self.map.keys().copied().collect();
for browser_id in browser_ids {
let info_map = self
.map
.get_mut(&browser_id)
.expect("missing browser info map");
let mut keep_going = true;
let mut removed = vec![];
let keys: Vec<_> = info_map.keys().copied().collect();
for key in keys {
let value = info_map.get(&key).expect("missing value");
let result = visitor.on_next_info(browser_id, key, value);
let (stop, result) = match result {
ControlFlow::Break(result) => (true, result),
ControlFlow::Continue(result) => (false, result),
};
if result == BrowserInfoMapVisitorResult::RemoveEntry {
removed.push(key);
}
if stop {
keep_going = false;
break;
}
}
for key in removed {
info_map.remove(&key);
}
if info_map.is_empty() {
self.map.remove(&browser_id);
}
if !keep_going {
break;
}
}
}
/// Find all objects associated with the specified browser. If any objects are
/// removed using the Visitor the caller is responsible for destroying them.
pub fn find_browser_all(&mut self, browser_id: i32, visitor: &dyn BrowserInfoMapVisitor<K, V>) {
let info_map = self
.map
.get_mut(&browser_id)
.expect("missing browser info map");
let mut removed = vec![];
let keys: Vec<_> = info_map.keys().copied().collect();
for key in keys {
let value = info_map.get(&key).expect("missing value");
let result = visitor.on_next_info(browser_id, key, value);
let (stop, result) = match result {
ControlFlow::Break(result) => (true, result),
ControlFlow::Continue(result) => (false, result),
};
if result == BrowserInfoMapVisitorResult::RemoveEntry {
removed.push(key);
}
if stop {
break;
}
}
for key in removed {
info_map.remove(&key);
}
if info_map.is_empty() {
self.map.remove(&browser_id);
}
}
/// Returns true if the map is empty.
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
/// Returns the number of objects in the map.
pub fn len(&self) -> usize {
self.map.values().map(|info_map| info_map.len()).sum()
}
/// Returns the number of objects in the map that are associated with the
/// specified browser.
pub fn browser_len(&self, browser_id: i32) -> usize {
self.map
.get(&browser_id)
.map_or(0, |info_map| info_map.len())
}
pub fn clear(&mut self) {
self.map.clear();
}
}

View File

@@ -0,0 +1,110 @@
//! Thread safe implementation of the [ReadHandler] type for reading an in-memory array of bytes.
use crate::*;
use std::sync::{Arc, Mutex};
pub struct ByteStream {
bytes: Vec<u8>,
offset: usize,
}
impl ByteStream {
pub fn new(bytes: Vec<u8>) -> Self {
ByteStream { bytes, offset: 0 }
}
}
wrap_read_handler! {
pub struct ByteReadHandler {
stream: Arc<Mutex<ByteStream>>,
}
impl ReadHandler {
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn read(&self, ptr: *mut u8, size: usize, n: usize) -> usize {
let Ok(mut stream) = self.stream.lock() else {
return 0;
};
let s = (stream.bytes.len() - stream.offset) / size;
let ret = s.min(n);
let buffer = unsafe { std::slice::from_raw_parts_mut(ptr, ret * size) };
buffer.copy_from_slice(&stream.bytes[stream.offset..stream.offset + ret * size]);
stream.offset += ret * size;
ret
}
fn seek(&self, offset: i64, whence: ::std::os::raw::c_int) -> ::std::os::raw::c_int {
let Ok(mut stream) = self.stream.lock() else {
return -1;
};
const SEEK_SET: i32 = 0;
const SEEK_CUR: i32 = 1;
const SEEK_END: i32 = 2;
match whence {
SEEK_SET => {
if offset < 0 {
return -1;
}
let offset = offset as usize;
if offset > stream.bytes.len() {
return -1;
}
stream.offset = offset;
0
}
SEEK_CUR => {
if offset < 0 {
let offset = -offset as usize;
if offset > stream.offset {
return -1;
}
stream.offset -= offset;
} else {
let offset = offset as usize;
if offset + stream.offset > stream.bytes.len() {
return -1;
}
stream.offset += offset;
}
0
}
SEEK_END => {
if offset > 0 {
return -1;
}
let offset = -offset as usize;
if offset > stream.bytes.len() {
return -1;
}
stream.offset = stream.bytes.len() - offset;
0
}
_ => -1,
}
}
fn tell(&self) -> i64 {
let Ok(stream) = self.stream.lock() else {
return 0;
};
stream.offset as i64
}
fn eof(&self) -> i32 {
let Ok(stream) = self.stream.lock() else {
return 1;
};
if stream.offset >= stream.bytes.len() {
1
} else {
0
}
}
fn may_block(&self) -> i32 {
0
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,726 @@
use super::message_router::*;
use crate::*;
use std::{
io::{self, Cursor, Read, Write},
marker::PhantomData,
mem,
rc::Rc,
slice,
sync::Arc,
};
const NO_ERROR: i32 = 0;
const CONTEXT_ID: usize = 0;
const REQUEST_ID: usize = 1;
const RENDERER_PAYLOAD: usize = 2;
const IS_SUCCESS: usize = 2;
const BROWSER_PAYLOAD: usize = 3;
const IS_PERSISTENT: usize = 3;
pub trait ProcessMessageBuilder {
fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option<ProcessMessage>;
fn build_renderer_message(
&self,
context_id: i32,
request_id: i32,
persistent: bool,
) -> Option<ProcessMessage>;
}
#[derive(Clone, Default)]
pub enum MessagePayload {
#[default]
Empty,
String(CefStringUtf8),
Binary(Arc<dyn BinaryBuffer>),
}
impl MessagePayload {
fn size(&self) -> usize {
match self {
Self::Empty => 0,
Self::String(s) => s.as_slice().map(|s| s.len()).unwrap_or(0),
Self::Binary(b) => b.data().len(),
}
}
fn read_string<R: Read>(f: &mut R) -> io::Result<Self> {
let mut buffer = vec![];
f.read_to_end(&mut buffer)?;
Ok(String::from_utf8(buffer)
.map(|value| value.as_str().into())
.unwrap_or(Self::Empty))
}
// fn read_binary<R: Read>(f: &mut R) -> io::Result<Self> {
// let mut buffer = vec![];
// f.read_to_end(&mut buffer)?;
// Ok(binary_value_create(Some(&buffer))
// .map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value)))))
// .unwrap_or(Self::Empty))
// }
fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
let buffer = match self {
Self::String(s) => s.as_slice(),
Self::Binary(b) if !b.data().is_empty() => Some(b.data()),
_ => None,
};
if let Some(buffer) = buffer {
f.write_all(buffer)?;
}
Ok(())
}
}
impl From<&CefString> for MessagePayload {
fn from(s: &CefString) -> Self {
Self::String(CefStringUtf8::from(s))
}
}
impl From<&str> for MessagePayload {
fn from(s: &str) -> Self {
Self::String(CefStringUtf8::from(s))
}
}
impl From<Option<&V8Value>> for MessagePayload {
fn from(v: Option<&V8Value>) -> Self {
let b = v.and_then(|v| {
let ptr = v.array_buffer_data();
let size = v.array_buffer_byte_length();
if ptr.is_null() || size == 0 {
return None;
}
let data = unsafe { slice::from_raw_parts(ptr as *const u8, size) };
binary_value_create(Some(data))
});
b.map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value)))))
.unwrap_or(Self::Empty)
}
}
impl From<&[u8]> for MessagePayload {
fn from(data: &[u8]) -> Self {
if data.is_empty() {
Self::Empty
} else {
Self::Binary(Arc::new(BinaryValueBuffer::new(
None,
binary_value_create(Some(data)),
)))
}
}
}
#[derive(Clone, Default)]
pub struct BrowserMessage {
pub context_id: i32,
pub request_id: i32,
pub is_success: bool,
pub error_code: i32,
pub payload: MessagePayload,
}
impl From<Option<ProcessMessage>> for BrowserMessage {
fn from(message: Option<ProcessMessage>) -> Self {
let Some(message) = message else {
return Default::default();
};
if let Some(args) = message.argument_list() {
let context_id = args.int(CONTEXT_ID);
let request_id = args.int(REQUEST_ID);
let is_success = args.bool(IS_SUCCESS);
if is_success != 0 {
debug_assert_eq!(args.size(), 4);
match args.get_type(BROWSER_PAYLOAD) {
ValueType::STRING => Self {
context_id,
request_id,
is_success: true,
error_code: NO_ERROR,
payload: MessagePayload::String(CefStringUtf8::from(&CefString::from(
&args.string(BROWSER_PAYLOAD),
))),
},
ValueType::BINARY => Self {
context_id,
request_id,
is_success: true,
error_code: NO_ERROR,
payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new(
Some(message),
args.binary(BROWSER_PAYLOAD),
))),
},
payload_type => {
assert_eq!(payload_type, ValueType::NULL);
Self {
context_id,
request_id,
is_success: true,
error_code: NO_ERROR,
payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)),
}
}
}
} else {
debug_assert_eq!(args.size(), 5);
Self {
context_id,
request_id,
is_success: false,
error_code: args.int(3),
payload: MessagePayload::String(CefStringUtf8::from(&CefString::from(
&args.string(4),
))),
}
}
} else if let Some(region) = message
.shared_memory_region()
.filter(|region| region.is_valid() != 0)
{
debug_assert!(region.size() >= BrowserMessageHeader::SIZE);
debug_assert!(!region.memory().is_null());
let buffer =
unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) };
let header = {
let mut cursor = Cursor::new(&buffer[..BrowserMessageHeader::SIZE]);
BrowserMessageHeader::read(&mut cursor).unwrap_or_default()
};
if header.is_binary {
Self {
context_id: header.context_id,
request_id: header.request_id,
is_success: true,
error_code: NO_ERROR,
payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new(
Some(region),
BrowserMessageHeader::SIZE,
))),
}
} else {
let mut cursor = Cursor::new(&buffer[BrowserMessageHeader::SIZE..]);
Self {
context_id: header.context_id,
request_id: header.request_id,
is_success: true,
error_code: NO_ERROR,
payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(),
}
}
} else {
Default::default()
}
}
}
#[derive(Clone, Default)]
pub struct RenderMessage {
pub context_id: i32,
pub request_id: i32,
pub is_persistent: bool,
pub payload: MessagePayload,
}
impl From<Option<ProcessMessage>> for RenderMessage {
fn from(message: Option<ProcessMessage>) -> Self {
let Some(message) = message else {
return Default::default();
};
if let Some(args) = message.argument_list() {
debug_assert_eq!(args.size(), 4);
let context_id = args.int(CONTEXT_ID);
let request_id = args.int(REQUEST_ID);
let is_persistent = args.bool(IS_PERSISTENT) != 0;
match args.get_type(RENDERER_PAYLOAD) {
ValueType::STRING => Self {
context_id,
request_id,
is_persistent,
payload: MessagePayload::String(CefStringUtf8::from(&CefString::from(
&args.string(RENDERER_PAYLOAD),
))),
},
ValueType::BINARY => Self {
context_id,
request_id,
is_persistent,
payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new(
Some(message),
args.binary(RENDERER_PAYLOAD),
))),
},
payload_type => {
debug_assert_eq!(payload_type, ValueType::NULL);
Self {
context_id,
request_id,
is_persistent,
payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)),
}
}
}
} else if let Some(region) = message
.shared_memory_region()
.filter(|region| region.is_valid() != 0)
{
debug_assert!(region.size() >= RendererMessageHeader::<true>::SIZE);
debug_assert!(!region.memory().is_null());
let buffer =
unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) };
let header = {
let mut cursor = Cursor::new(&buffer[..RendererMessageHeader::<true>::SIZE]);
RendererMessageHeader::<true>::read(&mut cursor).unwrap_or_default()
};
if header.is_binary {
Self {
context_id: header.context_id,
request_id: header.request_id,
is_persistent: header.is_persistent,
payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new(
Some(region),
RendererMessageHeader::<true>::SIZE,
))),
}
} else {
let mut cursor = Cursor::new(&buffer[RendererMessageHeader::<true>::SIZE..]);
Self {
context_id: header.context_id,
request_id: header.request_id,
is_persistent: header.is_persistent,
payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(),
}
}
} else {
Default::default()
}
}
}
#[cfg(not(feature = "sandbox"))]
wrap_v8_array_buffer_release_callback! {
pub struct BinaryValueArrayBufferReleaseCallback {
value: MessagePayload,
}
impl V8ArrayBufferReleaseCallback {
fn release_buffer(&self, _buffer: *mut u8) {}
}
}
trait MessageHeader: Sized {
const SIZE: usize;
fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self;
fn read<R: Read>(f: &mut R) -> io::Result<Self>;
fn write<W: Write>(&self, f: &mut W) -> io::Result<()>;
}
#[derive(Clone, Default)]
struct BrowserMessageHeader {
context_id: i32,
request_id: i32,
is_binary: bool,
}
impl MessageHeader for BrowserMessageHeader {
const SIZE: usize = mem::size_of::<i32>() * 2 + 1;
fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self {
Self {
context_id,
request_id,
is_binary,
}
}
fn read<R: Read>(f: &mut R) -> io::Result<Self> {
let mut data = [0_u8; mem::size_of::<i32>()];
f.read_exact(&mut data)?;
let context_id = i32::from_ne_bytes(data);
f.read_exact(&mut data)?;
let request_id = i32::from_ne_bytes(data);
let mut flags = [0_u8];
f.read_exact(&mut flags)?;
let is_binary = flags[0] != 0;
Ok(Self {
context_id,
request_id,
is_binary,
})
}
fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
f.write_all(&self.context_id.to_ne_bytes())?;
f.write_all(&self.request_id.to_ne_bytes())?;
f.write_all(&[self.is_binary.into()])
}
}
#[derive(Clone, Default)]
struct RendererMessageHeader<const DEFAULT_PERSISTENT: bool> {
context_id: i32,
request_id: i32,
is_persistent: bool,
is_binary: bool,
}
impl<const DEFAULT_PERSISTENT: bool> MessageHeader for RendererMessageHeader<DEFAULT_PERSISTENT> {
const SIZE: usize = mem::size_of::<i32>() * 2 + 2;
fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self {
Self {
context_id,
request_id,
is_persistent: DEFAULT_PERSISTENT,
is_binary,
}
}
fn read<R: Read>(f: &mut R) -> io::Result<Self> {
let mut data = [0_u8; mem::size_of::<i32>()];
f.read_exact(&mut data)?;
let context_id = i32::from_ne_bytes(data);
f.read_exact(&mut data)?;
let request_id = i32::from_ne_bytes(data);
let mut flags = [0_u8; 2];
f.read_exact(&mut flags)?;
let is_persistent = flags[0] != 0;
let is_binary = flags[1] != 0;
Ok(Self {
context_id,
request_id,
is_persistent,
is_binary,
})
}
fn write<W: Write>(&self, f: &mut W) -> io::Result<()> {
f.write_all(&self.context_id.to_ne_bytes())?;
f.write_all(&self.request_id.to_ne_bytes())?;
f.write_all(&[self.is_persistent.into()])?;
f.write_all(&[self.is_binary.into()])
}
}
fn build_browser_list_message(
name: &str,
context_id: i32,
request_id: i32,
payload: MessagePayload,
) -> Option<ProcessMessage> {
let message = process_message_create(Some(&CefString::from(name)))?;
let args = message.argument_list()?;
args.set_int(CONTEXT_ID, context_id);
args.set_int(REQUEST_ID, request_id);
args.set_bool(IS_SUCCESS, 1);
match payload {
MessagePayload::Empty => args.set_null(BROWSER_PAYLOAD),
MessagePayload::String(value) => {
args.set_string(BROWSER_PAYLOAD, Some(&CefString::from(&value)))
}
MessagePayload::Binary(value) => args.set_binary(
BROWSER_PAYLOAD,
binary_value_create(Some(value.data())).as_mut(),
),
};
Some(message)
}
fn build_render_list_message(
name: &str,
context_id: i32,
request_id: i32,
payload: MessagePayload,
is_persistent: bool,
) -> Option<ProcessMessage> {
let message = process_message_create(Some(&CefString::from(name)))?;
let args = message.argument_list()?;
args.set_int(CONTEXT_ID, context_id);
args.set_int(REQUEST_ID, request_id);
match payload {
MessagePayload::Empty => args.set_null(RENDERER_PAYLOAD),
MessagePayload::String(value) => {
args.set_string(RENDERER_PAYLOAD, Some(&CefString::from(&value)))
}
MessagePayload::Binary(value) => args.set_binary(
RENDERER_PAYLOAD,
binary_value_create(Some(value.data())).as_mut(),
),
};
args.set_bool(IS_PERSISTENT, is_persistent.into());
Some(message)
}
struct MessagePayloadBuilder {
pub name: String,
pub payload: MessagePayload,
}
impl ProcessMessageBuilder for MessagePayloadBuilder {
fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option<ProcessMessage> {
build_browser_list_message(&self.name, context_id, request_id, self.payload.clone())
}
fn build_renderer_message(
&self,
context_id: i32,
request_id: i32,
persistent: bool,
) -> Option<ProcessMessage> {
build_render_list_message(
&self.name,
context_id,
request_id,
self.payload.clone(),
persistent,
)
}
}
enum SharedProcessMessageRouter<Header>
where
Header: MessageHeader,
{
SharedMemory {
builder: SharedProcessMessageBuilder,
is_binary: bool,
_phantom: PhantomData<Header>,
},
Payload(MessagePayloadBuilder),
}
impl<Header> SharedProcessMessageRouter<Header>
where
Header: MessageHeader,
{
fn new(name: &str, payload: MessagePayload) -> Self {
let message_size = Header::SIZE + payload.size();
let builder =
match shared_process_message_builder_create(Some(&CefString::from(name)), message_size)
{
Some(builder) if builder.is_valid() != 0 => builder,
_ => {
return Self::Payload(MessagePayloadBuilder {
name: name.to_owned(),
payload,
})
}
};
let buffer =
unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, message_size) };
let mut cursor = Cursor::new(&mut buffer[Header::SIZE..]);
if payload.write(&mut cursor).is_err() {
return Self::Payload(MessagePayloadBuilder {
name: name.to_owned(),
payload,
});
}
Self::SharedMemory {
builder,
is_binary: matches!(payload, MessagePayload::Binary(_)),
_phantom: PhantomData,
}
}
}
impl<Header> ProcessMessageBuilder for SharedProcessMessageRouter<Header>
where
Header: MessageHeader,
{
fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option<ProcessMessage> {
match self {
Self::SharedMemory {
builder, is_binary, ..
} => {
let buffer =
unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) };
let mut cursor = Cursor::new(buffer);
Header::new(context_id, request_id, *is_binary)
.write(&mut cursor)
.ok()?;
builder.build()
}
Self::Payload(builder) => builder.build_browser_response(context_id, request_id),
}
}
fn build_renderer_message(
&self,
context_id: i32,
request_id: i32,
persistent: bool,
) -> Option<ProcessMessage> {
match self {
Self::SharedMemory {
builder, is_binary, ..
} => {
let buffer =
unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) };
let mut cursor = Cursor::new(buffer);
Header::new(context_id, request_id, *is_binary)
.write(&mut cursor)
.ok()?;
builder.build()
}
Self::Payload(builder) => {
builder.build_renderer_message(context_id, request_id, persistent)
}
}
}
}
pub struct EmptyBinaryBuffer;
impl BinaryBuffer for EmptyBinaryBuffer {
fn data(&self) -> &[u8] {
&[]
}
fn data_mut(&mut self) -> &mut [u8] {
&mut []
}
}
pub struct BinaryValueBuffer {
_message: Option<ProcessMessage>,
value: Option<BinaryValue>,
}
impl BinaryValueBuffer {
pub fn new(message: Option<ProcessMessage>, value: Option<BinaryValue>) -> Self {
Self {
_message: message,
value,
}
}
}
impl BinaryBuffer for BinaryValueBuffer {
fn data(&self) -> &[u8] {
self.value.as_ref().map_or(&[], |v| unsafe {
slice::from_raw_parts(v.raw_data() as *const u8, v.size())
})
}
fn data_mut(&mut self) -> &mut [u8] {
self.value.as_mut().map_or(&mut [], |v| unsafe {
slice::from_raw_parts_mut(v.raw_data() as *mut u8, v.size())
})
}
}
struct SharedMemoryRegionBuffer {
region: Option<SharedMemoryRegion>,
offset: usize,
}
impl SharedMemoryRegionBuffer {
fn new(region: Option<SharedMemoryRegion>, offset: usize) -> Self {
Self { region, offset }
}
fn data(&self) -> *mut u8 {
self.region
.as_ref()
.map_or(std::ptr::null_mut(), |r| unsafe {
r.memory().add(self.offset) as *mut u8
})
}
fn size(&self) -> usize {
self.region
.as_ref()
.map_or(0, |r| r.size().saturating_sub(self.offset))
}
}
impl BinaryBuffer for SharedMemoryRegionBuffer {
fn data(&self) -> &[u8] {
let data = self.data();
let size = self.size();
if data.is_null() || size == 0 {
&[]
} else {
unsafe { slice::from_raw_parts(data as *const u8, size) }
}
}
fn data_mut(&mut self) -> &mut [u8] {
let data = self.data();
let size = self.size();
if data.is_null() || size == 0 {
&mut []
} else {
unsafe { slice::from_raw_parts_mut(data, size) }
}
}
}
pub fn create_browser_response_builder(
threshold: usize,
name: &str,
payload: MessagePayload,
) -> Rc<dyn ProcessMessageBuilder> {
if payload.size() < threshold {
Rc::new(MessagePayloadBuilder {
name: name.to_string(),
payload,
})
} else {
Rc::new(SharedProcessMessageRouter::<BrowserMessageHeader>::new(
name, payload,
))
}
}
pub fn build_renderer_message(
threshold: usize,
name: &str,
context_id: i32,
request_id: i32,
request: Option<&V8Value>,
persistent: bool,
) -> Option<ProcessMessage> {
let payload = request
.map(|request| {
if request.is_string() != 0 {
MessagePayload::from(&CefString::from(&request.string_value()))
} else {
MessagePayload::from(Some(request))
}
})
.unwrap_or(MessagePayload::Empty);
let builder: Box<dyn ProcessMessageBuilder> = if payload.size() < threshold {
Box::new(MessagePayloadBuilder {
name: name.to_string(),
payload,
})
} else if persistent {
Box::new(SharedProcessMessageRouter::<RendererMessageHeader<true>>::new(name, payload))
} else {
Box::new(SharedProcessMessageRouter::<RendererMessageHeader<false>>::new(name, payload))
};
builder.build_renderer_message(context_id, request_id, persistent)
}

8
cef/src/wrapper/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod browser_info_map;
pub mod byte_read_handler;
pub mod message_router;
pub mod resource_manager;
pub mod stream_resource_handler;
pub mod zip_archive;
mod message_router_utils;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
//! Implementation of the ResourceHandler class for reading from a Stream.
use crate::*;
wrap_resource_handler! {
pub struct StreamResourceHandler {
status_code: i32,
status_text: String,
mime_type: String,
header_map: Option<CefStringMultimap>,
stream: Option<StreamReader>,
}
impl ResourceHandler {
fn open(
&self,
_request: Option<&mut Request>,
handle_request: Option<&mut i32>,
_callback: Option<&mut Callback>,
) -> i32 {
debug_assert_eq!(
currently_on(ThreadId::UI),
0,
"open must not be called on the UI thread"
);
debug_assert_eq!(
currently_on(ThreadId::IO),
0,
"open must not be called on the IO thread"
);
// Continue the request immediately.
if let Some(handle_request) = handle_request {
*handle_request = 1;
}
1
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
_redirect_url: Option<&mut CefString>,
) {
debug_assert_ne!(
currently_on(ThreadId::IO),
0,
"response_headers must be called on the IO thread"
);
let Some(response) = response else {
return;
};
response.set_status(self.status_code);
response.set_status_text(Some(&CefString::from(self.status_text.as_str())));
response.set_mime_type(Some(&CefString::from(self.mime_type.as_str())));
if let Some(mut header_map) = self.header_map.clone() {
response.set_header_map(Some(&mut header_map));
}
if let Some(response_length) = response_length {
*response_length = if self.stream.is_some() { -1 } else { 0 };
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn read(
&self,
data_out: *mut u8,
bytes_to_read: i32,
bytes_read: Option<&mut i32>,
_callback: Option<&mut ResourceReadCallback>,
) -> i32 {
debug_assert_eq!(
currently_on(ThreadId::UI),
0,
"read must not be called on the UI thread"
);
debug_assert_eq!(
currently_on(ThreadId::IO),
0,
"read must not be called on the IO thread"
);
if bytes_to_read < 1 {
return 0;
}
let Some(bytes_read) = bytes_read else {
return 0;
};
let Some(stream) = &self.stream else {
*bytes_read = 0;
return 1;
};
// Read until the buffer is full or until read() returns 0 to indicate no more data.
*bytes_read = 0;
loop {
let data_out = unsafe { data_out.add(*bytes_read as usize) };
let read = stream.read(data_out, 1, (bytes_to_read - *bytes_read) as usize);
*bytes_read += read as i32;
if read == 0 || *bytes_read >= bytes_to_read {
break;
}
}
if *bytes_read > 0 {
1
} else {
0
}
}
}
}
impl StreamResourceHandler {
pub fn new_with_stream(mime_type: String, stream: StreamReader) -> ResourceHandler {
Self::new(200, "OK".to_string(), mime_type, None, Some(stream))
}
}

View File

@@ -0,0 +1,131 @@
use super::byte_read_handler::*;
use crate::*;
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
pub trait File: Send {
fn stream_reader(&self) -> Option<StreamReader>;
}
pub type FileMap = BTreeMap<String, Arc<Mutex<dyn File>>>;
struct ZipFile {
data: Vec<u8>,
}
impl File for ZipFile {
fn stream_reader(&self) -> Option<StreamReader> {
let mut handler =
ByteReadHandler::new(Arc::new(Mutex::new(ByteStream::new(self.data.clone()))));
stream_reader_create_for_handler(Some(&mut handler))
}
}
#[derive(Default)]
pub struct ZipArchive {
contents: Mutex<FileMap>,
}
impl ZipArchive {
pub fn load(
&self,
stream: &mut StreamReader,
password: &str,
overwrite_existing: bool,
) -> usize {
let Ok(mut contents) = self.contents.lock() else {
return 0;
};
let Some(reader) = zip_reader_create(Some(stream)) else {
return 0;
};
let password = CefString::from(password);
let mut count = 0;
loop {
let size = reader.file_size();
if size <= 0 {
// Skip directories and empty files.
continue;
}
let size = size as usize;
if reader.open_file(Some(&password)) == 0 {
break;
}
let name = CefString::from(&reader.file_name())
.to_string()
.to_lowercase();
if contents.contains_key(&name) {
if overwrite_existing {
contents.remove(&name);
} else {
// Skip files that already exist.
continue;
}
}
let mut data = Vec::with_capacity(size);
while data.len() < size && reader.eof() == 0 {
let mut chunk = vec![0; size - data.len()];
let read = reader.read_file(Some(&mut chunk));
if read <= 0 {
break;
}
data.extend_from_slice(&chunk[..read as usize]);
}
debug_assert_eq!(data.len(), size);
reader.close_file();
count += 1;
// Add the file to the map.
contents.insert(name, Arc::new(Mutex::new(ZipFile { data })));
if reader.move_to_next_file() == 0 {
break;
}
}
count
}
pub fn clear(&self) {
let Ok(mut contents) = self.contents.lock() else {
return;
};
contents.clear();
}
pub fn file_count(&self) -> usize {
let Ok(contents) = self.contents.lock() else {
return 0;
};
contents.len()
}
pub fn file(&self, name: &str) -> Option<Arc<Mutex<dyn File>>> {
let Ok(contents) = self.contents.lock() else {
return None;
};
contents.get(name).cloned()
}
pub fn remove_file(&self, name: &str) -> bool {
let Ok(mut contents) = self.contents.lock() else {
return false;
};
contents.remove(name).is_some()
}
pub fn files(&self) -> FileMap {
let Ok(contents) = self.contents.lock() else {
return Default::default();
};
contents.clone()
}
}

View File

@@ -357,7 +357,7 @@ impl CefVersion {
where
P: AsRef<Path>,
{
let mut result = self.download_archive_from(&url, &location, show_progress);
let mut result = self.download_archive_from(url, &location, show_progress);
let mut retry = 0;
while let Err(Error::Io(_)) = &result {
@@ -368,7 +368,7 @@ impl CefVersion {
retry += 1;
thread::sleep(retry_delay * retry);
result = self.download_archive_from(&url, &location, show_progress);
result = self.download_archive_from(url, &location, show_progress);
}
result
@@ -459,7 +459,7 @@ where
println!("Downloading CEF archive for {target}...");
}
let index = CefIndex::download_from(&url)?;
let index = CefIndex::download_from(url)?;
let platform = index.platform(target)?;
let version = platform.version(cef_version)?;

View File

@@ -3,27 +3,55 @@ name = "cefsimple"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[[bin]]
name = "cefsimple"
[[bin]]
name = "cefsimple_helper"
path = "src/mac/helper.rs"
[[bin]]
name = "bundle_cefsimple"
path = "src/mac/bundle_cefsimple.rs"
[features]
default = ["sandbox"]
sandbox = ["cef/sandbox"]
default = [
"sandbox",
]
sandbox = [
"cef/sandbox",
]
linux-x11 = [
"x11-dl",
]
[package.metadata.cef.bundle]
helper_name = "cefsimple_helper"
resources_path = "resources"
[dependencies]
cef.workspace = true
cef-dll-sys.workspace = true
cef = { workspace = true, features = [
"build-util",
] }
[target.'cfg(target_os = "macos")'.dependencies]
plist.workspace = true
serde.workspace = true
objc2.workspace = true
objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] }
objc2-foundation.workspace = true
objc2-app-kit = { workspace = true, features = [
"NSApplication",
"NSEvent",
"NSResponder",
"NSUserInterfaceValidation",
"NSView",
"NSWindow",
] }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
] }
[target.'cfg(target_os = "windows")'.build-dependencies]
winres.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
x11-dl = { workspace = true, optional = true }

View File

@@ -0,0 +1,14 @@
#[cfg(target_os = "windows")]
include!("src/shared/resources.rs");
#[cfg(target_os = "windows")]
fn main() {
winres::WindowsResource::new()
.set_icon_with_id("resources/win/cefsimple.ico", &IDI_CEFSIMPLE.to_string())
.set_icon_with_id("resources/win/small.ico", &IDI_SMALL.to_string())
.compile()
.unwrap();
}
#[cfg(not(target_os = "windows"))]
fn main() {}

View File

@@ -0,0 +1,3 @@
/* Localized versions of Info.plist keys */
NSHumanReadableCopyright = "© Chromium Embedded Framework Authors, 2013";

View File

@@ -0,0 +1,330 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="16B2657" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<dependencies>
<deployment version="1090" identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application"/>
<menu title="AMainMenu" systemMenu="main" id="29" userLabel="MainMenu">
<items>
<menuItem title="cefsimple" id="56">
<menu key="submenu" title="TestShell" systemMenu="apple" id="57">
<items>
<menuItem title="About cefsimple" id="58">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="236">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Preferences…" keyEquivalent="," id="129" userLabel="121"/>
<menuItem isSeparatorItem="YES" id="143">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Services" id="131">
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="144">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Hide cefsimple" keyEquivalent="h" id="134">
<connections>
<action selector="hide:" target="-1" id="367"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="145">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="368"/>
</connections>
</menuItem>
<menuItem title="Show All" id="150">
<connections>
<action selector="unhideAllApplications:" target="-1" id="370"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="149">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Quit cefsimple" keyEquivalent="q" id="136" userLabel="1111">
<connections>
<action selector="terminate:" target="-1" id="369"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="83">
<menu key="submenu" title="File" id="81">
<items>
<menuItem title="New" keyEquivalent="n" id="82" userLabel="9">
<connections>
<action selector="newDocument:" target="-1" id="373"/>
</connections>
</menuItem>
<menuItem title="Open…" keyEquivalent="o" id="72">
<connections>
<action selector="openDocument:" target="-1" id="374"/>
</connections>
</menuItem>
<menuItem title="Open Recent" id="124">
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="125">
<items>
<menuItem title="Clear Menu" id="126">
<connections>
<action selector="clearRecentDocuments:" target="-1" id="127"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="79" userLabel="7">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Close" keyEquivalent="w" id="73" userLabel="1">
<connections>
<action selector="performClose:" target="-1" id="193"/>
</connections>
</menuItem>
<menuItem title="Save" keyEquivalent="s" id="75" userLabel="3">
<connections>
<action selector="saveDocument:" target="-1" id="362"/>
</connections>
</menuItem>
<menuItem title="Save As…" keyEquivalent="S" id="80" userLabel="8">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="saveDocumentAs:" target="-1" id="363"/>
</connections>
</menuItem>
<menuItem title="Revert to Saved" id="112" userLabel="10">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="revertDocumentToSaved:" target="-1" id="364"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="74" userLabel="2">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Page Setup..." keyEquivalent="P" id="77" userLabel="5">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="runPageLayout:" target="-1" id="87"/>
</connections>
</menuItem>
<menuItem title="Print…" keyEquivalent="p" id="78" userLabel="6">
<connections>
<action selector="print:" target="-1" id="86"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="217">
<menu key="submenu" title="Edit" id="205">
<items>
<menuItem title="Undo" keyEquivalent="z" id="207">
<connections>
<action selector="undo:" target="-1" id="223"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="215">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="redo:" target="-1" id="231"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="206">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Cut" keyEquivalent="x" id="199">
<connections>
<action selector="cut:" target="-1" id="228"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="197">
<connections>
<action selector="copy:" target="-1" id="224"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="203">
<connections>
<action selector="paste:" target="-1" id="226"/>
</connections>
</menuItem>
<menuItem title="Delete" id="202">
<connections>
<action selector="delete:" target="-1" id="235"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="198">
<connections>
<action selector="selectAll:" target="-1" id="232"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="214">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Find" id="218">
<menu key="submenu" title="Find" id="220">
<items>
<menuItem title="Find…" tag="37000" keyEquivalent="f" id="209">
<connections>
<action selector="commandDispatch:" target="-1" id="241"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="208"/>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221"/>
<menuItem title="Jump to Selection" keyEquivalent="j" id="210">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="245"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="216">
<menu key="submenu" title="Spelling and Grammar" id="200">
<items>
<menuItem title="Show Spelling…" keyEquivalent=":" id="204">
<connections>
<action selector="showGuessPanel:" target="-1" id="230"/>
</connections>
</menuItem>
<menuItem title="Check Spelling" keyEquivalent=";" id="201">
<connections>
<action selector="checkSpelling:" target="-1" id="225"/>
</connections>
</menuItem>
<menuItem title="Check Spelling While Typing" id="219">
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="222"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="346">
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="347"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="348">
<menu key="submenu" title="Substitutions" id="349">
<items>
<menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350">
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="355"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351">
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="356"/>
</connections>
</menuItem>
<menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="357"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="211">
<menu key="submenu" title="Speech" id="212">
<items>
<menuItem title="Start Speaking" id="196">
<connections>
<action selector="startSpeaking:" target="-1" id="233"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="195">
<connections>
<action selector="stopSpeaking:" target="-1" id="227"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Format" id="299">
<menu key="submenu" title="Format" id="300">
<items>
<menuItem title="Show Fonts" keyEquivalent="t" id="344"/>
<menuItem title="Show Colors" keyEquivalent="C" id="345">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="orderFrontColorPanel:" target="-1" id="361"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="295">
<menu key="submenu" title="View" id="296">
<items>
<menuItem title="Show Toolbar" keyEquivalent="t" id="297">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="-1" id="366"/>
</connections>
</menuItem>
<menuItem title="Customize Toolbar…" id="298">
<connections>
<action selector="runToolbarCustomizationPalette:" target="-1" id="365"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="19">
<menu key="submenu" title="Window" systemMenu="window" id="24">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="23">
<connections>
<action selector="performMiniaturize:" target="-1" id="37"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="239">
<connections>
<action selector="performZoom:" target="-1" id="240"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="92">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Bring All to Front" id="5">
<connections>
<action selector="arrangeInFront:" target="-1" id="39"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="103" userLabel="1">
<menu key="submenu" title="Help" id="106" userLabel="2">
<items>
<menuItem title="cefsimple Help" keyEquivalent="?" id="111">
<connections>
<action selector="showHelp:" target="-1" id="360"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<userDefaultsController representsSharedInstance="YES" id="389"/>
</objects>
</document>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,4 @@
#[cfg(all(target_os = "windows", feature = "sandbox"))]
pub mod shared;
#[cfg(all(target_os = "windows", feature = "sandbox"))]
mod win;

View File

@@ -1,182 +0,0 @@
#[cfg(target_os = "macos")]
mod mac {
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "CFBundleDisplayName")]
cf_bundle_display_name: String,
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundlePackageType")]
cf_bundle_package_type: String,
#[serde(rename = "CFBundleSignature")]
cf_bundle_signature: String,
#[serde(rename = "CFBundleVersion")]
cf_bundle_version: String,
#[serde(rename = "CFBundleShortVersionString")]
cf_bundle_short_version_string: String,
#[serde(rename = "LSEnvironment")]
ls_environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
ls_file_quarantine_enabled: bool,
#[serde(rename = "LSMinimumSystemVersion")]
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
ls_ui_element: Option<String>,
#[serde(rename = "NSBluetoothAlwaysUsageDescription")]
ns_bluetooth_always_usage_description: String,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
#[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")]
ns_web_browser_publickey_credential_usage_description: String,
#[serde(rename = "NSCameraUsageDescription")]
ns_camera_usage_description: String,
#[serde(rename = "NSMicrophoneUsageDescription")]
ns_microphone_usage_description: String,
}
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const HELPERS: &[&str] = &[
"cefsimple Helper (GPU)",
"cefsimple Helper (Renderer)",
"cefsimple Helper (Plugin)",
"cefsimple Helper (Alerts)",
"cefsimple Helper",
];
fn create_app_layout(app_path: &Path) -> PathBuf {
[EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH]
.iter()
.for_each(|p| fs::create_dir_all(app_path.join(p)).unwrap());
app_path.join("Contents")
}
fn create_app(app_path: &Path, exec_name: &str, bin: &Path, is_helper: bool) -> PathBuf {
let app_path = app_path.join(exec_name).with_extension("app");
let contents_path = create_app_layout(&app_path);
create_info_plist(&contents_path, exec_name, is_helper).unwrap();
fs::copy(bin, app_path.join(EXEC_PATH).join(exec_name)).unwrap();
app_path
}
// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos
fn bundle(app_path: &Path) {
let example_path = PathBuf::from(app_path);
let main_app_path = create_app(
app_path,
"cefsimple",
&example_path.join("cefsimple"),
false,
);
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK);
if to.exists() {
fs::remove_dir_all(&to).unwrap();
}
copy_directory(&cef_path.join(FRAMEWORK), &to);
HELPERS.iter().for_each(|helper| {
create_app(
&main_app_path.join(FRAMEWORKS_PATH),
helper,
&example_path.join("cefsimple_helper"),
true,
);
});
}
fn create_info_plist(
contents_path: &Path,
exec_name: &str,
is_helper: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let info_plist = InfoPlist {
cf_bundle_development_region: "en".to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_identifier: "org.cef-rs.cefsimple.helper".to_string(),
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_name: "cef-rs".to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
cf_bundle_version: "1.0.0".to_string(),
cf_bundle_short_version_string: "1.0".to_string(),
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())]
.iter()
.cloned()
.collect(),
ls_file_quarantine_enabled: true,
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper {
Some("1".to_string())
} else {
None
},
ns_bluetooth_always_usage_description: exec_name.to_string(),
ns_supports_automatic_graphics_switching: true,
ns_web_browser_publickey_credential_usage_description: exec_name.to_string(),
ns_camera_usage_description: exec_name.to_string(),
ns_microphone_usage_description: exec_name.to_string(),
};
plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?;
Ok(())
}
fn copy_directory(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_directory(&entry.path(), &dst_path);
} else {
fs::copy(entry.path(), &dst_path).unwrap();
}
}
}
fn run_command(args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new("cargo")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
std::process::exit(1);
}
Ok(())
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug");
run_command(&["build", "--bin", "cefsimple"])?;
run_command(&["build", "--bin", "cefsimple_helper"])?;
bundle(&app_path);
Ok(())
}
}
#[cfg(target_os = "macos")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
mac::main()
}
#[cfg(not(target_os = "macos"))]
fn main() {}

View File

@@ -0,0 +1,243 @@
use crate::shared::simple_handler::*;
use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol};
use objc2::{
ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods,
msg_send,
rc::Retained,
runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, ProtocolObject},
sel,
};
use objc2_app_kit::{
NSApp, NSApplication, NSApplicationDelegate, NSApplicationTerminateReply, NSEvent,
NSUserInterfaceValidations, NSValidatedUserInterfaceItem,
};
use objc2_foundation::{NSBundle, NSObjectNSThreadPerformAdditions, ns_string};
use std::{cell::Cell, ptr};
define_class! {
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
pub struct SimpleAppDelegate;
impl SimpleAppDelegate {
/// Create the application on the UI thread.
#[unsafe(method(createApplication:))]
unsafe fn create_application(&self, _object: Option<&AnyObject>) {
let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread"));
assert!(app.isKindOfClass(SimpleApplication::class()));
assert!(
app.delegate()
.unwrap()
.isKindOfClass(SimpleAppDelegate::class())
);
let main_bundle = NSBundle::mainBundle();
let _: Bool = msg_send![&main_bundle,
loadNibNamed: ns_string!("MainMenu"),
owner: &*app,
topLevelObjects: ptr::null_mut::<*const AnyObject>()
];
}
}
unsafe impl NSObjectProtocol for SimpleAppDelegate {}
unsafe impl NSApplicationDelegate for SimpleAppDelegate {
#[unsafe(method(applicationShouldTerminate:))]
unsafe fn application_should_terminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply {
NSApplicationTerminateReply::TerminateNow
}
/// Called when the user clicks the app dock icon while the application is
/// already running.
#[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))]
unsafe fn application_should_handle_reopen(&self, _sender: &NSApplication, _has_visible_windows: Bool) -> Bool {
if let Some(handler) = SimpleHandler::instance() {
let mut handler = handler.lock().expect("Failed to lock SimpleHandler");
if !handler.is_closing() {
handler.show_main_window();
}
}
Bool::NO
}
/// Requests that any state restoration archive be created with secure encoding
/// (macOS 12+ only). See https://crrev.com/c737387656 for details. This also
/// fixes an issue with macOS default behavior incorrectly restoring windows
/// after hard reset (holding down the power button).
#[unsafe(method(applicationSupportsSecureRestorableState:))]
unsafe fn application_supports_secure_restorable_state(&self, _sender: &NSApplication) -> Bool {
Bool::YES
}
}
unsafe impl NSUserInterfaceValidations for SimpleAppDelegate {
#[unsafe(method(validateUserInterfaceItem:))]
unsafe fn validate_user_interface_item(&self, item: &ProtocolObject<dyn NSValidatedUserInterfaceItem>) -> Bool {
const IDC_FIND: isize = 37000;
let tag = item.tag();
if tag == IDC_FIND {
Bool::YES
} else {
Bool::NO
}
}
}
}
impl SimpleAppDelegate {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = SimpleAppDelegate::alloc(mtm).set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
/// Instance variables of `SimpleApplication`.
#[derive(Default)]
pub struct SimpleApplicationIvars {
handling_send_event: Cell<Bool>,
}
define_class!(
/// A `NSApplication` subclass that implements the required CEF protocols.
///
/// This class provides the necessary `CefAppProtocol` conformance to
/// ensure that events are handled correctly by the Chromium framework on macOS.
#[unsafe(super(NSApplication))]
#[ivars = SimpleApplicationIvars]
pub struct SimpleApplication;
impl SimpleApplication {
#[unsafe(method(sendEvent:))]
unsafe fn send_event(&self, event: &NSEvent) {
let was_sending_event = self.is_handling_send_event();
if !was_sending_event {
self.set_handling_send_event(true);
}
let _: () = msg_send![super(self), sendEvent:event];
if !was_sending_event {
self.set_handling_send_event(false);
}
}
/// |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This
/// includes the application menu's quit menu item and keyboard equivalent, the
/// application's dock icon menu's quit menu item, "quit" (not "force quit") in
/// the Activity Monitor, and quits triggered by user logout and system restart
/// and shutdown.
///
/// The default |-terminate:| implementation ends the process by calling exit(),
/// and thus never leaves the main run loop. This is unsuitable for Chromium
/// since Chromium depends on leaving the main run loop to perform an orderly
/// shutdown. We support the normal |-terminate:| interface by overriding the
/// default implementation. Our implementation, which is very specific to the
/// needs of Chromium, works by asking the application delegate to terminate
/// using its |-tryToTerminateApplication:| method.
///
/// |-tryToTerminateApplication:| differs from the standard
/// |-applicationShouldTerminate:| in that no special event loop is run in the
/// case that immediate termination is not possible (e.g., if dialog boxes
/// allowing the user to cancel have to be shown). Instead, this method tries to
/// close all browsers by calling CloseBrowser(false) via
/// ClientHandler::CloseAllBrowsers. Calling CloseBrowser will result in a call
/// to ClientHandler::DoClose and execution of |-performClose:| on the NSWindow.
/// DoClose sets a flag that is used to differentiate between new close events
/// (e.g., user clicked the window close button) and in-progress close events
/// (e.g., user approved the close window dialog). The NSWindowDelegate
/// |-windowShouldClose:| method checks this flag and either calls
/// CloseBrowser(false) in the case of a new close event or destructs the
/// NSWindow in the case of an in-progress close event.
/// ClientHandler::OnBeforeClose will be called after the CEF NSView hosted in
/// the NSWindow is dealloc'ed.
///
/// After the final browser window has closed ClientHandler::OnBeforeClose will
/// begin actual tear-down of the application by calling CefQuitMessageLoop.
/// This ends the NSApplication event loop and execution then returns to the
/// main() function for cleanup before application termination.
///
/// The standard |-applicationShouldTerminate:| is not supported, and code paths
/// leading to it must be redirected.
#[unsafe(method(terminate:))]
unsafe fn terminate(&self, _sender: &AnyObject) {
if let Some(handler) = SimpleHandler::instance() {
let mut handler = handler.lock().expect("Failed to lock SimpleHandler");
if !handler.is_closing() {
handler.close_all_browsers(false);
}
}
}
}
unsafe impl CrAppControlProtocol for SimpleApplication {
#[unsafe(method(setHandlingSendEvent:))]
unsafe fn _set_handling_send_event(&self, handling_send_event: Bool) {
self.ivars().handling_send_event.set(handling_send_event);
}
}
unsafe impl CrAppProtocol for SimpleApplication {
#[unsafe(method(isHandlingSendEvent))]
unsafe fn _is_handling_send_event(&self) -> Bool {
self.ivars().handling_send_event.get()
}
}
unsafe impl CefAppProtocol for SimpleApplication {}
);
impl SimpleApplication {
extern_methods! {
#[unsafe(method(sharedApplication))]
fn shared_application() -> Retained<Self>;
#[unsafe(method(setHandlingSendEvent:))]
fn set_handling_send_event(&self, handling_send_event: bool);
#[unsafe(method(isHandlingSendEvent))]
fn is_handling_send_event(&self) -> bool;
}
}
pub fn setup_simple_application() {
// Initialize the SimpleApplication instance.
// SAFETY: mtm ensures that here is the main thread.
let _ = SimpleApplication::shared_application();
// If there was an invocation to NSApp prior to here,
// then the NSApp will not be a SimpleApplication.
// The following assertion ensures that this doesn't happen.
assert!(
NSApp(MainThreadMarker::new().expect("Not running on the main thread"))
.isKindOfClass(SimpleApplication::class())
);
}
pub fn setup_simple_app_delegate() -> Retained<SimpleAppDelegate> {
let mtm = MainThreadMarker::new().expect("Not running on the main thread");
// Create the application delegate.
let simple_delegate = SimpleAppDelegate::new(mtm);
let delegate_proto =
ProtocolObject::<dyn NSApplicationDelegate>::from_retained(simple_delegate.clone());
let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread"));
assert!(app.isKindOfClass(SimpleApplication::class()));
app.setDelegate(Some(&delegate_proto));
assert!(
app.delegate()
.unwrap()
.isKindOfClass(SimpleAppDelegate::class())
);
unsafe {
simple_delegate.performSelectorOnMainThread_withObject_waitUntilDone(
sel!(createApplication:),
None,
false,
);
}
simple_delegate
}

View File

@@ -1,230 +1,27 @@
use cef::{args::Args, rc::*, *};
use std::sync::{Arc, Mutex};
#![cfg_attr(
all(not(debug_assertions), not(feature = "sandbox"), target_os = "windows"),
windows_subsystem = "windows"
)]
wrap_app! {
struct DemoApp {
window: Arc<Mutex<Option<Window>>>,
}
impl App {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(DemoBrowserProcessHandler::new(
self.window.clone(),
))
}
}
}
wrap_browser_process_handler! {
struct DemoBrowserProcessHandler {
window: Arc<Mutex<Option<Window>>>,
}
impl BrowserProcessHandler {
// The real lifespan of cef starts from `on_context_initialized`, so all the cef objects should be manipulated after that.
fn on_context_initialized(&self) {
println!("cef context intiialized");
let mut client = DemoClient::new();
let url = CefString::from("https://www.google.com");
let browser_view = browser_view_create(
Some(&mut client),
Some(&url),
Some(&Default::default()),
Option::<&mut DictionaryValue>::None,
Option::<&mut RequestContext>::None,
Option::<&mut BrowserViewDelegate>::None,
)
.expect("Failed to create browser view");
let mut delegate = DemoWindowDelegate::new(browser_view);
if let Ok(mut window) = self.window.lock() {
*window = Some(
window_create_top_level(Some(&mut delegate)).expect("Failed to create window"),
);
}
}
}
}
wrap_client! {
struct DemoClient;
impl Client {}
}
wrap_window_delegate! {
struct DemoWindowDelegate {
browser_view: BrowserView,
}
impl ViewDelegate {
fn on_child_view_changed(
&self,
_view: Option<&mut View>,
_added: ::std::os::raw::c_int,
_child: Option<&mut View>,
) {
// view.as_panel().map(|x| x.as_window().map(|w| w.close()));
}
}
impl PanelDelegate {}
impl WindowDelegate {
fn on_window_created(&self, window: Option<&mut Window>) {
if let Some(window) = window {
let view = self.browser_view.clone();
window.add_child_view(Some(&mut (&view).into()));
window.show();
}
}
fn on_window_destroyed(&self, _window: Option<&mut Window>) {
quit_message_loop();
}
fn with_standard_window_buttons(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int {
1
}
fn can_resize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int {
1
}
fn can_maximize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int {
1
}
fn can_minimize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int {
1
}
fn can_close(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int {
1
}
}
}
// FIXME: Rewrite this demo based on cef/tests/cefsimple
fn main() {
#[cfg(target_os = "macos")]
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
assert!(loader.load());
loader
};
pub mod shared;
#[cfg(target_os = "macos")]
{
use objc2::{
ClassType, MainThreadMarker, msg_send,
rc::Retained,
runtime::{AnyObject, NSObjectProtocol},
mod mac;
#[cfg(not(all(feature = "sandbox", target_os = "windows")))]
fn main() -> Result<(), &'static str> {
let _library = shared::load_cef();
let args = cef::args::Args::new();
let Some(cmd_line) = args.as_cmd_line() else {
return Err("Failed to parse command line arguments");
};
use objc2_app_kit::NSApp;
use application::SimpleApplication;
let mtm = MainThreadMarker::new().unwrap();
unsafe {
// Initialize the SimpleApplication instance.
// SAFETY: mtm ensures that here is the main thread.
let _: Retained<AnyObject> = msg_send![SimpleApplication::class(), sharedApplication];
shared::run_main(args.as_main_args(), &cmd_line, std::ptr::null_mut());
Ok(())
}
// If there was an invocation to NSApp prior to here,
// then the NSApp will not be a SimpleApplication.
// The following assertion ensures that this doesn't happen.
assert!(NSApp(mtm).isKindOfClass(SimpleApplication::class()));
}
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = Args::new();
let cmd = args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
let window = Arc::new(Mutex::new(None));
let mut app = DemoApp::new(window.clone());
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
if is_browser_process {
println!("launch browser process");
assert!(ret == -1, "cannot execute browser process");
} else {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
println!("launch process {process_type}");
assert!(ret >= 0, "cannot execute non-browser process");
// non-browser process does not initialize cef
return;
}
let settings = Settings {
no_sandbox: !cfg!(feature = "sandbox") as _,
..Default::default()
};
assert_eq!(
initialize(
Some(args.as_main_args()),
Some(&settings),
Some(&mut app),
std::ptr::null_mut(),
),
1
);
run_message_loop();
let window = window.lock().expect("Failed to lock window");
let window = window.as_ref().expect("Window is None");
assert!(window.has_one_ref());
shutdown();
}
#[cfg(target_os = "macos")]
mod application {
use std::cell::Cell;
use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol};
use objc2::{DefinedClass, define_class, runtime::Bool};
use objc2_app_kit::NSApplication;
/// Instance variables of `SimpleApplication`.
pub struct SimpleApplicationIvars {
handling_send_event: Cell<Bool>,
}
define_class!(
/// A `NSApplication` subclass that implements the required CEF protocols.
///
/// This class provides the necessary `CefAppProtocol` conformance to
/// ensure that events are handled correctly by the Chromium framework on macOS.
#[unsafe(super(NSApplication))]
#[ivars = SimpleApplicationIvars]
pub struct SimpleApplication;
unsafe impl CrAppControlProtocol for SimpleApplication {
#[unsafe(method(setHandlingSendEvent:))]
unsafe fn set_handling_send_event(&self, handling_send_event: Bool) {
self.ivars().handling_send_event.set(handling_send_event);
}
}
unsafe impl CrAppProtocol for SimpleApplication {
#[unsafe(method(isHandlingSendEvent))]
unsafe fn is_handling_send_event(&self) -> Bool {
self.ivars().handling_send_event.get()
}
}
unsafe impl CefAppProtocol for SimpleApplication {}
);
#[cfg(all(feature = "sandbox", target_os = "windows"))]
fn main() -> Result<(), &'static str> {
Err("Running in sandbox mode on Windows requires bootstrap.exe or bootstrapc.exe.")
}

View File

@@ -0,0 +1,75 @@
//! Rust port of the [`cefsimple`](https://github.com/chromiumembedded/cef/tree/master/tests/cefsimple) example.
use cef::*;
pub mod resources;
pub mod simple_app;
pub mod simple_handler;
#[cfg(target_os = "macos")]
pub type Library = library_loader::LibraryLoader;
#[cfg(not(target_os = "macos"))]
pub type Library = ();
#[allow(dead_code)]
pub fn load_cef() -> Library {
#[cfg(target_os = "macos")]
let library = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
assert!(loader.load());
loader
};
#[cfg(not(target_os = "macos"))]
let library = ();
// Initialize the CEF API version.
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
#[cfg(target_os = "macos")]
crate::mac::setup_simple_application();
library
}
#[allow(dead_code)]
pub fn run_main(main_args: &MainArgs, cmd_line: &CommandLine, sandbox_info: *mut u8) {
let switch = CefString::from("type");
let is_browser_process = cmd_line.has_switch(Some(&switch)) != 1;
let ret = execute_process(Some(main_args), None, sandbox_info);
if is_browser_process {
println!("launch browser process");
assert_eq!(ret, -1, "cannot execute browser process");
} else {
let process_type = CefString::from(&cmd_line.switch_value(Some(&switch)));
println!("launch process {process_type}");
assert!(ret >= 0, "cannot execute non-browser process");
// non-browser process does not initialize cef
return;
}
let mut app = simple_app::SimpleApp::new();
let settings = Settings {
no_sandbox: !cfg!(feature = "sandbox") as _,
..Default::default()
};
assert_eq!(
initialize(
Some(main_args),
Some(&settings),
Some(&mut app),
sandbox_info,
),
1
);
#[cfg(target_os = "macos")]
let _delegate = crate::mac::setup_simple_app_delegate();
run_message_loop();
shutdown();
}

View File

@@ -0,0 +1,2 @@
pub const IDI_CEFSIMPLE: u16 = 120;
pub const IDI_SMALL: u16 = 121;

View File

@@ -0,0 +1,215 @@
use cef::*;
use std::cell::RefCell;
use super::simple_handler::*;
wrap_window_delegate! {
struct SimpleWindowDelegate {
browser_view: RefCell<Option<BrowserView>>,
runtime_style: RuntimeStyle,
initial_show_state: ShowState,
}
impl ViewDelegate {
fn preferred_size(&self, _view: Option<&mut View>) -> Size {
Size {
width: 800,
height: 600,
}
}
}
impl PanelDelegate {}
impl WindowDelegate {
fn on_window_created(&self, window: Option<&mut Window>) {
// Add the browser view and show the window.
let browser_view = self.browser_view.borrow();
let (Some(window), Some(browser_view)) = (window, browser_view.as_ref()) else {
return;
};
let mut view = View::from(browser_view);
window.add_child_view(Some(&mut view));
if self.initial_show_state != ShowState::HIDDEN {
window.show();
}
}
fn on_window_destroyed(&self, _window: Option<&mut Window>) {
let mut browser_view = self.browser_view.borrow_mut();
*browser_view = None;
}
fn can_close(&self, _window: Option<&mut Window>) -> i32 {
// Allow the window to close if the browser says it's OK.
let browser_view = self.browser_view.borrow();
let browser_view = browser_view.as_ref().expect("BrowserView is None");
if let Some(browser) = browser_view.browser() {
let browser_host = browser.host().expect("BrowserHost is None");
browser_host.try_close_browser()
} else {
1
}
}
fn initial_show_state(&self, _window: Option<&mut Window>) -> ShowState {
self.initial_show_state
}
fn window_runtime_style(&self) -> RuntimeStyle {
self.runtime_style
}
}
}
wrap_browser_view_delegate! {
struct SimpleBrowserViewDelegate {
runtime_style: RuntimeStyle,
}
impl ViewDelegate {}
impl BrowserViewDelegate {
fn on_popup_browser_view_created(
&self,
_browser_view: Option<&mut BrowserView>,
popup_browser_view: Option<&mut BrowserView>,
_is_devtools: i32,
) -> i32 {
// Create a new top-level Window for the popup. It will show itself after
// creation.
let mut window_delegate = SimpleWindowDelegate::new(
RefCell::new(popup_browser_view.cloned()),
self.runtime_style,
ShowState::NORMAL,
);
window_create_top_level(Some(&mut window_delegate));
// We created the Window.
1
}
fn browser_runtime_style(&self) -> RuntimeStyle {
self.runtime_style
}
}
}
wrap_app! {
pub struct SimpleApp;
impl App {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(SimpleBrowserProcessHandler::new(RefCell::new(None)))
}
}
}
wrap_browser_process_handler! {
struct SimpleBrowserProcessHandler {
client: RefCell<Option<Client>>,
}
impl BrowserProcessHandler {
fn on_context_initialized(&self) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
// Check if Alloy style will be used.
let command_line = command_line_get_global().expect("Failed to get command line");
let use_alloy_style =
command_line.has_switch(Some(&CefString::from("use-alloy-style"))) != 0;
let runtime_style = if use_alloy_style {
RuntimeStyle::ALLOY
} else {
RuntimeStyle::DEFAULT
};
{
// SimpleHandler implements browser-level callbacks.
let mut client = self.client.borrow_mut();
*client = Some(SimpleHandlerClient::new(SimpleHandler::new(
use_alloy_style,
)));
}
// Specify CEF browser settings here.
let settings = BrowserSettings::default();
// Check if a "--url=" value was provided via the command-line. If so, use
// that instead of the default URL.
let url = CefString::from(&command_line.switch_value(Some(&CefString::from("url"))))
.to_string();
let url = if url.is_empty() {
"https://www.google.com/"
} else {
url.as_str()
};
let url = CefString::from(url);
// Views is enabled by default (add `--use-native` to disable).
let use_views = command_line.has_switch(Some(&CefString::from("use-native"))) != 0;
// If using Views create the browser using the Views framework, otherwise
// create the browser using the native platform framework.
if use_views {
// Create the BrowserView.
let mut client = self.default_client();
let mut delegate = SimpleBrowserViewDelegate::new(runtime_style);
let browser_view = browser_view_create(
client.as_mut(),
Some(&url),
Some(&settings),
None,
None,
Some(&mut delegate),
);
// Optionally configure the initial show state.
let initial_show_state = CefString::from(
&command_line.switch_value(Some(&CefString::from("initial-show-state"))),
)
.to_string();
let initial_show_state = match initial_show_state.as_str() {
"minimized" => ShowState::MINIMIZED,
"maximized" => ShowState::MAXIMIZED,
// Hidden show state is only supported on MacOS.
#[cfg(target_os = "macos")]
"hidden" => ShowState::HIDDEN,
_ => ShowState::NORMAL,
};
// Create the Window. It will show itself after creation.
let mut delegate = SimpleWindowDelegate::new(
RefCell::new(browser_view),
runtime_style,
initial_show_state,
);
window_create_top_level(Some(&mut delegate));
} else {
// Information used when creating the native window.
let window_info = WindowInfo {
runtime_style,
..Default::default()
};
#[cfg(target_os = "windows")]
let window_info = window_info.set_as_popup(Default::default(), "cefsimple");
let mut client = self.default_client();
browser_host_create_browser(
Some(&window_info),
client.as_mut(),
Some(&url),
Some(&settings),
None,
None,
);
}
}
fn default_client(&self) -> Option<Client> {
self.client.borrow().clone()
}
}
}

View File

@@ -0,0 +1,76 @@
use cef::*;
fn window_from_browser(browser: Option<&mut Browser>) -> Option<WindowHandle> {
let window = browser?.host()?.window_handle();
if window == 0 { None } else { Some(window) }
}
pub fn platform_title_change(browser: Option<&mut Browser>, _title: Option<&CefString>) {
// Retrieve the X11 display shared with Chromium.
let display = get_xdisplay();
if display.is_null() {
return;
}
// Retrieve the X11 window handle for the browser.
let Some(_window) = window_from_browser(browser) else {
return;
};
#[cfg(feature = "linux-x11")]
unsafe {
use std::ffi::{CString, c_char};
use x11_dl::xlib::*;
// Load the Xlib library dynamically.
let Ok(xlib) = Xlib::open() else {
return;
};
// Retrieve the atoms required by the below XChangeProperty call.
let Ok(names) = ["_NET_WM_NAME", "UTF8_STRING"]
.into_iter()
.map(CString::new)
.collect::<Result<Vec<_>, _>>()
else {
return;
};
let mut names: Vec<_> = names
.iter()
.map(|name| name.as_ptr() as *mut c_char)
.collect();
let mut atoms = [0; 2];
let result = (xlib.XInternAtoms)(
display as *mut _,
names.as_mut_ptr(),
2,
0,
atoms.as_mut_ptr(),
);
if result == 0 {
return;
}
// Set the window title.
let Ok(title) = CString::new(_title.map(CefString::to_string).unwrap_or_default()) else {
return;
};
let title = title.as_c_str();
(xlib.XChangeProperty)(
display as *mut _,
_window,
atoms[0],
atoms[1],
8,
PropModeReplace,
title.as_ptr() as *const _,
title.count_bytes() as i32,
);
// TODO(erg): This is technically wrong. So XStoreName and friends expect
// this in Host Portable Character Encoding instead of UTF-8, which I believe
// is Compound Text. This shouldn't matter 90% of the time since this is the
// fallback to the UTF8 property above.
(xlib.XStoreName)(display as *mut _, _window, title.as_ptr());
}
}

View File

@@ -0,0 +1,29 @@
use cef::*;
use objc2::{Message, rc::Retained};
use objc2_app_kit::{NSView, NSWindow};
use objc2_foundation::NSString;
fn window_from_browser(browser: Option<&mut Browser>) -> Option<Retained<NSWindow>> {
let view_ptr = browser?.host()?.window_handle().cast::<NSView>();
let view = unsafe { view_ptr.as_ref()? };
let view = view.retain();
view.window()
}
pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) {
let Some(window) = window_from_browser(browser) else {
return;
};
let title = title.map(CefString::to_string).unwrap_or_default();
let title = NSString::from_str(&title);
window.setTitle(&title);
}
pub fn platform_show_window(browser: Option<&mut Browser>) {
let Some(window) = window_from_browser(browser) else {
return;
};
window.makeKeyAndOrderFront(Some(&window));
}

View File

@@ -0,0 +1,326 @@
use cef::*;
use std::sync::{Arc, Mutex, OnceLock, Weak};
fn get_data_uri(data: &[u8], mime_type: &str) -> String {
let data = CefString::from(&base64_encode(Some(data)));
let uri = CefString::from(&uriencode(Some(&data), 0)).to_string();
format!("data:{mime_type};base64,{uri}")
}
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "macos")]
use mac::*;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
use win::*;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux::*;
#[cfg(not(target_os = "macos"))]
fn platform_show_window(_browser: Option<&mut Browser>) {
todo!("Implement platform_show_window for non-macOS platforms");
}
static SIMPLE_HANDLER_INSTANCE: OnceLock<Weak<Mutex<SimpleHandler>>> = OnceLock::new();
pub struct SimpleHandler {
is_alloy_style: bool,
browser_list: Vec<Browser>,
is_closing: bool,
weak_self: Weak<Mutex<Self>>,
}
impl SimpleHandler {
pub fn instance() -> Option<Arc<Mutex<Self>>> {
SIMPLE_HANDLER_INSTANCE
.get()
.and_then(|weak| weak.upgrade())
}
pub fn new(is_alloy_style: bool) -> Arc<Mutex<Self>> {
Arc::new_cyclic(|weak| {
if let Err(instance) = SIMPLE_HANDLER_INSTANCE.set(weak.clone()) {
assert_eq!(instance.strong_count(), 0, "Replacing a viable instance");
}
Mutex::new(Self {
is_alloy_style,
browser_list: Vec::new(),
is_closing: false,
weak_self: weak.clone(),
})
})
}
fn on_title_change(&mut self, browser: Option<&mut Browser>, title: Option<&CefString>) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
let mut browser = browser.cloned();
if let Some(browser_view) = browser_view_get_for_browser(browser.as_mut()) {
if let Some(window) = browser_view.window() {
window.set_title(title);
}
} else if self.is_alloy_style {
platform_title_change(browser.as_mut(), title);
}
}
fn on_after_created(&mut self, browser: Option<&mut Browser>) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
let browser = browser.cloned().expect("Browser is None");
// Sanity-check the configured runtime style.
assert_eq!(
browser.host().expect("BrowserHost is None").runtime_style(),
if self.is_alloy_style {
RuntimeStyle::ALLOY
} else {
RuntimeStyle::CHROME
}
);
// Add to the list of existing browsers.
self.browser_list.push(browser);
}
fn do_close(&mut self, _browser: Option<&mut Browser>) -> bool {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
// Closing the main window requires special handling. See the DoClose()
// documentation in the CEF header for a detailed destription of this
// process.
if self.browser_list.len() == 1 {
// Set a flag to indicate that the window close should be allowed.
self.is_closing = true;
}
// Allow the close. For windowed browsers this will result in the OS close
// event being sent.
false
}
fn on_before_close(&mut self, browser: Option<&mut Browser>) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
// Remove from the list of existing browsers.
let mut browser = browser.cloned().expect("Browser is None");
if let Some(index) = self
.browser_list
.iter()
.position(move |elem| elem.is_same(Some(&mut browser)) != 0)
{
self.browser_list.remove(index);
}
if self.browser_list.is_empty() {
// All browser windows have closed. Quit the application message loop.
quit_message_loop();
}
}
fn on_load_error(
&mut self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
error_code: Errorcode,
error_text: Option<&CefString>,
failed_url: Option<&CefString>,
) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
// Allow Chrome to show the error page.
if !self.is_alloy_style {
return;
}
// Don't display an error for downloaded files.
let error_code = sys::cef_errorcode_t::from(error_code);
if error_code == sys::cef_errorcode_t::ERR_ABORTED {
return;
}
let error_code = error_code as i32;
let frame = frame.expect("Frame is None");
// Display a load error message using a data: URI.
let error_text = error_text.map(CefString::to_string).unwrap_or_default();
let failed_url = failed_url.map(CefString::to_string).unwrap_or_default();
let data = format!(
r#"
<html>
<body bgcolor="white">
<h2>Failed to load URL {failed_url} with error {error_text} ({error_code}).</h2>
</body>
</html>
"#
);
let uri = get_data_uri(data.as_bytes(), "text/html");
let uri = CefString::from(uri.as_str());
frame.load_url(Some(&uri));
}
pub fn show_main_window(&mut self) {
let thread_id = ThreadId::UI;
if currently_on(thread_id) == 0 {
// Execute on the UI thread.
let this = self
.weak_self
.upgrade()
.expect("Weak reference to SimpleHandler is None");
let mut task = ShowMainWindow::new(this);
post_task(thread_id, Some(&mut task));
return;
}
let Some(mut main_browser) = self.browser_list.first().cloned() else {
return;
};
if let Some(browser_view) = browser_view_get_for_browser(Some(&mut main_browser)) {
// Show the window using the Views framework.
if let Some(window) = browser_view.window() {
window.show();
}
} else if self.is_alloy_style {
platform_show_window(Some(&mut main_browser));
}
}
pub fn close_all_browsers(&mut self, force_close: bool) {
let thread_id = ThreadId::UI;
if currently_on(thread_id) == 0 {
// Execute on the UI thread.
let this = self
.weak_self
.upgrade()
.expect("Weak reference to SimpleHandler is None");
let mut task = CloseAllBrowsers::new(this, force_close);
post_task(thread_id, Some(&mut task));
return;
}
for browser in self.browser_list.iter() {
let browser_host = browser.host().expect("BrowserHost is None");
browser_host.close_browser(force_close.into());
}
}
pub fn is_closing(&self) -> bool {
self.is_closing
}
}
wrap_client! {
pub struct SimpleHandlerClient {
inner: Arc<Mutex<SimpleHandler>>,
}
impl Client {
fn display_handler(&self) -> Option<DisplayHandler> {
Some(SimpleHandlerDisplayHandler::new(self.inner.clone()))
}
fn life_span_handler(&self) -> Option<LifeSpanHandler> {
Some(SimpleHandlerLifeSpanHandler::new(self.inner.clone()))
}
fn load_handler(&self) -> Option<LoadHandler> {
Some(SimpleHandlerLoadHandler::new(self.inner.clone()))
}
}
}
wrap_display_handler! {
struct SimpleHandlerDisplayHandler {
inner: Arc<Mutex<SimpleHandler>>,
}
impl DisplayHandler {
fn on_title_change(&self, browser: Option<&mut Browser>, title: Option<&CefString>) {
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.on_title_change(browser, title);
}
}
}
wrap_life_span_handler! {
struct SimpleHandlerLifeSpanHandler {
inner: Arc<Mutex<SimpleHandler>>,
}
impl LifeSpanHandler {
fn on_after_created(&self, browser: Option<&mut Browser>) {
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.on_after_created(browser);
}
fn do_close(&self, browser: Option<&mut Browser>) -> i32 {
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.do_close(browser).into()
}
fn on_before_close(&self, browser: Option<&mut Browser>) {
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.on_before_close(browser);
}
}
}
wrap_load_handler! {
struct SimpleHandlerLoadHandler {
inner: Arc<Mutex<SimpleHandler>>,
}
impl LoadHandler {
fn on_load_error(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
error_code: Errorcode,
error_text: Option<&CefString>,
failed_url: Option<&CefString>,
) {
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.on_load_error(browser, frame, error_code, error_text, failed_url);
}
}
}
wrap_task! {
struct ShowMainWindow {
inner: Arc<Mutex<SimpleHandler>>,
}
impl Task {
fn execute(&self) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.show_main_window();
}
}
}
wrap_task! {
struct CloseAllBrowsers {
inner: Arc<Mutex<SimpleHandler>>,
force_close: bool,
}
impl Task {
fn execute(&self) {
debug_assert_ne!(currently_on(ThreadId::UI), 0);
let mut inner = self.inner.lock().expect("Failed to lock inner");
inner.close_all_browsers(self.force_close);
}
}
}

View File

@@ -0,0 +1,18 @@
use cef::*;
use std::iter;
use windows_sys::Win32::{Foundation::HWND, UI::WindowsAndMessaging::*};
fn window_from_browser(browser: Option<&mut Browser>) -> Option<HWND> {
let window = browser?.host()?.window_handle().0;
Some(window.cast())
}
pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) {
let Some(window) = window_from_browser(browser) else {
return;
};
let title = title.map(CefString::to_string).unwrap_or_default();
let title: Vec<_> = title.encode_utf16().chain(iter::once(0)).collect();
unsafe { SetWindowTextW(window, title.as_ptr()) };
}

View File

@@ -0,0 +1,21 @@
use crate::shared;
use cef::*;
#[unsafe(no_mangle)]
unsafe extern "C" fn RunWinMain(
instance: sys::HINSTANCE,
_command_line: *const u8,
_command_show: i32,
sandbox_info: *mut u8,
) -> i32 {
let _library = shared::load_cef();
let main_args = MainArgs { instance };
let args = args::Args::from(main_args);
let Some(cmd_line) = args.as_cmd_line() else {
return 1;
};
shared::run_main(args.as_main_args(), &cmd_line, sandbox_info);
0
}

View File

@@ -7,6 +7,9 @@ publish = false
default = ["accelerated_osr"]
accelerated_osr = ["cef/accelerated_osr"]
[package.metadata.cef.bundle]
helper_name = "cefsimple_helper"
[dependencies]
cef.workspace = true
wgpu.workspace = true

View File

@@ -33,8 +33,10 @@ wrap_app! {
command_line.append_switch(Some(&"hide-crash-restore-bubble".into()));
command_line.append_switch(Some(&"use-mock-keychain".into()));
command_line.append_switch(Some(&"enable-logging=stderr".into()));
command_line
.append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&"9229".into()));
command_line.append_switch_with_value(
Some(&"remote-debugging-port".into()),
Some(&"9229".into()),
);
}
fn browser_process_handler(&self) -> Option<cef::BrowserProcessHandler> {
@@ -223,7 +225,9 @@ wrap_render_handler! {
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
},
count: None,
},
@@ -348,7 +352,9 @@ wrap_render_handler! {
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
},
count: None,
},

View File

@@ -0,0 +1,34 @@
[package]
name = "tests_shared"
publish = false
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[features]
sandbox = ["cef/sandbox"]
[dependencies]
cef.workspace = true
[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
workspace = true
features = [
"Win32_System_Performance",
"Win32_System_SystemServices",
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
]
[target.'cfg(target_os = "macos")'.dependencies]
objc2.workspace = true
objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] }
objc2-foundation.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
glib.workspace = true

View File

@@ -0,0 +1,205 @@
<html>
<head><title>OSR Test</title></head>
<style>
.red_hover:hover {color:red;}
#li { width: 530px; }
body {background:rgba(255, 0, 0, 0.5); }
input {-webkit-appearance: none; }
#LI11select {width: 75px;}
#LI11select option { background-color: cyan; }
.dropdiv {
width:50px;
height:50px;
border:1px solid #aaaaaa;
float: left;
}
#dragdiv {
width: 30px;
height: 30px;
background-color: green;
margin: 10px;
}
#draghere {
position: relative;
z-index: -1;
top: 7px;
left: 7px;
opacity: 0.4;
}
#touchdiv, #pointerdiv {
width: 100px;
height: 50px;
background-color: red;
float: left;
margin-left: 10px;
}
</style>
<script>
function getElement(id) { return document.getElementById(id); }
function makeH1Red() { getElement('LI00').style.color='red'; }
function makeH1Black() { getElement('LI00').style.color='black'; }
function navigate() { location.href='?k='+getElement('editbox').value; }
function load() {
var elems = [];
var param = { type: 'ElementBounds', elems: elems };
elems.push(getElementBounds('LI00'));
elems.push(getElementBounds('LI01'));
elems.push(getElementBounds('LI02'));
elems.push(getElementBounds('LI03'));
elems.push(getElementBounds('LI04'));
elems.push(getElementBounds('LI05'));
elems.push(getElementBounds('LI06'));
elems.push(getElementBounds('LI07'));
elems.push(getElementBounds('LI08'));
elems.push(getElementBounds('LI09'));
elems.push(getElementBounds('LI10'));
elems.push(getElementBounds('LI11'));
elems.push(getElementBounds('LI11select'));
elems.push(getElementBounds('email'));
elems.push(getElementBounds('quickmenu'));
elems.push(getElementBounds('editbox'));
elems.push(getElementBounds('btnnavigate'));
elems.push(getElementBounds('dropdiv'));
elems.push(getElementBounds('dragdiv'));
elems.push(getElementBounds('touchdiv'));
elems.push(getElementBounds('pointerdiv'));
if (window.testQuery)
window.testQuery({request: JSON.stringify(param)});
fillDropDown();
}
function fillDropDown() {
var select = document.getElementById('LI11select');
for (var i = 1; i < 21; i++)
select.options.add(new Option('Option ' + i, i));
}
function getElementBounds(id) {
var element = document.getElementById(id);
var bounds = element.getBoundingClientRect();
return {
id: id,
x: Math.floor(bounds.x),
y: Math.floor(bounds.y),
width: Math.floor(bounds.width),
height: Math.floor(bounds.height)
};
}
function onEventTest(event) {
var param = 'osr' + event.type;
if (event.type == "click")
param += event.button;
// Results in a call to the OnQuery method in os_rendering_unittest.cc.
if (window.testQuery)
window.testQuery({request: param});
}
function onFocusTest(ev) {
if (window.testQuery)
window.testQuery({request: "osrfocus" + ev.target.id});
}
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev) {
ev.dataTransfer.setData("Text",ev.target.id);
}
function drop(ev) {
var data=ev.dataTransfer.getData("Text");
ev.target.innerHTML = '';
var dragged = document.getElementById(data);
dragged.setAttribute('draggable', 'false');
ev.target.appendChild(dragged);
if (window.testQuery)
window.testQuery({request: "osrdrop"});
}
function selectText(ev) {
var element = ev.target;
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
function onTouchEvent(ev) {
var param = 'osr' + ev.type;
// For Touch start also include touch points.
if (event.type == "touchstart")
param += ev.touches.length;
// For Touch Move include the touches that changed.
if (event.type == "touchmove")
param += ev.changedTouches.length;
// Results in a call to the OnQuery method in os_rendering_unittest.cc.
if (window.testQuery)
window.testQuery({request: param});
}
function onPointerEvent(ev) {
var param = 'osr' + ev.type + ' ' + ev.pointerType;
if (window.testQuery)
window.testQuery({request: param});
}
</script>
<body onfocus='onEventTest(event)' onblur='onEventTest(event)' onload='load();'>
<h1 id='LI00' onclick="onEventTest(event)">
OSR Testing h1 - Focus and blur
<select id='LI11select'>
<option value='0'>Default</option>
</select>
this page and will get this red black
</h1>
<ol>
<li id='LI01'>OnPaint should be called each time a page loads</li>
<li id='LI02' style='cursor:pointer;'><span>Move mouse
to require an OnCursorChange call</span></li>
<li id='LI03' onmousemove="onEventTest(event)"><span>Hover will color this with
red. Will trigger OnPaint once on enter and once on leave</span></li>
<li id='LI04'>Right clicking will show contextual menu and will request
GetScreenPoint</li>
<li id='LI05'>IsWindowRenderingDisabled should be true</li>
<li id='LI06'>WasResized should trigger full repaint if size changes.
</li>
<li id='LI07'>Invalidate should trigger OnPaint once</li>
<li id='LI08'>Click and write here with SendKeyEvent to trigger repaints:
<input id='editbox' type='text' value='' size="5" onfocus="onFocusTest(event)"></li>
<li id='LI09'>Click here with SendMouseClickEvent to navigate:
<input id='btnnavigate' type='button' onclick='navigate()'
value='Click here to navigate' /></li>
<li id='LI10' title='EXPECTED_TOOLTIP'>Mouse over this element will
trigger show a tooltip</li>
<li id='LI11' onclick='selectText(event)'>SELECTED_TEXT_RANGE</li>
<li><input id='email' type='text' size=10 inputmode='email' onfocus="onFocusTest(event)"></li>
<li id="quickmenu">Long touch press should trigger quick menu</li>
</ol>
<div class="dropdiv" id="dropdiv" ondrop="drop(event)" ondragover="allowDrop(event)">
<span id="draghere">Drag here</span>
</div>
<div class="dropdiv">
<div id="dragdiv" draggable="true" ondragstart="drag(event)"></div>
</div>
<div id="touchdiv" ontouchstart="onTouchEvent(event)" ontouchend="onTouchEvent(event)" ontouchmove="onTouchEvent(event)" ontouchcancel="onTouchEvent(event)">
</div>
<div id="pointerdiv" onpointerdown="onPointerEvent(event)" onpointerup="onPointerEvent(event)" onpointermove="onPointerEvent(event)" onpointercancel="onPointerEvent(event)">
</div>
<br />
<br />
<br />
<br />
<br />
<br />
</body>
</html>

View File

@@ -0,0 +1,9 @@
<html>
<head>
<title>PDF Test</title>
</head>
<body bgcolor="white">
<iframe src="pdf.pdf" width="500" height="500"></iframe>
<iframe src="pdf.pdf" width="500" height="500"></iframe>
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1,262 @@
use crate::{
browser::main_message_loop_external_pump::*,
common::{client_app::*, client_switches::*},
};
use cef::*;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
pub trait Delegate: Send {
fn on_before_command_line_processing(
&self,
_app: &Arc<ClientAppBrowser>,
_command_line: &mut CommandLine,
) {
}
fn on_register_custom_preferences(
&self,
_app: &Arc<ClientAppBrowser>,
_type_: PreferencesType,
_registrar: &mut PreferenceRegistrar,
) {
}
fn on_context_initialized(&self, _app: &Arc<ClientAppBrowser>) {}
fn on_already_running_app_relaunch(
&self,
_app: &Arc<ClientAppBrowser>,
_command_line: &mut CommandLine,
_current_directory: &Path,
) -> bool {
false
}
fn default_client(&self, _app: &Arc<ClientAppBrowser>) -> Option<Client> {
None
}
}
pub struct ClientAppBrowser {
delegates: Vec<Box<dyn Delegate>>,
}
impl ClientAppBrowser {
pub fn new(delegates: Vec<Box<dyn Delegate>>) -> Arc<Self> {
Arc::new(Self { delegates })
}
pub fn populate_settings(
command_line: Option<CommandLine>,
cookieable_schemes: Vec<String>,
settings: Settings,
) -> Settings {
#[cfg(any(target_os = "windows", target_os = "linux"))]
let settings = {
Settings {
multi_threaded_message_loop: command_line.as_ref().map_or(0, |command_line| {
command_line.has_switch(Some(&CefString::from(MULTI_THREADED_MESSAGE_LOOP)))
}),
..settings
}
};
let settings = if settings.multi_threaded_message_loop == 0 {
Settings {
external_message_pump: command_line.as_ref().map_or(0, |command_line| {
command_line.has_switch(Some(&CefString::from(EXTERNAL_MESSAGE_PUMP)))
}),
..settings
}
} else {
settings
};
let cookieable_schemes_list = CefString::from(cookieable_schemes.join(",").as_str());
Settings {
cookieable_schemes_list,
..settings
}
}
pub fn delegates(&self) -> &[Box<dyn Delegate>] {
&self.delegates
}
}
wrap_app! {
pub struct ClientAppBrowserApp {
base: ClientApp,
client_app_browser: Arc<ClientAppBrowser>,
}
impl App {
fn on_before_command_line_processing(
&self,
process_type: Option<&CefString>,
command_line: Option<&mut CommandLine>,
) {
let (Some(process_type), Some(command_line)) = (process_type, command_line) else {
return;
};
// Pass additional command-line flags to the browser process.
if process_type.to_string().is_empty() {
// Pass additional command-line flags when off-screen rendering is enabled.
if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED)))
!= 0
&& command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) == 0
{
// Use software rendering and compositing (disable GPU) for increased FPS
// and decreased CPU usage. This will also disable WebGL so remove these
// switches if you need that capability.
// See https://github.com/chromiumembedded/cef/issues/1257 for details.
if command_line.has_switch(Some(&CefString::from(ENABLE_GPU))) == 0 {
command_line.append_switch(Some(&CefString::from("disable-gpu")));
command_line
.append_switch(Some(&CefString::from("disable-gpu-compositing")));
}
}
if command_line.has_switch(Some(&CefString::from(USE_VIEWS))) != 0
&& command_line.has_switch(Some(&CefString::from("top-chrome-md"))) == 0
{
// Use non-material mode on all platforms by default. Among other things
// this causes menu buttons to show hover state. See usage of
// MaterialDesignController::IsModeMaterial() in Chromium code.
command_line.append_switch_with_value(
Some(&CefString::from("top-chrome-md")),
Some(&CefString::from("non-material")),
);
}
// Disable the toolchain prompt on macOS.
#[cfg(target_os = "macos")]
command_line.append_switch(Some(&CefString::from("use-mock-keychain")));
// On Linux, in off screen rendering (OSR) shared texture mode, we must
// ensure that ANGLE uses the EGL backend. Without this, DMABUF based
// rendering will fail. The Chromium fallback path uses X11 pixmaps,
// which are only supported by Mesa drivers (e.g., AMD and Intel).
//
// While Mesa supports DMABUFs via both EGL and pixmaps, the EGL based
// DMA BUF import path is more robust and required for compatibility with
// drivers like NVIDIA that do not support pixmaps.
//
// We also append the kOzonePlatform switch with value x11 to ensure
// that X11 semantics are preserved, which is necessary for compatibility
// with some GDK/X11 integrations (e.g. Wayland with AMD).
#[cfg(target_os = "linux")]
if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED)))
!= 0
&& command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) != 0
{
if command_line.has_switch(Some(&CefString::from(USE_ANGLE))) == 0 {
command_line.append_switch_with_value(
Some(&CefString::from(USE_ANGLE)),
Some(&CefString::from("gl-egl")),
);
}
if command_line.has_switch(Some(&CefString::from(OZONE_PLATFORM))) == 0 {
command_line.append_switch_with_value(
Some(&CefString::from(OZONE_PLATFORM)),
Some(&CefString::from("X11")),
);
}
}
}
for delegate in self.client_app_browser.delegates() {
delegate.on_before_command_line_processing(&self.client_app_browser, command_line);
}
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
self.base.on_register_custom_schemes(registrar);
}
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(ClientAppBrowserProcessHandler::new(
self.client_app_browser.clone(),
))
}
}
}
wrap_browser_process_handler! {
pub struct ClientAppBrowserProcessHandler {
client_app_browser: Arc<ClientAppBrowser>,
}
impl BrowserProcessHandler {
fn on_register_custom_preferences(
&self,
type_: PreferencesType,
registrar: Option<&mut PreferenceRegistrar>,
) {
let Some(registrar) = registrar else {
return;
};
for delegate in self.client_app_browser.delegates() {
delegate.on_register_custom_preferences(&self.client_app_browser, type_, registrar);
}
}
fn on_context_initialized(&self) {
for delegate in self.client_app_browser.delegates() {
delegate.on_context_initialized(&self.client_app_browser);
}
}
fn on_already_running_app_relaunch(
&self,
command_line: Option<&mut CommandLine>,
current_directory: Option<&CefString>,
) -> i32 {
let (Some(command_line), Some(current_directory)) = (command_line, current_directory)
else {
return 0;
};
let delegates = self.client_app_browser.delegates();
if !delegates.is_empty() {
let current_directory = PathBuf::from(current_directory.to_string().as_str());
for delegate in delegates {
if delegate.on_already_running_app_relaunch(
&self.client_app_browser,
command_line,
current_directory.as_path(),
) {
return 1;
}
}
}
0
}
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
if let Some(message_loop) = get_main_message_loop() {
if let Ok(mut message_loop) = message_loop.lock() {
message_loop.on_schedule_message_pump_work(delay_ms);
}
}
}
fn default_client(&self) -> Option<Client> {
for delegate in self.client_app_browser.delegates() {
let client = delegate.default_client(&self.client_app_browser);
if client.is_some() {
return client;
}
}
None
}
}
}

View File

@@ -0,0 +1,81 @@
use cef::*;
use std::{
fs::File,
io::{Read, Write},
path::Path,
};
fn allow_file_io() -> bool {
currently_on(ThreadId::UI) == 0 && currently_on(ThreadId::IO) == 0
}
/// Reads the file at `path` into `contents` and returns true on success and
/// false on error. In case of I/O error, `contents` holds the data that could
/// be read from the file before the error occurred. When the file size exceeds
/// `max_size`, the function returns false with `contents` holding the file
/// truncated to `max_size`. `contents` may be [None], in which case this
/// function is useful for its side effect of priming the disk cache (could be
/// used for unit tests). Calling this function on the browser process UI or IO
/// threads is not allowed.
pub fn read_file_to_buffer<P: AsRef<Path>>(
path: P,
contents: &mut Option<Vec<u8>>,
max_size: usize,
) -> bool {
if !allow_file_io() {
return false;
}
if let Some(contents) = contents.as_mut() {
contents.clear();
contents.reserve(max_size);
}
let Ok(mut file) = File::open(path) else {
return false;
};
// Many files supplied in `path` have incorrect size (proc files etc).
// Hence, the file is read sequentially as opposed to a one-shot read.
const BUFFER_SIZE: usize = 1 << 16;
let mut buffer = vec![0; BUFFER_SIZE];
let mut size = 0;
while let Ok(read) = file.read(&mut buffer) {
if read == 0 {
break;
}
if let Some(contents) = contents.as_mut() {
contents.extend_from_slice(&buffer[..read.min(max_size - size)]);
}
size += read;
if size > max_size {
return false;
}
}
true
}
/// Writes the given buffer into the file, overwriting any data that was
/// previously there. Returns the number of bytes written, or [None] on error.
/// Calling this function on the browser process UI or IO threads is not allowed.
pub fn write_file<P: AsRef<Path>>(path: P, contents: &[u8]) -> Option<usize> {
if !allow_file_io() {
return None;
}
let mut file = File::create(path).ok()?;
let mut size = 0;
while size < contents.len() {
let write = file.write(&contents[size..]).unwrap_or(0);
if write == 0 {
break;
}
size += write;
}
Some(size)
}

View File

@@ -0,0 +1,56 @@
use cef::*;
pub const fn logical_value_to_device(value: i32, scale_factor: f32) -> i32 {
(value as f32 * scale_factor) as i32
}
pub const fn logical_rect_to_device(rect: Rect, scale_factor: f32) -> Rect {
Rect {
x: logical_value_to_device(rect.x, scale_factor),
y: logical_value_to_device(rect.y, scale_factor),
width: logical_value_to_device(rect.width, scale_factor),
height: logical_value_to_device(rect.height, scale_factor),
}
}
pub const fn device_value_to_logical(value: i32, scale_factor: f32) -> i32 {
(value as f32 / scale_factor) as i32
}
pub const fn device_rect_to_logical(rect: Rect, scale_factor: f32) -> Rect {
Rect {
x: device_value_to_logical(rect.x, scale_factor),
y: device_value_to_logical(rect.y, scale_factor),
width: device_value_to_logical(rect.width, scale_factor),
height: device_value_to_logical(rect.height, scale_factor),
}
}
pub const fn device_mouse_event_to_logical(event: MouseEvent, scale_factor: f32) -> MouseEvent {
MouseEvent {
x: device_value_to_logical(event.x, scale_factor),
y: device_value_to_logical(event.y, scale_factor),
..event
}
}
pub const fn device_touch_event_to_logical(event: TouchEvent, scale_factor: f32) -> TouchEvent {
TouchEvent {
x: event.x / scale_factor,
y: event.y / scale_factor,
..event
}
}
pub fn constrain_window_bounds(display: &Rect, window: &mut Rect) {
window.x = window.x.max(display.x);
window.y = window.y.max(display.y);
window.width = window.width.clamp(100, display.width);
window.height = window.height.clamp(100, display.height);
if window.x + window.width > display.x + display.width {
window.x = display.x + display.width - window.width;
}
if window.y + window.height > display.y + display.height {
window.y = display.y + display.height - window.height;
}
}

View File

@@ -0,0 +1,135 @@
use cef::*;
use std::{
mem,
sync::{Arc, Mutex, OnceLock},
};
#[cfg(target_os = "windows")]
use windows_sys::Win32::Foundation::HWND;
static INSTANCE: OnceLock<Arc<Mutex<Option<Box<dyn MainMessageLoop>>>>> = OnceLock::new();
pub fn get_main_message_loop() -> Arc<Mutex<Option<Box<dyn MainMessageLoop>>>> {
INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone()
}
pub fn set_main_message_loop(
mut main_message_loop: Option<Box<dyn MainMessageLoop>>,
) -> Option<Box<dyn MainMessageLoop>> {
let instance = get_main_message_loop();
let Ok(mut instance) = instance.lock() else {
return main_message_loop;
};
mem::swap(&mut *instance, &mut main_message_loop);
main_message_loop
}
pub fn currently_on_main_thread() -> bool {
let instance = get_main_message_loop();
let Ok(instance) = instance.lock() else {
return false;
};
let Some(instance) = instance.as_ref() else {
return false;
};
instance.run_tasks_on_current_thread()
}
pub fn main_post_task(task: Option<&mut Task>) {
let instance = get_main_message_loop();
let Ok(mut instance) = instance.lock() else {
return;
};
let Some(instance) = instance.as_mut() else {
return;
};
instance.post_task(task);
}
pub fn main_post_once(closure: Box<dyn Send + FnOnce()>) {
let instance = get_main_message_loop();
let Ok(mut instance) = instance.lock() else {
return;
};
let Some(instance) = instance.as_mut() else {
return;
};
instance.post_once(closure);
}
pub fn main_post_repeating(closure: Box<dyn Send + FnMut()>) {
let instance = get_main_message_loop();
let Ok(mut instance) = instance.lock() else {
return;
};
let Some(instance) = instance.as_mut() else {
return;
};
instance.post_repeating(closure);
}
wrap_task! {
struct OnceClosure {
closure: Arc<Mutex<Option<Box<dyn Send + FnOnce()>>>>,
}
impl Task {
fn execute(&self) {
let Ok(mut closure) = self.closure.lock() else {
return;
};
let Some(closure) = closure.take() else {
return;
};
closure();
}
}
}
wrap_task! {
struct RepeatingClosure {
closure: Arc<Mutex<Option<Box<dyn Send + FnMut()>>>>,
}
impl Task {
fn execute(&self) {
let Ok(mut closure) = self.closure.lock() else {
return;
};
let Some(closure) = closure.as_mut() else {
return;
};
closure();
}
}
}
pub trait MainMessageLoop: Send {
/// Run the message loop. The thread that this method is called on will be considered the main
/// thread. This blocks until [MainMessageLoop::quit] is called.
fn run(&mut self) -> i32;
/// Quit the message loop.
fn quit(&mut self);
/// Post a task for execution on the main message loop.
fn post_task(&mut self, task: Option<&mut Task>);
/// Returns true if this message loop runs tasks on the current thread.
fn run_tasks_on_current_thread(&self) -> bool;
#[cfg(target_os = "windows")]
fn set_current_modeless_dialog(&mut self, hwnd: HWND);
/// Post a closure for execution on the main message loop.
fn post_once(&mut self, closure: Box<dyn Send + FnOnce()>) {
let mut task = OnceClosure::new(Arc::new(Mutex::new(Some(closure))));
self.post_task(Some(&mut task));
}
/// Post a closure for execution on the main message loop.
fn post_repeating(&mut self, closure: Box<dyn Send + FnMut()>) {
let mut task = RepeatingClosure::new(Arc::new(Mutex::new(Some(closure))));
self.post_task(Some(&mut task));
}
}

View File

@@ -0,0 +1,243 @@
use super::*;
use glib::*;
use std::{
io::{self, Read, Write},
os::fd::AsRawFd,
sync::{Arc, Mutex, Weak},
thread,
};
/// Return a timeout suitable for the glib loop, -1 to block forever,
/// 0 to return right away, or a timeout in milliseconds from now.
fn get_time_interval_milliseconds(cef_time: &cef::Time) -> i32 {
let mut time = 0.0;
time_to_doublet(Some(cef_time), Some(&mut time));
if time == 0.0 {
return -1;
}
let mut cef_now = Default::default();
time_now(Some(&mut cef_now));
let mut now = 0.0;
time_to_doublet(Some(&cef_now), Some(&mut now));
// Be careful here. CefTime has a precision of microseconds, but we want a
// value in milliseconds. If there are 5.5ms left, should the delay be 5 or
// 6? It should be 6 to avoid executing delayed work too early.
let interval = (time - now).ceil() * 1000.0;
let interval = interval as i32;
// If this value is negative, then we need to run delayed work soon.
interval.max(0)
}
fn handle_eintr<T>(mut callback: impl FnMut() -> io::Result<T>) -> io::Result<T> {
loop {
match callback() {
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
result => break result,
}
}
}
pub struct MainMessageLoopExternalPumpInner {
should_quit: bool,
main_context: MainContext,
work_source: Source,
timer_source: Option<Source>,
delayed_work_time: Arc<Mutex<Option<cef::Time>>>,
wakeup_pipe_write: io::PipeWriter,
}
impl Drop for MainMessageLoopExternalPumpInner {
fn drop(&mut self) {
self.work_source.destroy();
}
}
impl MainMessageLoopExternalPumpInner {
pub fn new(pump: &Weak<Mutex<MainMessageLoopExternalPump>>) -> Self {
let (mut wakeup_pipe_read, wakeup_pipe_write) =
io::pipe().expect("Failed to create wakeup pipe");
let delayed_work_time = Arc::new(Mutex::new(None));
let main_context = MainContext::default();
let work_source = {
let pump = pump.clone();
let delayed_work_time = delayed_work_time.clone();
unix_fd_source_new(
wakeup_pipe_read.as_raw_fd(),
IOCondition::IN,
None,
Priority::DEFAULT_IDLE,
move |raw_fd, condition| {
let Some(pump) = pump.upgrade() else {
return ControlFlow::Break;
};
let Ok(mut pump) = pump.lock() else {
return ControlFlow::Break;
};
// We usually have a single message on the wakeup pipe, since we are only
// signaled when the queue went from empty to non-empty, but there can be
// two messages if a task posted a task, hence we read at most two bytes.
// The glib poll will tell us whether there was data, so this read shouldn't
// block.
if condition.contains(IOCondition::IN) {
assert_eq!(wakeup_pipe_read.as_raw_fd(), raw_fd);
let mut buffer = [0; 16];
let size = handle_eintr(|| wakeup_pipe_read.read(&mut buffer))
.expect("Error reading from the wakeup pipe.");
match size {
16 => {
let mut delay_ms = [0; 8];
delay_ms.copy_from_slice(&buffer[..8]);
pump.on_schedule_work(i64::from_ne_bytes(delay_ms));
delay_ms.copy_from_slice(&buffer[8..]);
pump.on_schedule_work(i64::from_ne_bytes(delay_ms));
}
8..16 => {
let mut delay_ms = [0; 8];
delay_ms.copy_from_slice(&buffer[..8]);
pump.on_schedule_work(i64::from_ne_bytes(delay_ms));
}
_ => {}
}
}
if let Ok(delayed_work_time) = delayed_work_time.lock() {
let delay = delayed_work_time
.as_ref()
.map(get_time_interval_milliseconds)
.unwrap_or_default();
if delay == 0 {
// The timer has expired. That condition will stay true until we process
// that delayed work, so we don't need to record this differently.
pump.on_timer_timeout();
}
}
ControlFlow::Continue
},
)
};
work_source.attach(Some(&main_context));
Self {
should_quit: false,
main_context,
work_source,
timer_source: None,
delayed_work_time,
wakeup_pipe_write,
}
}
pub fn on_run(&mut self) -> bool {
// We really only do a single task for each iteration of the loop. If we
// have done something, assume there is likely something more to do. This
// will mean that we don't block on the message pump until there was nothing
// more to do. We also set this to true to make sure not to block on the
// first iteration of the loop.
let mut more_work_is_plausible = true;
// We run our own loop instead of using g_main_loop_quit in one of the
// callbacks. This is so we only quit our own loops, and we don't quit
// nested loops run by others.
loop {
// Don't block if we think we have more work to do.
let block = !more_work_is_plausible;
more_work_is_plausible = self.main_context.iteration(block);
if self.should_quit {
break;
}
}
// We need to run the message pump until it is idle. However we don't have
// that information here so we run the message loop "for a while".
for _ in 0..10 {
// Do some work.
do_message_loop_work();
// Sleep to allow the CEF proc to do work.
thread::sleep(Duration::from_micros(50000));
}
false
}
pub fn on_quit(&mut self) {
self.should_quit = true;
}
pub fn on_schedule_message_pump_work(&mut self, delay: i64) {
let buffer = delay.to_ne_bytes();
let size = handle_eintr(|| self.wakeup_pipe_write.write(&buffer)).unwrap_or_default();
assert_eq!(
size, 8,
"Could not write to the UI message loop wakeup pipe!"
);
}
pub fn set_timer(&mut self, delay: i64) {
assert!(delay > 0);
let mut delayed_work_time = self
.delayed_work_time
.lock()
.expect("Failed to lock delayed_work_time member");
let mut cef_now = Default::default();
time_now(Some(&mut cef_now));
let mut now = 0.0;
time_to_doublet(Some(&cef_now), Some(&mut now));
let time = now + delay as f64 / 1000.0;
let mut cef_time = Default::default();
if time_from_doublet(time, Some(&mut cef_time)) == 0 {
panic!("Failed to convert time to CEF time");
}
*delayed_work_time = Some(cef_time);
if let Some(timer_source) = self.timer_source.take() {
self.work_source.remove_child_source(&timer_source);
}
let timer_source = timeout_source_new(
Duration::from_millis(delay.max(0).unsigned_abs()),
None,
Priority::DEFAULT_IDLE,
|| ControlFlow::Continue,
);
self.work_source.add_child_source(&timer_source);
}
pub fn kill_timer(&mut self) {
let mut delayed_work_time = self
.delayed_work_time
.lock()
.expect("Failed to lock delayed_work_time member");
*delayed_work_time = None;
if let Some(timer_source) = self.timer_source.take() {
self.work_source.remove_child_source(&timer_source);
}
}
pub fn is_timer_pending(&self) -> bool {
let delayed_work_time = self
.delayed_work_time
.lock()
.expect("Failed to lock delayed_work_time member");
let delay = delayed_work_time
.as_ref()
.map(get_time_interval_milliseconds)
.unwrap_or_default();
delay > 0
}
}

View File

@@ -0,0 +1,135 @@
use super::*;
use objc2::{define_class, msg_send, rc::Retained, sel, AnyThread, DefinedClass};
use objc2_app_kit::{NSApp, NSEventTrackingRunLoopMode};
use objc2_foundation::{
MainThreadMarker, NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol,
NSRunLoop, NSRunLoopCommonModes, NSThread, NSTimer,
};
use std::sync::{Mutex, Weak};
define_class! {
#[unsafe(super(NSObject))]
#[ivars = Weak<Mutex<MainMessageLoopExternalPump>>]
struct EventHandler;
impl EventHandler {
#[unsafe(method(scheduleWork:))]
fn schedule_work(&self, delay_ms: &NSNumber) {
let Ok(delay_ms) = i64::try_from(delay_ms.integerValue()) else {
return;
};
let Some(pump) = self.ivars().upgrade() else {
return;
};
let Ok(mut pump) = pump.lock() else {
return;
};
pump.on_schedule_work(delay_ms);
}
#[unsafe(method(timerTimeout:))]
fn timer_timeout(&self, _: &NSTimer) {
let Some(pump) = self.ivars().upgrade() else {
return;
};
let Ok(mut pump) = pump.lock() else {
return;
};
pump.on_timer_timeout();
}
}
unsafe impl NSObjectProtocol for EventHandler {}
}
impl EventHandler {
fn new(pump: Weak<Mutex<MainMessageLoopExternalPump>>) -> Retained<Self> {
let this = Self::alloc().set_ivars(pump);
unsafe { msg_send![super(this), init] }
}
}
pub struct MainMessageLoopExternalPumpInner {
owner_thread: Retained<NSThread>,
timer: Option<Retained<NSTimer>>,
event_handler: Retained<EventHandler>,
}
unsafe impl Send for MainMessageLoopExternalPumpInner {}
impl MainMessageLoopExternalPumpInner {
pub fn new(pump: &Weak<Mutex<MainMessageLoopExternalPump>>) -> Self {
let event_handler = EventHandler::new(pump.clone());
Self {
owner_thread: NSThread::currentThread(),
timer: None,
event_handler,
}
}
pub fn on_run(&mut self) -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
NSApp(mtm).run();
true
}
pub fn on_quit(&mut self) {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
NSApp(mtm).stop(None);
}
pub fn on_schedule_message_pump_work(&mut self, delay: i64) {
// This method may be called on any thread.
let delay = isize::try_from(delay).unwrap_or(isize::MAX);
let number = NSNumber::numberWithInteger(delay);
unsafe {
self.event_handler
.performSelector_onThread_withObject_waitUntilDone(
sel!(scheduleWork:),
&self.owner_thread,
Some(&number),
false,
);
}
}
pub fn set_timer(&mut self, delay: i64) {
let delay_s = delay as f64 / 1000.0;
let timer = unsafe {
NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats(
delay_s,
&self.event_handler,
sel!(timerTimeout:),
None,
false,
)
};
// Add the timer to default and tracking runloop modes.
let owner_runloop = NSRunLoop::currentRunLoop();
unsafe {
owner_runloop.addTimer_forMode(&timer, NSRunLoopCommonModes);
owner_runloop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode);
}
self.timer = Some(timer);
}
pub fn kill_timer(&mut self) {
let Some(timer) = self.timer.take() else {
return;
};
timer.invalidate();
}
pub fn is_timer_pending(&self) -> bool {
self.timer.is_some()
}
}

View File

@@ -0,0 +1,218 @@
use super::{main_message_loop::*, main_message_loop_std::*};
use cef::*;
use std::{
sync::{Arc, Mutex, OnceLock, Weak},
time::Duration,
};
#[cfg(target_os = "windows")]
use windows_sys::Win32::Foundation::HWND;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
use win::MainMessageLoopExternalPumpInner;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "macos")]
use mac::MainMessageLoopExternalPumpInner;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux::MainMessageLoopExternalPumpInner;
/// Special timer delay placeholder value. Intentionally 32-bit for Windows and
/// OS X platform API compatibility.
const TIMER_DELAY_PLACEHOLDER: i32 = i32::MAX;
// The maximum number of milliseconds we're willing to wait between calls to
// [MainMessageLoopExternalPump::do_work].
const MAX_TIMER_DELAY: i64 = 1000 / 30; // 30fps
static INSTANCE: OnceLock<Weak<Mutex<MainMessageLoopExternalPump>>> = OnceLock::new();
pub fn get_main_message_loop() -> Option<Arc<Mutex<MainMessageLoopExternalPump>>> {
INSTANCE.get()?.upgrade()
}
pub fn set_main_message_loop(
main_message_loop: Option<Arc<Mutex<MainMessageLoopExternalPump>>>,
) -> Option<Arc<Mutex<MainMessageLoopExternalPump>>> {
let main_message_loop = main_message_loop
.as_ref()
.map(Arc::downgrade)
.unwrap_or_default();
if let Err(instance) = INSTANCE.set(main_message_loop) {
instance.upgrade()
} else {
None
}
}
pub struct MainMessageLoopExternalPump {
standard_message_loop: Arc<Mutex<Option<Box<dyn MainMessageLoop>>>>,
is_active: bool,
reentrancy_detected: bool,
inner: MainMessageLoopExternalPumpInner,
}
impl MainMessageLoopExternalPump {
pub fn new() -> Arc<Mutex<Self>> {
let external_pump = Arc::new_cyclic(|weak_ref| {
Mutex::new(Self {
standard_message_loop: MainMessageLoopStd::new(),
is_active: false,
reentrancy_detected: false,
inner: MainMessageLoopExternalPumpInner::new(weak_ref),
})
});
set_main_message_loop(Some(external_pump.clone()));
external_pump
}
/// Called from [BrowserProcessHandler::on_schedule_message_pump_work] on any thread.
/// The platform subclass must implement this method and schedule a call to
/// [MainMessageLoopExternalPump::on_schedule_work] on the main application thread.
pub fn on_schedule_message_pump_work(&mut self, delay: i64) {
self.inner.on_schedule_message_pump_work(delay);
}
fn on_schedule_work(&mut self, delay: i64) {
assert!(currently_on_main_thread());
if delay == i64::from(TIMER_DELAY_PLACEHOLDER) && self.is_timer_pending() {
// Don't set the maximum timer requested from DoWork() if a timer event is
// currently pending.
return;
}
self.kill_timer();
let delay = if delay <= 0 {
// Execute the work immediately.
self.do_work();
0
} else {
// Never wait longer than the maximum allowed time.
delay.min(MAX_TIMER_DELAY)
};
// Results in call to on_timer_timeout after the specified delay.
self.set_timer(delay);
}
fn on_timer_timeout(&mut self) {
assert!(currently_on_main_thread());
self.kill_timer();
self.do_work();
}
/// Control the pending work timer in the platform subclass. Only called on
/// the main application thread.
fn set_timer(&mut self, delay: i64) {
assert!(!self.is_timer_pending());
assert!(delay > 0);
self.inner.set_timer(delay);
}
/// Control the pending work timer in the platform subclass. Only called on
/// the main application thread.
fn kill_timer(&mut self) {
self.inner.kill_timer();
}
/// Control the pending work timer in the platform subclass. Only called on
/// the main application thread.
fn is_timer_pending(&self) -> bool {
self.inner.is_timer_pending()
}
/// Handle work processing.
fn do_work(&mut self) {
let was_reentrant = self.perform_message_loop_work();
if was_reentrant {
self.on_schedule_message_pump_work(0);
} else if !self.is_timer_pending() {
self.on_schedule_message_pump_work(i64::from(TIMER_DELAY_PLACEHOLDER));
}
}
fn perform_message_loop_work(&mut self) -> bool {
if self.is_active {
// When do_message_loop_work is called there may be various callbacks
// (such as paint and IPC messages) that result in additional calls to this
// method. If re-entrancy is detected we must repost a request again to the
// owner thread to ensure that the discarded call is executed in the future.
self.reentrancy_detected = true;
return false;
}
self.reentrancy_detected = false;
self.is_active = true;
do_message_loop_work();
self.is_active = false;
// `reentrancy_detected` may have changed due to re-entrant calls to this
// method.
self.reentrancy_detected
}
}
impl MainMessageLoop for MainMessageLoopExternalPump {
fn run(&mut self) -> i32 {
if !self.inner.on_run() {
return 0;
}
self.kill_timer();
// We need to run the message pump until it is idle. However we don't have
// that information here so we run the message loop "for a while".
for _ in 0..10 {
// Do some work.
do_message_loop_work();
// Sleep to allow the CEF proc to do work.
std::thread::sleep(Duration::from_millis(50));
}
0
}
fn quit(&mut self) {
self.inner.on_quit();
}
fn post_task(&mut self, task: Option<&mut Task>) {
let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else {
return;
};
let Some(standard_message_loop) = standard_message_loop.as_mut() else {
return;
};
standard_message_loop.post_task(task);
}
fn run_tasks_on_current_thread(&self) -> bool {
let Ok(standard_message_loop) = self.standard_message_loop.lock() else {
return false;
};
let Some(standard_message_loop) = standard_message_loop.as_ref() else {
return false;
};
standard_message_loop.run_tasks_on_current_thread()
}
#[cfg(target_os = "windows")]
fn set_current_modeless_dialog(&mut self, hwnd: HWND) {
let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else {
return;
};
let Some(standard_message_loop) = standard_message_loop.as_mut() else {
return;
};
standard_message_loop.set_current_modeless_dialog(hwnd);
}
}

View File

@@ -0,0 +1,145 @@
use super::*;
use crate::browser::util_win::*;
use std::{
mem, ptr,
sync::{Mutex, Weak},
};
use windows_sys::{w, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*};
const MSG_HAVE_WORK: u32 = WM_USER + 1;
pub struct MainMessageLoopExternalPumpInner {
timer_pending: bool,
main_thread_target: Option<usize>,
}
impl MainMessageLoopExternalPumpInner {
pub fn new(pump: &Weak<Mutex<MainMessageLoopExternalPump>>) -> Self {
let main_thread_target = {
const WINDOW_CLASS_NAME: *const u16 = w!("MainMessageLoopExternalPump");
let instance = get_code_module_handle();
unsafe {
let wcex = WNDCLASSEXW {
cbSize: mem::size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(Self::window_proc),
hInstance: instance,
lpszClassName: WINDOW_CLASS_NAME,
..mem::zeroed()
};
RegisterClassExW(&wcex);
// Create the message handling window.
let main_thread_target = CreateWindowExW(
0,
WINDOW_CLASS_NAME,
ptr::null(),
WS_OVERLAPPEDWINDOW,
0,
0,
0,
0,
HWND_MESSAGE,
ptr::null_mut(),
instance,
ptr::null(),
);
assert!(!main_thread_target.is_null());
main_thread_target
}
};
set_user_data(main_thread_target, Some(pump.clone()));
Self {
timer_pending: false,
main_thread_target: Some(main_thread_target as usize),
}
}
unsafe extern "system" fn window_proc(
hwnd: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match message {
MSG_HAVE_WORK | WM_TIMER => {
if let Some(message_loop) =
get_user_data::<Weak<Mutex<MainMessageLoopExternalPump>>>(hwnd)
{
if let Some(message_loop) = message_loop.upgrade() {
if let Ok(mut message_loop) = message_loop.lock() {
if message == MSG_HAVE_WORK {
let delay = lparam as i64;
message_loop.on_schedule_work(delay);
} else {
message_loop.on_timer_timeout();
}
}
}
}
}
WM_DESTROY => {
let _ = set_user_data::<Weak<Mutex<MainMessageLoopExternalPump>>>(hwnd, None);
}
_ => {}
}
DefWindowProcW(hwnd, message, wparam, lparam)
}
pub fn on_run(&mut self) -> bool {
let mut msg = Default::default();
unsafe {
while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
true
}
pub fn on_quit(&mut self) {
unsafe {
PostMessageW(ptr::null_mut(), WM_QUIT, 0, 0);
}
}
pub fn on_schedule_message_pump_work(&mut self, delay: i64) {
// This method may be called on any thread.
unsafe {
if let Some(main_thread_target) = self.main_thread_target {
PostMessageW(
main_thread_target as HWND,
MSG_HAVE_WORK,
0,
delay as LPARAM,
);
}
}
}
pub fn set_timer(&mut self, delay: i64) {
self.timer_pending = true;
if let Some(main_thread_target) = self.main_thread_target {
unsafe {
SetTimer(main_thread_target as HWND, 1, delay as u32, None);
}
}
}
pub fn kill_timer(&mut self) {
if self.timer_pending {
if let Some(main_thread_target) = self.main_thread_target {
unsafe {
KillTimer(main_thread_target as HWND, 1);
}
}
self.timer_pending = false;
}
}
pub fn is_timer_pending(&self) -> bool {
self.timer_pending
}
}

View File

@@ -0,0 +1,46 @@
use super::main_message_loop::*;
use cef::*;
use std::sync::{Arc, Mutex};
#[cfg(target_os = "windows")]
use windows_sys::Win32::Foundation::HWND;
pub struct MainMessageLoopStd;
impl MainMessageLoopStd {
pub fn new() -> Arc<Mutex<Option<Box<dyn MainMessageLoop>>>> {
set_main_message_loop(Some(Box::new(MainMessageLoopStd)));
get_main_message_loop()
}
}
impl Drop for MainMessageLoopStd {
fn drop(&mut self) {
set_main_message_loop(None);
}
}
impl MainMessageLoop for MainMessageLoopStd {
fn run(&mut self) -> i32 {
run_message_loop();
0
}
fn quit(&mut self) {
quit_message_loop();
}
fn post_task(&mut self, task: Option<&mut Task>) {
post_task(ThreadId::UI, task);
}
fn run_tasks_on_current_thread(&self) -> bool {
currently_on(ThreadId::UI) != 0
}
#[cfg(target_os = "windows")]
fn set_current_modeless_dialog(&mut self, _hwnd: HWND) {
// Nothing to do here. The Chromium message loop implementation will internally route
// dialog messages.
}
}

View File

@@ -0,0 +1,10 @@
pub mod client_app_browser;
pub mod file_util;
pub mod geometry_util;
pub mod main_message_loop;
pub mod main_message_loop_external_pump;
pub mod main_message_loop_std;
pub mod resource_util;
#[cfg(target_os = "windows")]
pub mod util_win;

View File

@@ -0,0 +1,9 @@
#[cfg(target_os = "windows")]
pub mod win;
#[cfg(target_os = "windows")]
pub use win::*;
#[cfg(not(target_os = "windows"))]
pub mod posix;
#[cfg(not(target_os = "windows"))]
pub use posix::*;

View File

@@ -0,0 +1,42 @@
use cef::*;
use std::{fs::File, io::Read, path::PathBuf};
pub fn get_resource_directory() -> Option<PathBuf> {
let mut path = std::env::current_exe().ok()?;
// Pop the executable file name.
path.pop();
#[cfg(target_os = "macos")]
{
// Pop the MacOS directory.
path.pop();
path.push("Resources");
}
#[cfg(target_os = "linux")]
{
path.push("files");
}
Some(path)
}
pub fn load_binary_resource(resource_name: &str) -> Option<Vec<u8>> {
let path = get_resource_directory()?.join(resource_name);
let mut file = File::open(path).ok()?;
let mut data = Vec::new();
file.read_to_end(&mut data).ok()?;
Some(data)
}
pub fn get_binary_resource_reader(resource_name: &str) -> Option<StreamReader> {
let path = get_resource_directory()?.join(resource_name);
if !path.exists() {
return None;
}
let path = path.to_str()?;
let path = CefString::from(path);
stream_reader_create_for_file(Some(&CefString::from(path)))
}

View File

@@ -0,0 +1,124 @@
use crate::browser::util_win::*;
use cef::{
wrapper::{byte_read_handler::*, resource_manager::*, stream_resource_handler::*},
*,
};
use std::{
mem,
sync::{Arc, Mutex, OnceLock},
};
use windows_sys::Win32::System::LibraryLoader::{
FindResourceW, LoadResource, LockResource, SizeofResource,
};
pub type GetResourceId = Box<dyn Send + Sync + Fn(&str) -> u16>;
static INSTANCE: OnceLock<Arc<Mutex<Option<GetResourceId>>>> = OnceLock::new();
pub fn get_fn_get_resource_id() -> Arc<Mutex<Option<GetResourceId>>> {
INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone()
}
pub fn set_fn_get_resource_id(mut get_resource_id: Option<GetResourceId>) -> Option<GetResourceId> {
let instance = get_fn_get_resource_id();
let Ok(mut instance) = instance.lock() else {
return get_resource_id;
};
mem::swap(&mut *instance, &mut get_resource_id);
get_resource_id
}
pub fn load_binary_resource(resource_name: &str) -> Option<Vec<u8>> {
let get_resource_id = get_fn_get_resource_id();
let get_resource_id = get_resource_id.lock().ok()?;
let get_resource_id = get_resource_id.as_ref()?;
let resource_id = (*get_resource_id)(resource_name);
let instance = get_code_module_handle();
unsafe {
// Defined in https://github.com/chromiumembedded/cef/blob/master/tests/cefclient/browser/resource.h
const RT_BINARY: u16 = 256;
let resource = FindResourceW(
instance,
resource_id as usize as *const _,
RT_BINARY as usize as *const _,
);
if resource.is_null() {
return None;
}
let data = LoadResource(instance, resource);
if data.is_null() {
return None;
}
let size = SizeofResource(instance, resource);
if size == 0 {
return None;
}
let ptr = LockResource(data);
if ptr.is_null() {
return None;
}
Some(std::slice::from_raw_parts(ptr as *const u8, size as usize).to_vec())
}
}
pub fn get_binary_resource_reader(resource_name: &str) -> Option<StreamReader> {
let data = load_binary_resource(resource_name)?;
let stream = ByteStream::new(data);
let mut handler = ByteReadHandler::new(Arc::new(Mutex::new(stream)));
stream_reader_create_for_handler(Some(&mut handler))
}
struct BinaryResourceProvider {
url_path: String,
resource_path_prefix: String,
}
impl BinaryResourceProvider {
fn new(url_path: &str, resource_path_prefix: &str) -> Self {
Self {
url_path: normalize_url_path(url_path),
resource_path_prefix: normalize_url_path(resource_path_prefix),
}
}
}
impl ResourceManagerProvider for BinaryResourceProvider {
fn on_request(&self, request: Arc<Mutex<ResourceManagerRequest>>) -> bool {
assert_ne!(
currently_on(ThreadId::IO),
0,
"on_request must be called on the IO thread"
);
let Ok(mut request) = request.lock() else {
return false;
};
let url = request.url();
let Some(relative_path) = url.strip_prefix(self.url_path.as_str()) else {
// Not handled by this provider.
return false;
};
let mime_type = request.mime_type_resolver()(url);
let relative_path = format!("{}/{relative_path}", self.resource_path_prefix);
let handler = get_binary_resource_reader(&relative_path)
.map(|stream| StreamResourceHandler::new_with_stream(mime_type, stream));
request.continue_request(handler);
true
}
}
pub fn create_binary_resource_provider(
url_path: &str,
resource_path_prefix: &str,
) -> Box<dyn ResourceManagerProvider> {
Box::new(BinaryResourceProvider::new(url_path, resource_path_prefix))
}

View File

@@ -0,0 +1,256 @@
use std::{ffi::c_void, mem, ptr, sync::OnceLock};
use windows_sys::Win32::{
Foundation::*,
Graphics::Gdi::*,
System::{
LibraryLoader::{
GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
},
Performance::*,
SystemServices::*,
},
UI::{Input::KeyboardAndMouse::*, WindowsAndMessaging::*},
};
pub fn get_time_now() -> u64 {
static FREQUENCY: OnceLock<f64> = OnceLock::new();
let frequency = FREQUENCY.get_or_init(|| {
let mut frequency = 0;
unsafe { QueryPerformanceFrequency(&mut frequency) };
frequency.max(1) as f64 / 1000000.0
});
let mut current_time = 0;
unsafe { QueryPerformanceCounter(&mut current_time) };
((current_time as f64 / *frequency) as i64).max(0) as u64
}
pub fn set_user_data_ptr(hwnd: HWND, data: *mut c_void) -> *mut c_void {
unsafe {
SetLastError(ERROR_SUCCESS);
let result = SetWindowLongPtrW(hwnd, GWLP_USERDATA, data as isize);
assert!(result != 0 || GetLastError() == ERROR_SUCCESS);
result as *mut _
}
}
pub fn set_user_data<T>(hwnd: HWND, data: Option<T>) -> Option<Box<T>> {
let ptr: *mut T = set_user_data_ptr(
hwnd,
data.map(|data| Box::into_raw(Box::new(data)).cast())
.unwrap_or(ptr::null_mut()),
)
.cast();
if ptr.is_null() {
None
} else {
unsafe { Some(Box::from_raw(ptr)) }
}
}
pub fn get_user_data<'a, T>(hwnd: HWND) -> Option<&'a mut T> {
unsafe {
let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut T;
ptr.as_mut()
}
}
pub fn set_window_proc_ptr(hwnd: HWND, proc: WNDPROC) -> WNDPROC {
Some(unsafe {
let old = GetWindowLongPtrW(hwnd, GWLP_WNDPROC);
assert_ne!(old, 0);
if let Some(proc) = proc {
let result = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, mem::transmute(proc));
assert!(result != 0 || GetLastError() == ERROR_SUCCESS);
}
mem::transmute(old)
})
}
#[repr(i32)]
#[derive(Default, Debug)]
pub enum EventFlag {
#[default]
None = 0,
CapsLockOn = 1 << 0,
ShiftDown = 1 << 1,
ControlDown = 1 << 2,
AltDown = 1 << 3,
LeftMouseButton = 1 << 4,
MiddleMouseButton = 1 << 5,
RightMouseButton = 1 << 6,
CommandDown = 1 << 7,
NumLockOn = 1 << 8,
IsKeyPad = 1 << 9,
IsLeft = 1 << 10,
IsRight = 1 << 11,
AltGrDown = 1 << 12,
IsRepeat = 1 << 13,
PrecisionScrollingDelta = 1 << 14,
ScrollByPage = 1 << 15,
}
pub fn get_resource_string(id: u32) -> String {
let mut buffer = [0u16; 100];
String::from_utf16_lossy(unsafe {
let len = LoadStringW(
get_code_module_handle(),
id,
buffer.as_mut_ptr(),
buffer.len() as i32,
) as usize;
&buffer[..len]
})
}
pub fn get_cef_mouse_modifiers(wparam: WPARAM) -> i32 {
let mut modifiers = 0;
if wparam & MK_CONTROL as usize != 0 {
modifiers |= EventFlag::ControlDown as i32;
}
if wparam & MK_SHIFT as usize != 0 {
modifiers |= EventFlag::ShiftDown as i32;
}
if is_key_down(VK_MENU as _) {
modifiers |= EventFlag::AltDown as i32;
}
if wparam & MK_LBUTTON as usize != 0 {
modifiers |= EventFlag::LeftMouseButton as i32;
}
if wparam & MK_MBUTTON as usize != 0 {
modifiers |= EventFlag::MiddleMouseButton as i32;
}
if wparam & MK_RBUTTON as usize != 0 {
modifiers |= EventFlag::RightMouseButton as i32;
}
// Low bit set from GetKeyState indicates "toggled".
unsafe {
if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 {
modifiers |= EventFlag::NumLockOn as i32;
}
if GetKeyState(VK_CAPITAL as i32) & 1 != 0 {
modifiers |= EventFlag::CapsLockOn as i32;
}
}
modifiers
}
pub fn get_cef_keyboard_modifiers(wparam: WPARAM, lparam: LPARAM) -> i32 {
let mut modifiers = 0;
if is_key_down(VK_SHIFT as _) {
modifiers |= EventFlag::ShiftDown as i32;
}
if is_key_down(VK_CONTROL as _) {
modifiers |= EventFlag::ControlDown as i32;
}
if is_key_down(VK_MENU as _) {
modifiers |= EventFlag::AltDown as i32;
}
// Low bit set from GetKeyState indicates "toggled".
unsafe {
if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 {
modifiers |= EventFlag::NumLockOn as i32;
}
if GetKeyState(VK_CAPITAL as i32) & 1 != 0 {
modifiers |= EventFlag::CapsLockOn as i32;
}
}
match wparam as VIRTUAL_KEY {
VK_RETURN => {
if (lparam >> 16) & KF_EXTENDED as isize != 0 {
modifiers |= EventFlag::IsKeyPad as i32;
}
}
VK_INSERT | VK_DELETE | VK_HOME | VK_END | VK_PRIOR | VK_NEXT | VK_UP | VK_DOWN
| VK_LEFT | VK_RIGHT => {
if (lparam >> 16) & KF_EXTENDED as isize == 0 {
modifiers |= EventFlag::IsKeyPad as i32;
}
}
VK_NUMLOCK | VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4
| VK_NUMPAD5 | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DIVIDE
| VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_DECIMAL | VK_CLEAR => {
modifiers |= EventFlag::IsKeyPad as i32;
}
VK_SHIFT => {
if is_key_down(VK_LSHIFT as _) {
modifiers |= EventFlag::IsLeft as i32;
} else if is_key_down(VK_RSHIFT as _) {
modifiers |= EventFlag::IsRight as i32;
}
}
VK_CONTROL => {
if is_key_down(VK_LCONTROL as _) {
modifiers |= EventFlag::IsLeft as i32;
} else if is_key_down(VK_RCONTROL as _) {
modifiers |= EventFlag::IsRight as i32;
}
}
VK_MENU => {
if is_key_down(VK_LMENU as _) {
modifiers |= EventFlag::IsLeft as i32;
} else if is_key_down(VK_RMENU as _) {
modifiers |= EventFlag::IsRight as i32;
}
}
VK_LWIN => {
modifiers |= EventFlag::IsLeft as i32;
}
VK_RWIN => {
modifiers |= EventFlag::IsRight as i32;
}
_ => {}
}
modifiers
}
pub fn is_key_down(wparam: WPARAM) -> bool {
unsafe {
let key_state = GetKeyState(wparam as i32) as u16;
key_state & 0x8000 != 0
}
}
pub fn get_device_scale_factor() -> f32 {
static SCALE_FACTOR: OnceLock<f32> = OnceLock::new();
*SCALE_FACTOR.get_or_init(|| unsafe {
let screen_dc = GetDC(ptr::null_mut());
let dpi_x = GetDeviceCaps(screen_dc, LOGPIXELSX as i32);
ReleaseDC(ptr::null_mut(), screen_dc);
dpi_x as f32 / 96.0
})
}
pub fn get_code_module_handle() -> HINSTANCE {
let mut module: HMODULE = ptr::null_mut();
unsafe {
let result = GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
get_code_module_handle as *const _,
&mut module,
);
assert_ne!(result, 0);
}
module
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_time_now() {
let time = get_time_now();
assert!(time > 0);
}
}

View File

@@ -0,0 +1,147 @@
use cef::*;
use std::{
fmt::Debug,
sync::OnceLock,
time::{Duration, Instant},
};
pub const TEST_SEND_PROCESS_MESSAGE: &[u8] = b"testSendProcessMessage";
pub const TEST_SEND_SMR_PROCESS_MESSAGE: &[u8] = b"testSendSMRProcessMessage";
#[derive(Debug)]
pub struct MessageId {
pub id: u32,
}
impl From<u32> for MessageId {
fn from(id: u32) -> Self {
Self { id }
}
}
impl From<&[u8]> for MessageId {
fn from(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), 4);
let mut id = [0; 4];
id.copy_from_slice(bytes);
Self::from(u32::from_ne_bytes(id))
}
}
impl From<&MessageId> for [u8; 4] {
fn from(id: &MessageId) -> Self {
id.id.to_ne_bytes()
}
}
pub struct ElapsedMicros {
duration: u128,
}
impl ElapsedMicros {
pub fn now() -> Self {
static START_TIME: OnceLock<Instant> = OnceLock::new();
let start_time = START_TIME.get_or_init(Instant::now);
Self {
duration: start_time.elapsed().as_micros(),
}
}
}
impl From<&[u8]> for ElapsedMicros {
fn from(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), 16);
let mut duration = [0; 16];
duration.copy_from_slice(bytes);
Self {
duration: u128::from_ne_bytes(duration),
}
}
}
impl From<&ElapsedMicros> for [u8; 16] {
fn from(elapsed: &ElapsedMicros) -> Self {
elapsed.duration.to_ne_bytes()
}
}
impl Debug for ElapsedMicros {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let duration = u64::try_from(self.duration).unwrap_or(u64::MAX);
let duration = Duration::from_micros(duration);
write!(f, "{duration:?}")
}
}
#[derive(Debug)]
pub struct BrowserMessage {
pub test_id: MessageId,
pub start_time: ElapsedMicros,
}
impl From<&BinaryValue> for BrowserMessage {
fn from(message: &BinaryValue) -> Self {
assert_eq!(message.size(), 20);
let mut data = vec![0; message.size()];
message.data(Some(&mut data), 0);
Self {
test_id: MessageId::from(&data[0..4]),
start_time: ElapsedMicros::from(&data[4..20]),
}
}
}
impl From<&BrowserMessage> for Option<BinaryValue> {
fn from(message: &BrowserMessage) -> Self {
let mut data = vec![0; 20];
let test_id: [u8; 4] = (&message.test_id).into();
let start_time: [u8; 16] = (&message.start_time).into();
data[0..4].copy_from_slice(&test_id);
data[4..20].copy_from_slice(&start_time);
binary_value_create(Some(&data))
}
}
#[derive(Debug)]
pub struct RendererMessage {
pub test_id: MessageId,
pub duration: ElapsedMicros,
pub start_time: ElapsedMicros,
}
impl From<&BinaryValue> for RendererMessage {
fn from(message: &BinaryValue) -> Self {
assert_eq!(message.size(), 36);
let mut data = vec![0; message.size()];
message.data(Some(&mut data), 0);
Self {
test_id: MessageId::from(&data[0..4]),
duration: ElapsedMicros::from(&data[4..20]),
start_time: ElapsedMicros::from(&data[20..36]),
}
}
}
impl From<&RendererMessage> for Option<BinaryValue> {
fn from(message: &RendererMessage) -> Self {
let mut data = vec![0; 36];
let test_id: [u8; 4] = (&message.test_id).into();
let duration: [u8; 16] = (&message.duration).into();
let start_time: [u8; 16] = (&message.start_time).into();
data[0..4].copy_from_slice(&test_id);
data[4..20].copy_from_slice(&duration);
data[20..36].copy_from_slice(&start_time);
binary_value_create(Some(&data))
}
}

View File

@@ -0,0 +1,66 @@
use cef::*;
pub const PROCESS_TYPE: &str = "type";
pub const RENDERER_PROCESS: &str = "renderer";
#[cfg(target_os = "linux")]
pub const ZYGOTE_PROCESS: &str = "zygote";
pub enum ProcessType {
Browser,
Renderer,
#[cfg(target_os = "linux")]
Zygote,
Other,
}
impl From<&CommandLine> for ProcessType {
fn from(value: &CommandLine) -> Self {
let process_type = CefString::from(PROCESS_TYPE);
if value.has_switch(Some(&process_type)) == 0 {
return Self::Browser;
}
let value = CefString::from(&value.switch_value(Some(&process_type))).to_string();
match value.as_str() {
RENDERER_PROCESS => Self::Renderer,
#[cfg(target_os = "linux")]
ZYGOTE_PROCESS => Self::Zygote,
_ => Self::Other,
}
}
}
#[derive(Clone)]
pub struct ClientAppCustomScheme {
name: String,
options: i32,
}
impl ClientAppCustomScheme {
pub fn new(name: &str, options: &[SchemeOptions]) -> Self {
let options = options.iter().fold(0, |acc, opt| acc | opt.get_raw()) as i32;
Self {
name: name.to_string(),
options,
}
}
}
wrap_app! {
pub struct ClientApp {
custom_schemes: Vec<ClientAppCustomScheme>,
}
impl App {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
let Some(registrar) = registrar else {
return;
};
for scheme in &self.custom_schemes {
let name = CefString::from(scheme.name.as_str());
registrar.add_custom_scheme(Some(&name), scheme.options);
}
}
}
}

View File

@@ -0,0 +1,13 @@
use cef::*;
wrap_app! {
pub struct ClientAppOther {
base: App,
}
impl App {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
self.base.on_register_custom_schemes(registrar);
}
}
}

View File

@@ -0,0 +1,52 @@
//! CEF and Chromium support a wide range of command-line switches. This file
//! only contains command-line switches specific to the cefclient application.
//! View CEF/Chromium documentation or search for *_switches.cc files in the
//! Chromium source code to identify other existing command-line switches.
pub const MULTI_THREADED_MESSAGE_LOOP: &str = "multi-threaded-message-loop";
pub const EXTERNAL_MESSAGE_PUMP: &str = "external-message-pump";
pub const CACHE_PATH: &str = "cache-path";
pub const URL: &str = "url";
pub const OFF_SCREEN_RENDERING_ENABLED: &str = "off-screen-rendering-enabled";
pub const OFF_SCREEN_FRAME_RATE: &str = "off-screen-frame-rate";
pub const TRANSPARENT_PAINTING_ENABLED: &str = "transparent-painting-enabled";
pub const SHOW_UPDATE_RECT: &str = "show-update-rect";
pub const FAKE_SCREEN_BOUNDS: &str = "fake-screen-bounds";
pub const SHARED_TEXTURE_ENABLED: &str = "shared-texture-enabled";
pub const EXTERNAL_BEGIN_FRAME_ENABLED: &str = "external-begin-frame-enabled";
pub const MOUSE_CURSOR_CHANGE_DISABLED: &str = "mouse-cursor-change-disabled";
pub const OFFLINE: &str = "offline";
pub const FILTER_CHROME_COMMANDS: &str = "filter-chrome-commands";
pub const REQUEST_CONTEXT_PER_BROWSER: &str = "request-context-per-browser";
pub const REQUEST_CONTEXT_SHARED_CACHE: &str = "request-context-shared-cache";
pub const BACKGROUND_COLOR: &str = "background-color";
pub const ENABLE_GPU: &str = "enable-gpu";
pub const FILTER_URL: &str = "filter-url";
pub const USE_VIEWS: &str = "use-views";
pub const USE_NATIVE: &str = "use-native";
pub const HIDE_FRAME: &str = "hide-frame";
pub const HIDE_CONTROLS: &str = "hide-controls";
pub const HIDE_OVERLAYS: &str = "hide-overlays";
pub const ALWAYS_ON_TOP: &str = "always-on-top";
pub const HIDE_TOP_MENU: &str = "hide-top-menu";
pub const SSL_CLIENT_CERTIFICATE: &str = "ssl-client-certificate";
pub const CRL_SETS_PATH: &str = "crl-sets-path";
pub const NO_ACTIVATE: &str = "no-activate";
pub const SHOW_CHROME_TOOLBAR: &str = "show-chrome-toolbar";
pub const INITIAL_SHOW_STATE: &str = "initial-show-state";
pub const USE_DEFAULT_POPUP: &str = "use-default-popup";
pub const USE_CLIENT_DIALOGS: &str = "use-client-dialogs";
pub const USE_TEST_HTTP_SERVER: &str = "use-test-http-server";
pub const SHOW_WINDOW_BUTTONS: &str = "show-window-buttons";
pub const USE_WINDOW_MODAL_DIALOG: &str = "use-window-modal-dialog";
pub const USE_BOTTOM_CONTROLS: &str = "use-bottom-controls";
pub const HIDE_PIP_FRAME: &str = "hide-pip-frame";
pub const MOVE_PIP_ENABLED: &str = "move-pip-enabled";
pub const HIDE_CHROME_BUBBLES: &str = "hide-chrome-bubbles";
pub const HIDE_WINDOW_ON_CLOSE: &str = "hide-window-on-close";
pub const ACCEPTS_FIRST_MOUSE: &str = "accepts-first-mouse";
pub const USE_ALLOY_STYLE: &str = "use-alloy-style";
pub const USE_CHROME_STYLE_WINDOW: &str = "use-chrome-style-window";
pub const SHOW_OVERLAY_BROWSER: &str = "show-overlay-browser";
pub const USE_ANGLE: &str = "use-angle";
pub const OZONE_PLATFORM: &str = "ozone-platform";

View File

@@ -0,0 +1,4 @@
pub mod binary_value_utils;
pub mod client_app;
pub mod client_app_other;
pub mod client_switches;

View File

@@ -0,0 +1,6 @@
pub mod browser;
pub mod common;
pub mod renderer;
#[cfg(target_os = "macos")]
pub mod process_helper_mac;

View File

@@ -0,0 +1,44 @@
use crate::{
common::{client_app::*, client_app_other::*},
renderer::client_app_renderer::*,
};
use cef::{args::Args, *};
pub fn run_main(
args: Args,
custom_schemes: Vec<ClientAppCustomScheme>,
app_renderer_delegates: Vec<Box<dyn Delegate>>,
) -> Result<(), i32> {
#[cfg(feature = "sandbox")]
let _sandbox = {
let mut sandbox = cef::sandbox::Sandbox::new();
sandbox.initialize(args.as_main_args());
sandbox
};
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true);
assert!(loader.load());
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let app = ClientApp::new(custom_schemes);
let mut app = match args.as_cmd_line().map(|cmd| ProcessType::from(&cmd)) {
Some(ProcessType::Renderer) => {
let app_renderer = ClientAppRenderer::new(app_renderer_delegates);
ClientAppRendererApp::new(app, app_renderer)
}
_ => ClientAppOther::new(app),
};
match execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
) {
0 => Ok(()),
err => Err(err),
}
}

View File

@@ -0,0 +1,255 @@
use cef::*;
use std::sync::Arc;
pub trait Delegate: Send {
fn on_web_kit_initialized(&self, _app: &Arc<ClientAppRenderer>) {}
fn on_browser_created(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_extra_info: Option<&DictionaryValue>,
) {
}
fn on_browser_destroyed(&self, _app: &Arc<ClientAppRenderer>, _browser: Option<&Browser>) {}
fn load_handler(&self, _app: &Arc<ClientAppRenderer>) -> Option<LoadHandler> {
None
}
fn on_context_created(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_frame: Option<&Frame>,
_context: Option<&V8Context>,
) {
}
fn on_context_released(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_frame: Option<&Frame>,
_context: Option<&V8Context>,
) {
}
fn on_uncaught_exception(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_frame: Option<&Frame>,
_context: Option<&V8Context>,
_exception: Option<&V8Exception>,
_stack_trace: Option<&V8StackTrace>,
) {
}
fn on_focused_node_changed(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_frame: Option<&Frame>,
_node: Option<&Domnode>,
) {
}
fn on_process_message_received(
&self,
_app: &Arc<ClientAppRenderer>,
_browser: Option<&Browser>,
_frame: Option<&Frame>,
_source_process: ProcessId,
_message: Option<&ProcessMessage>,
) -> i32 {
0
}
}
pub struct ClientAppRenderer {
delegates: Vec<Box<dyn Delegate>>,
}
impl ClientAppRenderer {
pub fn new(delegates: Vec<Box<dyn Delegate>>) -> Arc<Self> {
Arc::new(Self { delegates })
}
pub fn delegates(&self) -> &[Box<dyn Delegate>] {
&self.delegates
}
}
wrap_render_process_handler! {
struct ClientAppRendererRenderProcessHandler {
client_app_renderer: Arc<ClientAppRenderer>,
}
impl RenderProcessHandler {
fn on_web_kit_initialized(&self) {
for delegate in self.client_app_renderer.delegates() {
delegate.on_web_kit_initialized(&self.client_app_renderer);
}
}
fn on_browser_created(
&self,
browser: Option<&mut Browser>,
extra_info: Option<&mut DictionaryValue>,
) {
let browser = browser.cloned();
let extra_info = extra_info.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_browser_created(
&self.client_app_renderer,
browser.as_ref(),
extra_info.as_ref(),
);
}
}
fn on_browser_destroyed(&self, browser: Option<&mut Browser>) {
let browser = browser.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_browser_destroyed(&self.client_app_renderer, browser.as_ref());
}
}
fn load_handler(&self) -> Option<LoadHandler> {
for delegate in self.client_app_renderer.delegates() {
if let Some(load_handler) = delegate.load_handler(&self.client_app_renderer) {
return Some(load_handler);
}
}
None
}
fn on_context_created(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
let browser = browser.cloned();
let frame = frame.cloned();
let context = context.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_context_created(
&self.client_app_renderer,
browser.as_ref(),
frame.as_ref(),
context.as_ref(),
);
}
}
fn on_context_released(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
let browser = browser.cloned();
let frame = frame.cloned();
let context = context.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_context_released(
&self.client_app_renderer,
browser.as_ref(),
frame.as_ref(),
context.as_ref(),
);
}
}
fn on_uncaught_exception(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
exception: Option<&mut V8Exception>,
stack_trace: Option<&mut V8StackTrace>,
) {
let browser = browser.cloned();
let frame = frame.cloned();
let context = context.cloned();
let exception = exception.cloned();
let stack_trace = stack_trace.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_uncaught_exception(
&self.client_app_renderer,
browser.as_ref(),
frame.as_ref(),
context.as_ref(),
exception.as_ref(),
stack_trace.as_ref(),
);
}
}
fn on_focused_node_changed(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
node: Option<&mut Domnode>,
) {
let browser = browser.cloned();
let frame = frame.cloned();
let node = node.cloned();
for delegate in self.client_app_renderer.delegates() {
delegate.on_focused_node_changed(
&self.client_app_renderer,
browser.as_ref(),
frame.as_ref(),
node.as_ref(),
);
}
}
fn on_process_message_received(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
source_process: ProcessId,
message: Option<&mut ProcessMessage>,
) -> i32 {
let browser = browser.cloned();
let frame = frame.cloned();
let message = message.cloned();
for delegate in self.client_app_renderer.delegates() {
let handled = delegate.on_process_message_received(
&self.client_app_renderer,
browser.as_ref(),
frame.as_ref(),
source_process,
message.as_ref(),
);
if handled != 0 {
return handled;
}
}
0
}
}
}
wrap_app! {
pub struct ClientAppRendererApp {
base: App,
client_app_renderer: Arc<ClientAppRenderer>,
}
impl App {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
self.base.on_register_custom_schemes(registrar);
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(ClientAppRendererRenderProcessHandler::new(
self.client_app_renderer.clone(),
))
}
}
}

View File

@@ -0,0 +1 @@
pub mod client_app_renderer;

View File

@@ -19,7 +19,7 @@ fn default_version() -> &'static str {
fn default_download_url() -> &'static str {
static DEFAULT_DOWNLOAD_URL: OnceLock<String> = OnceLock::new();
DEFAULT_DOWNLOAD_URL
.get_or_init(|| download_cef::default_download_url())
.get_or_init(download_cef::default_download_url)
.as_str()
}

View File

@@ -44,7 +44,7 @@ type Result<T> = std::result::Result<T, Error>;
fn default_download_url() -> &'static str {
static DEFAULT_DOWNLOAD_URL: OnceLock<String> = OnceLock::new();
DEFAULT_DOWNLOAD_URL
.get_or_init(|| download_cef::default_download_url())
.get_or_init(download_cef::default_download_url)
.as_str()
}

View File

@@ -41,7 +41,7 @@ fn default_version() -> &'static str {
fn default_download_url() -> &'static str {
static DEFAULT_DOWNLOAD_URL: OnceLock<String> = OnceLock::new();
DEFAULT_DOWNLOAD_URL
.get_or_init(|| download_cef::default_download_url())
.get_or_init(download_cef::default_download_url)
.as_str()
}

View File

@@ -2720,6 +2720,7 @@ fn make_my_struct() -> {rust_name} {{
$($generic_type: $first_generic_type_bound $(+ $generic_type_bound)*,)+
)?
{
#[allow(clippy::new_ret_no_self)]
pub fn new($($field_name: $field_type),*) -> #rust_name {
#rust_name::new(
Self {