From 816b57162ef710f598d3e471bd7f37c6ae89fc07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:32:26 +0000 Subject: [PATCH 01/16] Initial plan From e6b1a918c70501314c7c5926c4a7ce24844fe977 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:43:24 +0000 Subject: [PATCH 02/16] Extract core wef library, cargo-wef tool, and winit example Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- .gitignore | 35 + Cargo.toml | 61 ++ cargo-wef/Cargo.toml | 28 + cargo-wef/src/commands/add_framework.rs | 25 + cargo-wef/src/commands/build.rs | 713 +++++++++++++++++++ cargo-wef/src/commands/init.rs | 85 +++ cargo-wef/src/commands/mod.rs | 9 + cargo-wef/src/commands/run.rs | 18 + cargo-wef/src/internal/add_cef_framework.rs | 214 ++++++ cargo-wef/src/internal/add_helper.rs | 308 ++++++++ cargo-wef/src/internal/cef_platform.rs | 47 ++ cargo-wef/src/internal/download_cef.rs | 138 ++++ cargo-wef/src/internal/find_cef_root.rs | 9 + cargo-wef/src/internal/mod.rs | 13 + cargo-wef/src/internal/plist.rs | 122 ++++ cargo-wef/src/main.rs | 188 +++++ examples/wef-winit/Cargo.toml | 20 + examples/wef-winit/README.md | 21 + examples/wef-winit/build.rs | 13 + examples/wef-winit/icons/icon128x128.png | Bin 0 -> 19340 bytes examples/wef-winit/icons/icon32x32.png | Bin 0 -> 2601 bytes examples/wef-winit/src/main.rs | 364 ++++++++++ wef/.rustfmt.toml | 12 + wef/Cargo.toml | 29 + wef/LICENSE-APACHE | 191 +++++ wef/README.md | 359 ++++++++++ wef/build.rs | 305 ++++++++ wef/cpp/app.h | 80 +++ wef/cpp/app_callbacks.h | 7 + wef/cpp/app_render_process.h | 74 ++ wef/cpp/browser_callbacks.h | 67 ++ wef/cpp/client.cpp | 487 +++++++++++++ wef/cpp/client.h | 291 ++++++++ wef/cpp/cursor.cpp | 21 + wef/cpp/dirty_rect.cpp | 14 + wef/cpp/external_pump.cpp | 74 ++ wef/cpp/external_pump.h | 30 + wef/cpp/external_pump_linux.cpp | 212 ++++++ wef/cpp/external_pump_mac.mm | 123 ++++ wef/cpp/external_pump_win.cpp | 81 +++ wef/cpp/file_dialog.cpp | 21 + wef/cpp/frame.cpp | 65 ++ wef/cpp/frame.h | 7 + wef/cpp/js_dialog.cpp | 15 + wef/cpp/load_library.cpp | 27 + wef/cpp/query.cpp | 25 + wef/cpp/sandbox_context.cpp | 19 + wef/cpp/utils.h | 35 + wef/cpp/wef.cpp | 417 +++++++++++ wef/src/app_handler.rs | 37 + wef/src/browser.rs | 224 ++++++ wef/src/browser_handler.rs | 667 +++++++++++++++++ wef/src/builder.rs | 186 +++++ wef/src/context_menu.rs | 147 ++++ wef/src/cursor.rs | 72 ++ wef/src/dirty_rects.rs | 95 +++ wef/src/dpi.rs | 43 ++ wef/src/error.rs | 15 + wef/src/ffi.rs | 337 +++++++++ wef/src/file_dialog.rs | 73 ++ wef/src/frame.rs | 153 ++++ wef/src/framework_loader.rs | 38 + wef/src/func_registry/async_function_type.rs | 120 ++++ wef/src/func_registry/builder.rs | 91 +++ wef/src/func_registry/dyn_wrapper.rs | 96 +++ wef/src/func_registry/error.rs | 26 + wef/src/func_registry/function_type.rs | 110 +++ wef/src/func_registry/inject.js | 35 + wef/src/func_registry/into_result.rs | 30 + wef/src/func_registry/mod.rs | 13 + wef/src/func_registry/registry.rs | 105 +++ wef/src/geom.rs | 117 +++ wef/src/input.rs | 66 ++ wef/src/js_dialog.rs | 43 ++ wef/src/lib.rs | 54 ++ wef/src/query.rs | 39 + wef/src/sandbox_context.rs | 37 + wef/src/settings.rs | 117 +++ wef/src/wef.rs | 146 ++++ 79 files changed, 8851 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 cargo-wef/Cargo.toml create mode 100644 cargo-wef/src/commands/add_framework.rs create mode 100644 cargo-wef/src/commands/build.rs create mode 100644 cargo-wef/src/commands/init.rs create mode 100644 cargo-wef/src/commands/mod.rs create mode 100644 cargo-wef/src/commands/run.rs create mode 100644 cargo-wef/src/internal/add_cef_framework.rs create mode 100644 cargo-wef/src/internal/add_helper.rs create mode 100644 cargo-wef/src/internal/cef_platform.rs create mode 100644 cargo-wef/src/internal/download_cef.rs create mode 100644 cargo-wef/src/internal/find_cef_root.rs create mode 100644 cargo-wef/src/internal/mod.rs create mode 100644 cargo-wef/src/internal/plist.rs create mode 100644 cargo-wef/src/main.rs create mode 100644 examples/wef-winit/Cargo.toml create mode 100644 examples/wef-winit/README.md create mode 100644 examples/wef-winit/build.rs create mode 100644 examples/wef-winit/icons/icon128x128.png create mode 100644 examples/wef-winit/icons/icon32x32.png create mode 100644 examples/wef-winit/src/main.rs create mode 100644 wef/.rustfmt.toml create mode 100644 wef/Cargo.toml create mode 100644 wef/LICENSE-APACHE create mode 100644 wef/README.md create mode 100644 wef/build.rs create mode 100644 wef/cpp/app.h create mode 100644 wef/cpp/app_callbacks.h create mode 100644 wef/cpp/app_render_process.h create mode 100644 wef/cpp/browser_callbacks.h create mode 100644 wef/cpp/client.cpp create mode 100644 wef/cpp/client.h create mode 100644 wef/cpp/cursor.cpp create mode 100644 wef/cpp/dirty_rect.cpp create mode 100644 wef/cpp/external_pump.cpp create mode 100644 wef/cpp/external_pump.h create mode 100644 wef/cpp/external_pump_linux.cpp create mode 100644 wef/cpp/external_pump_mac.mm create mode 100644 wef/cpp/external_pump_win.cpp create mode 100644 wef/cpp/file_dialog.cpp create mode 100644 wef/cpp/frame.cpp create mode 100644 wef/cpp/frame.h create mode 100644 wef/cpp/js_dialog.cpp create mode 100644 wef/cpp/load_library.cpp create mode 100644 wef/cpp/query.cpp create mode 100644 wef/cpp/sandbox_context.cpp create mode 100644 wef/cpp/utils.h create mode 100644 wef/cpp/wef.cpp create mode 100644 wef/src/app_handler.rs create mode 100644 wef/src/browser.rs create mode 100644 wef/src/browser_handler.rs create mode 100644 wef/src/builder.rs create mode 100644 wef/src/context_menu.rs create mode 100644 wef/src/cursor.rs create mode 100644 wef/src/dirty_rects.rs create mode 100644 wef/src/dpi.rs create mode 100644 wef/src/error.rs create mode 100644 wef/src/ffi.rs create mode 100644 wef/src/file_dialog.rs create mode 100644 wef/src/frame.rs create mode 100644 wef/src/framework_loader.rs create mode 100644 wef/src/func_registry/async_function_type.rs create mode 100644 wef/src/func_registry/builder.rs create mode 100644 wef/src/func_registry/dyn_wrapper.rs create mode 100644 wef/src/func_registry/error.rs create mode 100644 wef/src/func_registry/function_type.rs create mode 100644 wef/src/func_registry/inject.js create mode 100644 wef/src/func_registry/into_result.rs create mode 100644 wef/src/func_registry/mod.rs create mode 100644 wef/src/func_registry/registry.rs create mode 100644 wef/src/geom.rs create mode 100644 wef/src/input.rs create mode 100644 wef/src/js_dialog.rs create mode 100644 wef/src/lib.rs create mode 100644 wef/src/query.rs create mode 100644 wef/src/sandbox_context.rs create mode 100644 wef/src/settings.rs create mode 100644 wef/src/wef.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e694a17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +/target/ +Cargo.lock + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ + +# CEF files (will be downloaded by cargo-wef) +.cef/ +*.pak +*.dat +*.bin +*.dll +*.so +*.dylib +libcef.* +libEGL.* +libGLESv2.* +icudtl.dat +resources.pak +chrome_*.pak +snapshot_blob.bin +v8_context_snapshot.bin +locales/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9d95054 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[workspace] +members = [ + "wef", + "cargo-wef", + "examples/wef-winit", +] + +default-members = ["wef"] +resolver = "2" + +[workspace.dependencies] +wef = { path = "wef" } +ropey = { version = "=2.0.0-beta.1", features = ["metric_utf16", "metric_lines_lf"] } + +anyhow = "1" +log = "0.4" +serde = { version = "1.0.219", features = ["derive"] } +serde_repr = "0.1" +serde_json = "1" +schemars = "1" +smallvec = "1" +rust-i18n = "3" +raw-window-handle = "0.6.2" +smol = "1" +tracing = "0.1.41" +notify = "7.0.0" +lsp-types = "0.97.0" + +[workspace.dependencies.windows] +features = ["Wdk", "Wdk_System", "Wdk_System_SystemServices"] +version = "0.58.0" + +[workspace.lints.clippy] +almost_complete_range = "allow" +arc_with_non_send_sync = "allow" +borrowed_box = "allow" +dbg_macro = "deny" +let_underscore_future = "allow" +map_entry = "allow" +module_inception = "allow" +non_canonical_partial_ord_impl = "allow" +reversed_empty_ranges = "allow" +single_range_in_vec_init = "allow" +style = { level = "allow", priority = -1 } +todo = "deny" +type_complexity = "allow" +manual_is_multiple_of = "allow" + +[profile.dev] +codegen-units = 16 +debug = "limited" +split-debuginfo = "unpacked" + +[profile.dev.package] +resvg = { opt-level = 3 } +rustybuzz = { opt-level = 3 } +taffy = { opt-level = 3 } +ttf-parser = { opt-level = 3 } + +[workspace.metadata.typos] +files.extend-exclude = ["**/fixtures/*"] \ No newline at end of file diff --git a/cargo-wef/Cargo.toml b/cargo-wef/Cargo.toml new file mode 100644 index 0000000..7d1452b --- /dev/null +++ b/cargo-wef/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "cargo-wef" +version = "0.7.0" +edition = "2024" +authors = ["sunli "] +license = "Apache-2.0" +homepage = "https://github.com/longbridge/wef" +repository = "https://github.com/longbridge/wef" +description = "Cargo-wef is a command line tool for wef" +readme = "../wef/README.md" + +[dependencies] +clap = { version = "4.5.38", features = ["derive", "env"] } +plist = "1.7.1" +tempfile = "3.20.0" +anyhow = "1.0.98" +askama = { version = "0.14.0", features = ["code-in-doc"] } +reqwest = { version = "0.12.15", features = ["blocking", "json"] } +serde = { version = "1.0.219", features = ["derive"] } +fs_extra = "1.3.0" +tar = "0.4.44" +bzip2 = "0.5.2" +indicatif = "0.17.7" +dirs = "6.0.0" +cargo_metadata = "0.20.0" +serde_json = "1.0.140" +image = "0.25.6" +icns = "0.3.1" \ No newline at end of file diff --git a/cargo-wef/src/commands/add_framework.rs b/cargo-wef/src/commands/add_framework.rs new file mode 100644 index 0000000..05c2220 --- /dev/null +++ b/cargo-wef/src/commands/add_framework.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use anyhow::Result; + +#[allow(unused_variables)] +pub(crate) fn add_framework( + app_path: PathBuf, + release: bool, + force: bool, + wef_version: Option, + wef_path: Option, +) -> Result<()> { + let cef_root = crate::internal::find_cef_root(); + crate::internal::add_cef_framework(&cef_root, &app_path, release, force)?; + + #[cfg(target_os = "macos")] + crate::internal::add_helper( + &app_path, + wef_version.as_deref(), + wef_path.as_deref(), + release, + force, + )?; + Ok(()) +} diff --git a/cargo-wef/src/commands/build.rs b/cargo-wef/src/commands/build.rs new file mode 100644 index 0000000..b56d556 --- /dev/null +++ b/cargo-wef/src/commands/build.rs @@ -0,0 +1,713 @@ +use std::{ + ffi::OsStr, + fs::File, + io::BufWriter, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{Context, Result}; +use askama::Template; +use cargo_metadata::{Metadata, MetadataCommand}; +use icns::IconFamily; +use image::GenericImageView; + +use crate::internal::{InfoPlist, add_cef_framework, add_helper}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BinaryKind { + Bin, + Example, +} + +#[derive(Debug)] +struct BinaryInfo { + metadata: serde_json::Value, + package_name: String, + package_path: PathBuf, + target_name: String, + kind: BinaryKind, + version: String, +} + +fn execute_path( + metadata: &Metadata, + target_dir: &Path, + package: Option<&str>, + bin: Option<&str>, + example: Option<&str>, +) -> Result<(PathBuf, BinaryInfo)> { + let packages = if let Some(package_name) = package { + vec![ + metadata + .workspace_packages() + .into_iter() + .find(|package| package.name.as_str() == package_name) + .ok_or_else(|| { + anyhow::anyhow!("No package `{}` found in the workspace", package_name) + })?, + ] + } else if metadata.workspace_default_members.is_available() { + metadata.workspace_default_packages() + } else { + metadata.workspace_packages() + }; + + let (package, target, binary_kind) = if let Some(bin_name) = bin { + packages + .iter() + .find_map(|package| { + package + .targets + .iter() + .find(|target| target.is_bin() && target.name == bin_name) + .map(|target| (*package, target, BinaryKind::Bin)) + }) + .ok_or_else(|| anyhow::anyhow!("no bin target named `{}`", bin_name))? + } else if let Some(example_name) = example { + packages + .iter() + .find_map(|package| { + package + .targets + .iter() + .find(|target| target.is_example() && target.name == example_name) + .map(|target| (*package, target, BinaryKind::Example)) + }) + .ok_or_else(|| anyhow::anyhow!("no example target named `{}`", example_name))? + } else { + let mut bin_targets = packages + .iter() + .flat_map(|package| { + package + .targets + .iter() + .filter_map(|target| target.is_bin().then_some((*package, target))) + }) + .collect::>(); + anyhow::ensure!(!bin_targets.is_empty(), "a bin target must be available"); + anyhow::ensure!(bin_targets.len() == 1, "could not determine which binary"); + let (package, target) = bin_targets.remove(0); + (package, target, BinaryKind::Bin) + }; + + let exec_path = match std::env::consts::OS { + "macos" => Ok(target_dir.join(&target.name)), + "windows" => Ok(target_dir.join(&target.name).with_extension("exe")), + "linux" => Ok(target_dir.join(&target.name)), + _ => Err(anyhow::anyhow!( + "Unsupported platform: {}", + std::env::consts::OS + )), + }; + Ok(( + exec_path?, + BinaryInfo { + metadata: package.metadata.clone(), + package_name: package.name.to_string(), + package_path: package + .manifest_path + .parent() + .unwrap() + .to_path_buf() + .into_std_path_buf(), + target_name: target.name.clone(), + kind: binary_kind, + version: package.version.to_string(), + }, + )) +} + +fn find_bundle_settings( + binary_info: &BinaryInfo, + bundle_type: Option<&str>, +) -> Result> { + let config = match binary_info.kind { + BinaryKind::Bin => { + let mut config = binary_info + .metadata + .pointer(&format!("/bundle/bin/{}", binary_info.target_name)); + if config.is_none() && binary_info.target_name == binary_info.package_name { + config = binary_info.metadata.pointer("/bundle"); + } + config + } + BinaryKind::Example => binary_info + .metadata + .pointer(&format!("/bundle/example/{}", binary_info.target_name)), + }; + + let config = if let Some(bundle_type) = bundle_type { + Some(config.and_then(|c| c.get(bundle_type)).ok_or_else(|| { + anyhow::anyhow!("No bundle settings found for type `{}`", bundle_type) + })?) + } else { + config + }; + + config + .map(|config| { + let mut config: InfoPlist = serde_json::from_value(config.clone())?; + if config.bundle_short_version.is_none() { + config.bundle_short_version = Some(binary_info.version.clone()); + } + Ok::<_, anyhow::Error>(config) + }) + .transpose() + .context("parse bundle settings") +} + +fn create_plist(binary_info: &BinaryInfo, bundle_type: Option<&str>) -> Result { + let plist = find_bundle_settings(binary_info, bundle_type)?.unwrap_or_else(|| { + println!("Bundle settings is not found, fallback to default settings"); + let mut plist = InfoPlist::new( + &binary_info.target_name, + format!("io.github.wef.{}", &binary_info.target_name), + ); + plist.bundle_short_version = Some(binary_info.version.clone()); + plist + }); + Ok(plist) +} + +fn create_icns_file( + package_path: &Path, + resources_dir: &Path, + plist: &mut InfoPlist, +) -> Result<()> { + if plist.icons.is_empty() { + return Ok(()); + } + + for icon_path in &plist.icons { + let icon_path = package_path.join(icon_path); + if icon_path.extension() == Some(OsStr::new("icns")) { + std::fs::create_dir(resources_dir)?; + + let target_path = resources_dir.join(&plist.name).with_extension("icns"); + std::fs::copy(&icon_path, &target_path).with_context(|| { + format!("copy {} to {}", icon_path.display(), target_path.display()) + })?; + plist.icon = Some(format!("{}.icns", plist.name)); + return Ok(()); + } + } + + let mut family = IconFamily::new(); + + fn make_icns_image(img: image::DynamicImage) -> Result { + let pixel_format = match img.color() { + image::ColorType::Rgba8 => icns::PixelFormat::RGBA, + image::ColorType::Rgb8 => icns::PixelFormat::RGB, + image::ColorType::La8 => icns::PixelFormat::GrayAlpha, + image::ColorType::L8 => icns::PixelFormat::Gray, + _ => { + anyhow::bail!("Unsupported image color type: {:?}", img.color()); + } + }; + Ok(icns::Image::from_data( + pixel_format, + img.width(), + img.height(), + img.into_bytes(), + )?) + } + + fn add_icon_to_family( + icon: image::DynamicImage, + density: u32, + family: &mut icns::IconFamily, + ) -> Result<()> { + // Try to add this image to the icon family. Ignore images whose sizes + // don't map to any ICNS icon type; print warnings and skip images that + // fail to encode. + match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) { + Some(icon_type) => { + if !family.has_icon_with_type(icon_type) { + let icon = make_icns_image(icon)?; + family.add_icon_with_type(&icon, icon_type)?; + } + Ok(()) + } + None => anyhow::bail!("No matching IconType"), + } + } + + fn is_retina(path: &Path) -> bool { + path.file_stem() + .and_then(OsStr::to_str) + .map(|stem| stem.ends_with("@2x")) + .unwrap_or(false) + } + + let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![]; + for icon_path in &plist.icons { + let icon_path = package_path.join(icon_path); + let icon = image::open(&icon_path) + .with_context(|| format!("load image {}", icon_path.display()))?; + let density = if is_retina(&icon_path) { 2 } else { 1 }; + let (w, h) = icon.dimensions(); + let orig_size = w.min(h); + let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32; + if orig_size > next_size_down { + images_to_resize.push((icon, next_size_down, density)); + } else { + add_icon_to_family(icon, density, &mut family)?; + } + } + + for (icon, next_size_down, density) in images_to_resize { + let icon = icon.resize_exact(next_size_down, next_size_down, image::imageops::Lanczos3); + add_icon_to_family(icon, density, &mut family)?; + } + + if !family.is_empty() { + std::fs::create_dir_all(resources_dir)?; + let icns_path = resources_dir.join(&plist.name).with_extension("icns"); + let icns_file = BufWriter::new(File::create(&icns_path)?); + family + .write(icns_file) + .with_context(|| format!("write icns file {}", icns_path.display()))?; + plist.icon = Some(format!("{}.icns", plist.name)); + return Ok(()); + } + + anyhow::bail!("No usable icon files found.") +} + +fn bundle_macos_app( + exec_path: &Path, + binary_info: BinaryInfo, + cef_root: &Path, + release: bool, + wef_version: Option<&str>, + wef_path: Option<&Path>, + bundle_type: Option<&str>, +) -> Result { + let filename = exec_path.file_name().unwrap(); + let app_path = exec_path + .parent() + .unwrap() + .join(format!("{}.app", filename.to_string_lossy())); + + let macos_path = app_path.join("Contents").join("MacOS"); + std::fs::create_dir_all(&macos_path).context("create app directory")?; + + std::fs::copy(exec_path, macos_path.join(filename)).context("copy binary to app bundle")?; + + let plist_path = app_path.join("Contents").join("Info.plist"); + let mut plist = create_plist(&binary_info, bundle_type)?; + + println!("Create ICNS file..."); + let resources_path = app_path.join("Contents").join("Resources"); + create_icns_file(&binary_info.package_path, &resources_path, &mut plist) + .context("create ICNS file")?; + + plist + .write_into(&mut File::create(&plist_path)?) + .with_context(|| format!("create file at {}", plist_path.display()))?; + + println!("Add CEF Framework..."); + add_cef_framework(cef_root, &app_path, release, false)?; + println!("Add Helper Processes..."); + add_helper(&app_path, wef_version, wef_path, release, false)?; + Ok(macos_path.join(filename)) +} + +pub(crate) fn build( + package: Option, + bin: Option, + example: Option, + release: bool, + wef_version: Option<&str>, + wef_path: Option<&Path>, + bundle_type: Option<&str>, +) -> Result { + let cef_root = crate::internal::find_cef_root(); + println!("Using CEF_ROOT: {}", cef_root.display()); + + let metadata = MetadataCommand::new() + .current_dir(std::env::current_dir().unwrap()) + .exec()?; + + let mut command = Command::new("cargo"); + + command.arg("build"); + + if let Some(package) = &package { + command.arg("--package").arg(package); + } + + if let Some(bin) = &bin { + command.arg("--bin").arg(bin); + } + + if let Some(example) = &example { + command.arg("--example").arg(example); + } + + if release { + command.arg("--release"); + } + + anyhow::ensure!(command.status()?.success(), "failed to build the project"); + + let target_dir = metadata + .target_directory + .join(if release { "release" } else { "debug" }); + + match std::env::consts::OS { + "macos" => { + let (exec_path, binary_info) = execute_path( + &metadata, + target_dir.as_std_path(), + package.as_deref(), + bin.as_deref(), + example.as_deref(), + )?; + bundle_macos_app( + &exec_path, + binary_info, + &cef_root, + release, + wef_version, + wef_path, + bundle_type, + ) + } + "windows" | "linux" => { + anyhow::ensure!( + bundle_type.is_none(), + "bundle-type argument is used only on macOS" + ); + + add_cef_framework(&cef_root, target_dir.as_std_path(), release, false)?; + execute_path( + &metadata, + target_dir.as_std_path(), + package.as_deref(), + bin.as_deref(), + example.as_deref(), + ) + .map(|(path, _)| path) + } + _ => { + anyhow::bail!("Unsupported platform: {}", std::env::consts::OS); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn package_bin() { + let metadata = MetadataCommand::new() + .current_dir("tests/package_bin") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + None, + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "package-bin"); + assert_eq!(binary_info.version, "0.3.0"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.0".to_string()), + ..InfoPlist::new( + "test-package-bin", + "io.github.longbridge.wef.tests.package-bin" + ) + }) + ); + } + + #[test] + fn bin() { + let metadata = MetadataCommand::new() + .current_dir("tests/bin") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + Some("bin1"), + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "bin1"); + assert_eq!(binary_info.version, "0.5.0"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.5.0".to_string()), + ..InfoPlist::new("test-bin", "io.github.longbridge.wef.tests.bin") + }) + ); + } + + #[test] + fn example() { + let metadata = MetadataCommand::new() + .current_dir("tests/example") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + None, + Some("example1"), + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Example); + assert_eq!(binary_info.target_name, "example1"); + assert_eq!(binary_info.version, "0.3.2"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.2".to_string()), + ..InfoPlist::new("test-example", "io.github.longbridge.wef.tests.example") + }) + ); + } + + #[test] + fn workspace_package_bin() { + let metadata = MetadataCommand::new() + .current_dir("tests/workspace") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + Some("package-bin"), + None, + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "package-bin"); + assert_eq!(binary_info.version, "0.3.0"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.0".to_string()), + ..InfoPlist::new( + "test-package-bin", + "io.github.longbridge.wef.tests.package-bin" + ) + }) + ); + } + + #[test] + fn workspace_bin() { + let metadata = MetadataCommand::new() + .current_dir("tests/workspace") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + Some("pkg-bin"), + Some("bin1"), + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "bin1"); + assert_eq!(binary_info.version, "0.5.0"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.5.0".to_string()), + ..InfoPlist::new("test-bin", "io.github.longbridge.wef.tests.bin") + }) + ); + } + + #[test] + fn workspace_bin_without_package() { + let metadata = MetadataCommand::new() + .current_dir("tests/workspace") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + Some("bin1"), + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "bin1"); + assert_eq!(binary_info.version, "0.5.0"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.5.0".to_string()), + ..InfoPlist::new("test-bin", "io.github.longbridge.wef.tests.bin") + }) + ); + } + + #[test] + fn workspace_example() { + let metadata = MetadataCommand::new() + .current_dir("tests/workspace") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + Some("pkg-example"), + None, + Some("example1"), + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Example); + assert_eq!(binary_info.target_name, "example1"); + assert_eq!(binary_info.version, "0.3.2"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.2".to_string()), + ..InfoPlist::new("test-example", "io.github.longbridge.wef.tests.example") + }) + ); + } + + #[test] + fn workspace_example_without_package() { + let metadata = MetadataCommand::new() + .current_dir("tests/workspace") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + None, + Some("example1"), + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Example); + assert_eq!(binary_info.target_name, "example1"); + assert_eq!(binary_info.version, "0.3.2"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.2".to_string()), + ..InfoPlist::new("test-example", "io.github.longbridge.wef.tests.example") + }) + ); + } + + #[test] + fn workspace_default_members() { + let metadata = MetadataCommand::new() + .current_dir("tests/default_members") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + Some("bin2"), + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.package_name, "bin2"); + assert_eq!(binary_info.target_name, "bin2"); + assert_eq!(binary_info.version, "0.5.1"); + + assert_eq!( + find_bundle_settings(&binary_info, None).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.5.1".to_string()), + ..InfoPlist::new("test-bin2", "io.github.longbridge.wef.tests.bin2") + }) + ); + + let err = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + Some("bin1"), + None, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "no bin target named `bin1`"); + } + + #[test] + fn bundle_type() { + let metadata = MetadataCommand::new() + .current_dir("tests/package_bin") + .exec() + .unwrap(); + + let (_, binary_info) = execute_path( + &metadata, + metadata.target_directory.as_std_path(), + None, + None, + None, + ) + .unwrap(); + assert_eq!(binary_info.kind, BinaryKind::Bin); + assert_eq!(binary_info.target_name, "package-bin"); + assert_eq!(binary_info.version, "0.3.0"); + + assert_eq!( + find_bundle_settings(&binary_info, Some("preview")).unwrap(), + Some(InfoPlist { + category: Some("Utility".to_string()), + bundle_short_version: Some("0.3.0".to_string()), + ..InfoPlist::new( + "test-package-bin-preview", + "io.github.longbridge.wef.tests.package-bin.preview" + ) + }) + ); + } +} diff --git a/cargo-wef/src/commands/init.rs b/cargo-wef/src/commands/init.rs new file mode 100644 index 0000000..7d3f7ed --- /dev/null +++ b/cargo-wef/src/commands/init.rs @@ -0,0 +1,85 @@ +use std::path::PathBuf; + +use anyhow::Result; +use indicatif::{ProgressBar, ProgressStyle}; + +use crate::internal::{CefBuildsPlatform, DownloadCefCallback}; + +#[derive(Debug, Default)] +struct CmdDownloadCallback { + download_progress: Option, + extract_progress: Option, +} + +pub(crate) fn init( + path: Option, + version: String, + platform: CefBuildsPlatform, + force: bool, +) -> Result<()> { + let path = path.unwrap_or_else(|| dirs::home_dir().expect("get home directory").join(".cef")); + crate::internal::download_cef( + &path, + &version, + platform, + force, + CmdDownloadCallback::default(), + )?; + println!("Set environment variable CEF_ROOT={}", path.display()); + Ok(()) +} + +fn create_download_progress_bar() -> ProgressBar { + ProgressBar::new_spinner().with_style( + ProgressStyle::default_spinner() + .progress_chars("#>-" ) + .template("Downloading CEF {spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}) {eta}").unwrap() + ) +} + +fn create_extract_progress_bar() -> ProgressBar { + ProgressBar::new_spinner().with_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .unwrap(), + ) +} + +impl DownloadCefCallback for CmdDownloadCallback { + fn download_start(&mut self, total_size: u64) { + self.download_progress = Some(create_download_progress_bar()); + if let Some(pb) = &self.download_progress { + pb.set_length(total_size); + } + } + + fn download_progress(&mut self, downloaded: u64) { + if let Some(pb) = &self.download_progress { + pb.set_position(downloaded); + } + } + + fn download_end(&mut self) { + if let Some(pb) = self.download_progress.take() { + pb.finish_and_clear(); + println!("Download complete"); + } + } + + fn extract_start(&mut self) { + self.extract_progress = Some(create_extract_progress_bar()); + } + + fn extract_file(&mut self, path: &str) { + if let Some(pb) = &self.extract_progress { + pb.set_message(format!("Extracting {}", path)); + } + } + + fn extract_end(&mut self) { + if let Some(pb) = self.extract_progress.take() { + pb.finish_and_clear(); + println!("Extraction complete"); + } + } +} diff --git a/cargo-wef/src/commands/mod.rs b/cargo-wef/src/commands/mod.rs new file mode 100644 index 0000000..3316bab --- /dev/null +++ b/cargo-wef/src/commands/mod.rs @@ -0,0 +1,9 @@ +mod add_framework; +mod build; +mod init; +mod run; + +pub(crate) use add_framework::add_framework; +pub(crate) use build::build; +pub(crate) use init::init; +pub(crate) use run::run; diff --git a/cargo-wef/src/commands/run.rs b/cargo-wef/src/commands/run.rs new file mode 100644 index 0000000..a916cf5 --- /dev/null +++ b/cargo-wef/src/commands/run.rs @@ -0,0 +1,18 @@ +use std::{path::Path, process::Command}; + +use anyhow::Result; + +pub(crate) fn run( + package: Option, + bin: Option, + example: Option, + release: bool, + wef_version: Option<&str>, + wef_path: Option<&Path>, + args: Vec, +) -> Result<()> { + let exec_path = + crate::commands::build(package, bin, example, release, wef_version, wef_path, None)?; + Command::new(&exec_path).args(args).status()?; + Ok(()) +} diff --git a/cargo-wef/src/internal/add_cef_framework.rs b/cargo-wef/src/internal/add_cef_framework.rs new file mode 100644 index 0000000..641c273 --- /dev/null +++ b/cargo-wef/src/internal/add_cef_framework.rs @@ -0,0 +1,214 @@ +use std::path::Path; + +use anyhow::{Context, Result}; + +pub(crate) fn add_cef_framework( + cef_root: &Path, + app_path: &Path, + release: bool, + force: bool, +) -> Result<()> { + match std::env::consts::OS { + "macos" => add_cef_framework_macos(cef_root, app_path, release, force), + "windows" => add_cef_framework_windows(cef_root, app_path, release, force), + "linux" => add_cef_framework_linux(cef_root, app_path, release, force), + _ => { + anyhow::bail!("Unsupported platform: {}", std::env::consts::OS); + } + } +} + +fn add_cef_framework_macos( + cef_root: &Path, + app_path: &Path, + release: bool, + force: bool, +) -> Result<()> { + let contents_path = app_path.join("Contents"); + anyhow::ensure!( + contents_path.exists(), + "{} is not a valid MacOS app.", + app_path.display() + ); + + // create frameworks directory + let frameworks_path = contents_path.join("Frameworks"); + std::fs::create_dir_all(&frameworks_path).with_context(|| { + format!( + "create frameworks directory at {}", + frameworks_path.display() + ) + })?; + + // copy CEF framework + let cef_framework_path = cef_root + .join(if !release { "Debug" } else { "Release" }) + .join("Chromium Embedded Framework.framework"); + + if !force + && frameworks_path + .join("Chromium Embedded Framework.framework") + .exists() + { + return Ok(()); + } + + fs_extra::dir::copy( + &cef_framework_path, + &frameworks_path, + &fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + copy_inside: false, + content_only: false, + ..Default::default() + }, + ) + .with_context(|| { + format!( + "copy CEF framework from {} to {}", + cef_framework_path.display(), + frameworks_path.display() + ) + })?; + + Ok(()) +} + +fn add_cef_framework_windows( + cef_root: &Path, + app_path: &Path, + release: bool, + force: bool, +) -> Result<()> { + let files = [ + "chrome_elf.dll", + "d3dcompiler_47.dll", + "dxcompiler.dll", + "dxil.dll", + "libcef.dll", + "libEGL.dll", + "libGLESv2.dll", + "v8_context_snapshot.bin", + "vk_swiftshader.dll", + "vk_swiftshader_icd.json", + "vulkan-1.dll", + ]; + + let resources = [ + "chrome_100_percent.pak", + "chrome_200_percent.pak", + "icudtl.dat", + "resources.pak", + "locales", + ]; + + if !force + && files + .iter() + .all(|filename| app_path.join(filename).exists()) + && resources + .iter() + .all(|filename| app_path.join(filename).exists()) + { + return Ok(()); + } + + for filename in files { + let src_path = cef_root + .join(if !release { "Debug" } else { "Release" }) + .join(filename); + let dst_path = app_path.join(filename); + std::fs::copy(src_path, dst_path) + .with_context(|| format!("copy {} to {}", filename, app_path.display()))?; + } + + let resources_src_path = cef_root.join("Resources"); + fs_extra::dir::copy( + &resources_src_path, + app_path, + &fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + copy_inside: false, + content_only: true, + ..Default::default() + }, + ) + .with_context(|| { + format!( + "copy CEF Resources from {} to {}", + resources_src_path.display(), + app_path.display() + ) + })?; + + Ok(()) +} + +fn add_cef_framework_linux( + cef_root: &Path, + app_path: &Path, + release: bool, + force: bool, +) -> Result<()> { + let files = [ + "libcef.so", + "libEGL.so", + "libGLESv2.so", + "libvk_swiftshader.so", + "libvulkan.so.1", + "v8_context_snapshot.bin", + "vk_swiftshader_icd.json", + ]; + + let resources = [ + "chrome_100_percent.pak", + "chrome_200_percent.pak", + "icudtl.dat", + "resources.pak", + "locales", + ]; + + if !force + && files + .iter() + .all(|filename| app_path.join(filename).exists()) + && resources + .iter() + .all(|filename| app_path.join(filename).exists()) + { + return Ok(()); + } + + for filename in files { + let src_path = cef_root + .join(if !release { "Debug" } else { "Release" }) + .join(filename); + let dst_path = app_path.join(filename); + std::fs::copy(src_path, dst_path) + .with_context(|| format!("copy {} to {}", filename, app_path.display()))?; + } + + let resources_src_path = cef_root.join("Resources"); + fs_extra::dir::copy( + &resources_src_path, + app_path, + &fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + copy_inside: false, + content_only: true, + ..Default::default() + }, + ) + .with_context(|| { + format!( + "copy CEF Resources from {} to {}", + resources_src_path.display(), + app_path.display() + ) + })?; + + Ok(()) +} diff --git a/cargo-wef/src/internal/add_helper.rs b/cargo-wef/src/internal/add_helper.rs new file mode 100644 index 0000000..d3019b0 --- /dev/null +++ b/cargo-wef/src/internal/add_helper.rs @@ -0,0 +1,308 @@ +use std::{fs::File, path::Path, process::Command}; + +use anyhow::{Context, Result}; +use askama::Template; +use serde::Deserialize; + +use crate::internal::InfoPlist; + +/// ```askama +/// [package] +/// name = "helper" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// {% if let Some(wef_version) = wef_version %} +/// wef = "{{ wef_version }}" +/// {% endif %} +/// {% if let Some(wef_path) = wef_path %} +/// wef = { path = "{{ wef_path }}" } +/// {% endif %} +/// ``` +#[derive(Template)] +#[template(ext = "txt", in_doc = true)] +struct TemplateCargoToml { + wef_version: Option, + wef_path: Option, +} + +const MAIN_RS: &str = r#" +fn main() -> Result<(), Box> { + let _ = wef::SandboxContext::new(); + let _ = wef::FrameworkLoader::load_in_helper()?; + wef::exec_process()?; + Ok(()) +} +"#; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HelperKind { + Main, + Alerts, + Gpu, + Plugin, + Renderer, +} + +impl HelperKind { + const ALL: &[HelperKind] = &[ + HelperKind::Main, + HelperKind::Alerts, + HelperKind::Gpu, + HelperKind::Plugin, + HelperKind::Renderer, + ]; + + fn bundle_name(&self, bundle_name: &str) -> String { + let helper_name = match self { + HelperKind::Main => "Helper", + HelperKind::Alerts => "Helper (Alerts)", + HelperKind::Gpu => "Helper (GPU)", + HelperKind::Plugin => "Helper (Plugin)", + HelperKind::Renderer => "Helper (Renderer)", + }; + format!("{} {}", bundle_name, helper_name) + } + + fn bundle_identifier(&self, bundle_identifier: &str) -> String { + match self { + HelperKind::Main => format!("{}.helper", bundle_identifier), + HelperKind::Alerts => format!("{}.helper.alerts", bundle_identifier), + HelperKind::Gpu => format!("{}.helper.gpu", bundle_identifier), + HelperKind::Plugin => format!("{}.helper.plugin", bundle_identifier), + HelperKind::Renderer => format!("{}.helper.renderer", bundle_identifier), + } + } +} + +fn query_wef_max_stable_version() -> Result { + #[derive(Debug, Deserialize)] + struct CrateInfo { + max_stable_version: String, + } + + #[derive(Debug, Deserialize)] + struct Response { + #[serde(rename = "crate")] + crate_: CrateInfo, + } + + let client = reqwest::blocking::Client::new(); + Ok(client + .get("https://crates.io/api/v1/crates/wef") + .header("user-agent", "curl/8.7.1") + .send()? + .error_for_status()? + .json::()? + .crate_ + .max_stable_version) +} + +fn create_helper_bin( + wef_version: Option<&str>, + wef_path: Option<&Path>, + release: bool, + callback: F, +) -> Result +where + F: FnOnce(&Path) -> Result, +{ + let proj_dir = dirs::home_dir() + .expect("home directory not found") + .join(".wef-tool") + .join("helper"); + std::fs::create_dir_all(proj_dir.as_path()) + .with_context(|| format!("create project directory at {}", proj_dir.display()))?; + + // query wef version + let (wef_version, wef_path) = if let Some(wef_path) = &wef_path { + (None, Some(wef_path.display().to_string())) + } else { + match wef_version { + Some(version) => (Some(version.to_string()), None), + None => ( + Some( + query_wef_max_stable_version() + .with_context(|| "query latest stable version of Wef from crates.io")?, + ), + None, + ), + } + }; + + // create Cargo.toml + let cargo_toml_path = proj_dir.join("Cargo.toml"); + let mut cargo_toml_file = File::create(&cargo_toml_path) + .with_context(|| format!("create Cargo.toml at {}", cargo_toml_path.display()))?; + + TemplateCargoToml { + wef_version, + wef_path, + } + .write_into(&mut cargo_toml_file) + .with_context(|| format!("create file at {}", cargo_toml_path.display()))?; + + // create src/main.rs + let src_path = proj_dir.join("src"); + std::fs::create_dir_all(&src_path) + .with_context(|| format!("create directory at {}", src_path.display()))?; + + let main_rs_path = proj_dir.join("src").join("main.rs"); + std::fs::write(&main_rs_path, MAIN_RS) + .with_context(|| format!("create file at {}", main_rs_path.display()))?; + + // build + let mut command = Command::new("cargo"); + + command + .arg("build") + .arg("--target-dir") + .arg(proj_dir.join("target")); + + if release { + command.arg("--release"); + } + + let output = command + .current_dir(&proj_dir) + .output() + .with_context(|| format!("run cargo build in {}", proj_dir.display()))?; + + anyhow::ensure!( + output.status.success(), + "failed to compile helper binary:\n{}\n", + String::from_utf8_lossy(&output.stderr) + ); + + let target_path = proj_dir + .join("target") + .join(if !release { "debug" } else { "release" }) + .join("helper"); + callback(&target_path) +} + +#[derive(Debug, Deserialize)] +struct BundleInfo { + #[serde(rename = "CFBundleName")] + bundle_name: String, + #[serde(rename = "CFBundleIdentifier")] + bundle_identifier: String, + #[serde(rename = "CFBundleExecutable")] + executable_name: Option, +} + +fn read_bundle_info(path: &Path) -> Result { + plist::from_file(path).map_err(Into::into) +} + +fn create_helper_app( + app_path: &Path, + kind: HelperKind, + bundle_info: &BundleInfo, + bin_path: &Path, +) -> Result<()> { + let frameworks_path = app_path.join("Contents").join("Frameworks"); + + // create frameworks directory + std::fs::create_dir_all(&frameworks_path).with_context(|| { + format!( + "create frameworks directory at {}", + frameworks_path.display() + ) + })?; + + let helper_app_path = frameworks_path.join(format!( + "{}.app", + kind.bundle_name(&bundle_info.bundle_name) + )); + + // create app directory + std::fs::create_dir_all(&helper_app_path).with_context(|| { + format!( + "create helper app directory at {}", + helper_app_path.display() + ) + })?; + + // create Contents directory + let contents_path = helper_app_path.join("Contents"); + std::fs::create_dir_all(&contents_path) + .with_context(|| format!("create directory at {}", contents_path.display()))?; + + // create plist + let plist_path = contents_path.join("Info.plist"); + let mut plist = InfoPlist::new( + kind.bundle_name(&bundle_info.bundle_name), + kind.bundle_identifier(&bundle_info.bundle_identifier), + ); + plist.executable_name = Some( + bundle_info + .executable_name + .as_ref() + .map(|executable_name| kind.bundle_name(executable_name)) + .unwrap_or_else(|| kind.bundle_name(&bundle_info.bundle_name)), + ); + plist.agent_app = true; + + plist + .write_into(&mut File::create(&plist_path)?) + .with_context(|| format!("create file at {}", plist_path.display()))?; + + // create MacOS directory + let macos_path = contents_path.join("MacOS"); + std::fs::create_dir_all(&macos_path) + .with_context(|| format!("create directory at {}", macos_path.display()))?; + + // copy binary + let target_path = macos_path.join(kind.bundle_name(&bundle_info.bundle_name)); + std::fs::copy(bin_path, &target_path).with_context(|| { + format!( + "copy binary from {} to {}", + bin_path.display(), + target_path.display() + ) + })?; + + Ok(()) +} + +pub(crate) fn add_helper( + app_path: &Path, + wef_version: Option<&str>, + wef_path: Option<&Path>, + release: bool, + force: bool, +) -> Result<()> { + let info_path = app_path.join("Contents").join("Info.plist"); + + anyhow::ensure!( + info_path.exists(), + "{} is not a valid Macos app.", + app_path.display() + ); + + let bundle_info = read_bundle_info(&info_path) + .with_context(|| format!("read bundle info from {}", info_path.display()))?; + + if !force + && HelperKind::ALL.iter().all(|kind| { + let helper_path = app_path.join("Contents").join("Frameworks").join(format!( + "{}.app", + kind.bundle_name(&bundle_info.bundle_name) + )); + helper_path.exists() + }) + { + return Ok(()); + } + + create_helper_bin(wef_version, wef_path, release, |path| { + for kind in HelperKind::ALL { + create_helper_app(app_path, *kind, &bundle_info, path)?; + } + Ok(()) + })?; + + Ok(()) +} diff --git a/cargo-wef/src/internal/cef_platform.rs b/cargo-wef/src/internal/cef_platform.rs new file mode 100644 index 0000000..d700331 --- /dev/null +++ b/cargo-wef/src/internal/cef_platform.rs @@ -0,0 +1,47 @@ +use clap::ValueEnum; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[allow(non_camel_case_types)] +pub enum CefBuildsPlatform { + Auto, + Windows_x64, + Macos_x64, + Macos_arm64, + Linux_x64, +} + +pub(crate) const DEFAULT_CEF_VERSION: &str = "137.0.10+g7e14fe1+chromium-137.0.7151.69"; + +impl CefBuildsPlatform { + fn arch(&self) -> Option<&'static str> { + match self { + CefBuildsPlatform::Auto => match (std::env::consts::OS, std::env::consts::ARCH) { + ("windows", "x86_64") => CefBuildsPlatform::Windows_x64.arch(), + ("macos", "x86_64") => CefBuildsPlatform::Macos_x64.arch(), + ("macos", "aarch64") => CefBuildsPlatform::Macos_arm64.arch(), + ("linux", "x86_64") => CefBuildsPlatform::Linux_x64.arch(), + _ => None, + }, + CefBuildsPlatform::Windows_x64 => Some("windows64"), + CefBuildsPlatform::Macos_x64 => Some("macosx64"), + CefBuildsPlatform::Macos_arm64 => Some("macosarm64"), + CefBuildsPlatform::Linux_x64 => Some("linux64"), + } + } + + pub(crate) fn download_url(&self, version: &str) -> Option { + Some(format!( + "https://cef-builds.spotifycdn.com/cef_binary_{version}_{arch}.tar.bz2", + version = version, + arch = self.arch()? + )) + } + + pub(crate) fn root_dir_name(&self, version: &str) -> Option { + Some(format!( + "cef_binary_{version}_{arch}", + version = version, + arch = self.arch()? + )) + } +} diff --git a/cargo-wef/src/internal/download_cef.rs b/cargo-wef/src/internal/download_cef.rs new file mode 100644 index 0000000..c6ed6cb --- /dev/null +++ b/cargo-wef/src/internal/download_cef.rs @@ -0,0 +1,138 @@ +use std::{ + fs::{self, File}, + io::{Read, Write}, + path::Path, +}; + +use anyhow::{Context, Result}; +use reqwest::blocking::Client; +use tar::EntryType; + +use crate::internal::CefBuildsPlatform; + +pub(crate) fn download_cef( + path: &Path, + version: &str, + platform: CefBuildsPlatform, + force: bool, + mut callback: impl DownloadCefCallback, +) -> Result<()> { + if !force && path.exists() { + return Ok(()); + } + + let url = platform + .download_url(version) + .ok_or_else(|| anyhow::anyhow!("unsupported platform: {:?}", platform))?; + + // Download with progress + let client = Client::new(); + let response = client.get(&url).send().context("download CEF")?; + + let total_size = response.content_length().unwrap_or(0); + + let tmpdir_path = tempfile::tempdir().context("create temporary directory")?; + let archive_path = tmpdir_path.path().join("cef.tar.bz2"); + + callback.download_start(total_size); + download_file(&url, &archive_path, &mut callback)?; + callback.download_end(); + + // Create the target directory if it doesn't exist + fs::create_dir_all(path).context("create target directory")?; + + // Extract with progress + callback.extract_start(); + extract_archive( + &archive_path, + path, + &platform.root_dir_name(version).unwrap(), + &mut callback, + ) + .context("extract CEF archive")?; + callback.extract_end(); + + Ok(()) +} + +fn download_file(url: &str, path: &Path, callback: &mut impl DownloadCefCallback) -> Result<()> { + let client = Client::new(); + + let mut response = client + .get(url) + .send() + .and_then(|resp| resp.error_for_status()) + .with_context(|| format!("download CEF from {}", url))?; + + let mut downloaded: u64 = 0; + let mut buffer = [0; 8192]; + + let mut file = File::create(path).with_context(|| format!("create file {}", path.display()))?; + + while let Ok(bytes_read) = response.read(&mut buffer) { + if bytes_read == 0 { + break; + } + file.write_all(&buffer[..bytes_read]) + .with_context(|| format!("write to file {}", path.display()))?; + downloaded += bytes_read as u64; + callback.download_progress(downloaded); + } + + Ok(()) +} + +fn extract_archive( + archive_path: &Path, + target_dir: &Path, + root_dir_name: &str, + callback: &mut impl DownloadCefCallback, +) -> Result<()> { + let tar_bz2 = File::open(archive_path) + .with_context(|| format!("open archive file {}", archive_path.display()))?; + + let bz2 = bzip2::read::BzDecoder::new(tar_bz2); + let mut archive = tar::Archive::new(bz2); + + let entries = archive.entries().context("read entries from archive")?; + + for res in entries { + let mut entry = res.context("get entry from archive")?; + + if entry.header().entry_type() != EntryType::Regular { + continue; + } + + let entry_path = entry.path().unwrap().to_path_buf(); + let filepath = target_dir.join(entry_path.strip_prefix(root_dir_name).unwrap()); + let parent_path = filepath.parent().unwrap(); + std::fs::create_dir_all(parent_path) + .with_context(|| format!("create directory for entry {}", parent_path.display()))?; + + entry.unpack(&filepath).with_context(|| { + format!( + "unpack entry {} to {}", + entry_path.display(), + filepath.display() + ) + })?; + callback.extract_file(&entry_path.display().to_string()); + } + + Ok(()) +} + +#[allow(unused_variables)] +pub(crate) trait DownloadCefCallback { + fn download_start(&mut self, total: u64) {} + + fn download_progress(&mut self, downloaded: u64) {} + + fn download_end(&mut self) {} + + fn extract_start(&mut self) {} + + fn extract_file(&mut self, path: &str) {} + + fn extract_end(&mut self) {} +} diff --git a/cargo-wef/src/internal/find_cef_root.rs b/cargo-wef/src/internal/find_cef_root.rs new file mode 100644 index 0000000..36da777 --- /dev/null +++ b/cargo-wef/src/internal/find_cef_root.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +pub(crate) fn find_cef_root() -> PathBuf { + if let Ok(cef_root) = std::env::var("CEF_ROOT") { + cef_root.into() + } else { + dirs::home_dir().expect("get home directory").join(".cef") + } +} diff --git a/cargo-wef/src/internal/mod.rs b/cargo-wef/src/internal/mod.rs new file mode 100644 index 0000000..636db43 --- /dev/null +++ b/cargo-wef/src/internal/mod.rs @@ -0,0 +1,13 @@ +mod add_cef_framework; +mod add_helper; +mod cef_platform; +mod download_cef; +mod find_cef_root; +mod plist; + +pub(crate) use add_cef_framework::add_cef_framework; +pub(crate) use add_helper::add_helper; +pub(crate) use cef_platform::{CefBuildsPlatform, DEFAULT_CEF_VERSION}; +pub(crate) use download_cef::{DownloadCefCallback, download_cef}; +pub(crate) use find_cef_root::find_cef_root; +pub(crate) use plist::InfoPlist; diff --git a/cargo-wef/src/internal/plist.rs b/cargo-wef/src/internal/plist.rs new file mode 100644 index 0000000..16c65aa --- /dev/null +++ b/cargo-wef/src/internal/plist.rs @@ -0,0 +1,122 @@ +use askama::Template; +use serde::Deserialize; + +/// ```askama +/// +/// +/// +/// +/// CFBundleDevelopmentRegion +/// {% if let Some(region) = region %} +/// {{ region }} +/// {% else %} +/// en +/// {% endif %} +/// CFBundleDisplayName +/// {% if let Some(display_name) = display_name %} +/// {{ display_name | e }} +/// {% else %} +/// {{ name | e }} +/// {% endif %} +/// CFBundleExecutable +/// {% if let Some(executable_name) = executable_name %} +/// {{ executable_name | e }} +/// {% else %} +/// {{ name | e }} +/// {% endif %} +/// CFBundleIdentifier +/// {{ identifier | e }} +/// CFBundleInfoDictionaryVersion +/// 6.0 +/// CFBundleName +/// {{ name | e }} +/// CFBundlePackageType +/// APPL +/// CFBundleVersion +/// {% if let Some(bundle_version) = bundle_version %} +/// {{ bundle_version | e }} +/// {% else %} +/// +/// {% endif %} +/// CFBundleShortVersionString +/// {% if let Some(bundle_short_version) = bundle_short_version %} +/// {{ bundle_short_version | e }} +/// {% else %} +/// +/// {% endif %} +/// {% if let Some(category) = category %} +/// LSApplicationCategoryType +/// {{ category | e }} +/// {% endif %} +/// {% if let Some(icon) = icon %} +/// CFBundleIconFile +/// {{ icon | e }} +/// {% endif %} +/// {% if agent_app %} +/// LSUIElement +/// 1 +/// {% endif %} +/// {% if let Some(minimum_system_version) = minimum_system_version %} +/// LSMinimumSystemVersion +/// {{ minimum_system_version | e }} +/// {% endif %} +/// {% if url_schemes.len() > 0 %} +/// CFBundleURLTypes +/// +/// +/// CFBundleURLName +/// {{ name | e }} +/// CFBundleTypeRole +/// Viewer +/// CFBundleURLSchemes +/// +/// {% for scheme in url_schemes %} +/// {{ scheme | e }} +/// {% endfor %} +/// +/// +/// +/// {% endif %} +/// +/// +/// ``` +#[derive(Debug, Template, Deserialize, PartialEq, Eq)] +#[template(ext = "xml", in_doc = true)] +pub(crate) struct InfoPlist { + pub(crate) name: String, + pub(crate) identifier: String, + pub(crate) display_name: Option, + pub(crate) executable_name: Option, + pub(crate) region: Option, + pub(crate) bundle_version: Option, + pub(crate) bundle_short_version: Option, + #[serde(default)] + pub(crate) icons: Vec, + pub(crate) icon: Option, + pub(crate) category: Option, + pub(crate) minimum_system_version: Option, + #[serde(default)] + pub(crate) url_schemes: Vec, + #[serde(default)] + pub(crate) agent_app: bool, +} + +impl InfoPlist { + pub(crate) fn new(name: impl Into, identifier: impl Into) -> Self { + Self { + name: name.into(), + identifier: identifier.into(), + display_name: None, + executable_name: None, + region: None, + bundle_version: None, + bundle_short_version: None, + icons: vec![], + icon: None, + category: None, + minimum_system_version: None, + url_schemes: vec![], + agent_app: false, + } + } +} diff --git a/cargo-wef/src/main.rs b/cargo-wef/src/main.rs new file mode 100644 index 0000000..1dd55ef --- /dev/null +++ b/cargo-wef/src/main.rs @@ -0,0 +1,188 @@ +mod commands; +mod internal; + +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +use crate::internal::{CefBuildsPlatform, DEFAULT_CEF_VERSION}; + +#[derive(Subcommand)] +enum Commands { + /// Download CEF framework + Init { + /// Target path + path: Option, + /// CEF version + #[clap(long, default_value = DEFAULT_CEF_VERSION)] + version: String, + /// Platform + #[clap(long, default_value = "auto")] + platform: CefBuildsPlatform, + /// Force download even if the file already exists + #[clap(long, short, default_value_t = false)] + force: bool, + }, + /// Compile a local package and all of its dependencies + Build { + /// Package to build (see `cargo help pkgid`) + #[clap(long, short, value_name = "SPEC")] + package: Option, + /// Build only the specified binary + #[clap(long, value_name = "NAME")] + bin: Option, + /// Build only the specified example + #[clap(long, value_name = "NAME")] + example: Option, + /// Build artifacts in release mode, with optimizations + #[clap(long, short)] + release: bool, + /// Use the specified Wef version + /// + /// If not specified, use the latest version + #[clap(long)] + wef_version: Option, + /// Specify the source code path of the local Wef library instead of the + /// published version + #[clap(long)] + wef_path: Option, + /// Specify the bundle type for the MacOS application + bundle_type: Option, + }, + /// Run a binary or example of the local package + Run { + /// Package to build (see `cargo help pkgid`) + #[clap(long, short, value_name = "SPEC")] + package: Option, + /// Build only the specified binary + #[clap(long, value_name = "NAME")] + bin: Option, + /// Build only the specified example + #[clap(long, value_name = "NAME")] + example: Option, + /// Build artifacts in release mode, with optimizations + #[clap(long, short)] + release: bool, + /// Use the specified Wef version + /// + /// If not specified, use the latest version + #[clap(long)] + wef_version: Option, + /// Specify the source code path of the local Wef library instead of the + /// published version + #[clap(long)] + wef_path: Option, + #[arg(last = true)] + args: Vec, + }, + /// Add CEF framework to the application + AddFramework { + /// Target app path + app_path: PathBuf, + /// Build artifacts in release mode, with optimizations + #[clap(long, short)] + release: bool, + /// Force adding the framework even if it already exists + #[clap(long, short, default_value_t = false)] + force: bool, + /// Use the specified Wef version + /// + /// If not specified, use the latest version + #[clap(long)] + wef_version: Option, + /// Specify the source code path of the local Wef library instead of the + /// published version + #[clap(long)] + wef_path: Option, + }, +} + +/// Wef CLI tool +#[derive(Parser)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +#[clap(version, about)] +struct Cli { + #[command(subcommand)] + wef: WefCommands, +} + +#[derive(Subcommand)] +enum WefCommands { + Wef { + #[command(subcommand)] + commands: Commands, + }, +} + +fn main() { + let cli = Cli::parse(); + + let res = match cli.wef { + WefCommands::Wef { + commands: + Commands::Init { + path, + version, + platform, + force, + }, + } => commands::init(path, version, platform, force), + WefCommands::Wef { + commands: + Commands::Build { + package, + bin, + example, + release, + wef_version, + wef_path, + bundle_type, + }, + } => commands::build( + package, + bin, + example, + release, + wef_version.as_deref(), + wef_path.as_deref(), + bundle_type.as_deref(), + ) + .map(|_| ()), + WefCommands::Wef { + commands: + Commands::Run { + package, + bin, + example, + release, + wef_version, + wef_path, + args, + }, + } => commands::run( + package, + bin, + example, + release, + wef_version.as_deref(), + wef_path.as_deref(), + args, + ), + WefCommands::Wef { + commands: + Commands::AddFramework { + app_path, + release, + force, + wef_version, + wef_path, + }, + } => commands::add_framework(app_path, release, force, wef_version, wef_path), + }; + + if let Err(err) = res { + eprintln!("{:?}", err); + std::process::exit(-1); + } +} diff --git a/examples/wef-winit/Cargo.toml b/examples/wef-winit/Cargo.toml new file mode 100644 index 0000000..3452ff0 --- /dev/null +++ b/examples/wef-winit/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wef-winit" +version = "0.1.0" +edition = "2024" + +[build-dependencies] +embed-manifest = "1.4.0" + +[dependencies] +wef = { path = "../../wef" } + +image = "0.25.6" +winit = "0.30.9" +softbuffer = "0.4.6" + +[package.metadata.bundle] +name = "wef-winit" +identifier = "io.github.longbridge.wef.examples.winit" +category = "Utility" +icons = ["icons/icon32x32.png", "icons/icon128x128.png"] diff --git a/examples/wef-winit/README.md b/examples/wef-winit/README.md new file mode 100644 index 0000000..348cdd8 --- /dev/null +++ b/examples/wef-winit/README.md @@ -0,0 +1,21 @@ +# Wef example for Winit + +## How to run + +1. Install `cargo-wef` + + ```bash + cargo install cargo-wef + ``` + +2. Download CEF framework + + ```bash + cargo wef init + ``` + +2. Run the example + + ```bash + cargo wef run -p wef-winit + ``` diff --git a/examples/wef-winit/build.rs b/examples/wef-winit/build.rs new file mode 100644 index 0000000..e628f61 --- /dev/null +++ b/examples/wef-winit/build.rs @@ -0,0 +1,13 @@ +use embed_manifest::{embed_manifest, new_manifest}; + +fn main() { + if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { + embed_manifest(new_manifest("Wef.Example.Winit")).expect("unable to embed manifest file"); + } + + if cfg!(target_os = "linux") { + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); + } + + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/examples/wef-winit/icons/icon128x128.png b/examples/wef-winit/icons/icon128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..213de6360a63aaf0b6e993163cea6f7d1e750d90 GIT binary patch literal 19340 zcmV)sK$yRYP)W~kYhb?tysY5|_8S16w?}kupSRv?9m^nU?EdIE)W8%yFJI1ychYjb^Rboc z!dZ7cMivia?aHY}qmghpuo|EQil@ zWB^gjtmih!pq|?vUcRkIx|LW+pL{l)RX;><`X`Y&Wa;S_Z5_Gu*H^x8x`I&RI6=0Ew1PZaKc&ecqYf^D>u|N=4&&74_-U zey?tQ_`2XrcbsDn>73{0^Sv+?ocHth7rwxujL~06t$qw;ta|QN0~sjxKhK&5B7F+@ zE?e~$9G^*TymMT?_V1=Gc;)FG*E=!Y0zYAfN~`sIUUe1XUIw4I;Z$^bGkt>*HsG5h`-KUVzB+>?<=cfmYYzCko#j&*-{7BnFG6!0CkviUOOWkANcd+>>*_!2Zg04Tvs<9Ok&B_>pgEYhJx_B>2F^i~h4?R^so9 zW z9I2JdLnB}_6h%mQCXWpGnbhB3x2gfrqmayg0Y7%kY32ao=N~BDAbtUbDyuIH zjl_}{V)Um4^`}+aUu!oFh#n*Gy~9pPryss$!hPd=r!RTsn>wy?Je4%QHAG5eSkYwHN88csqbFM?ZLH@qHAkoV*;~s^vT-fQ>SP zf!*-^G(x?@r%`9W=%fZje?o)_vh+v$zWK-{i@(%0JNd3cF7PQXW#IA;he$5yJOOj2 z&%?IdYHZG~LSS4B4HnFf{&B^|l@IY%K|*@j04SE5iU(l9KvR>o8C9!5B0ggt6APl5A35 zdBUzA*kb<}NMeqotqV6ONdZNVaGv^5eWQ&`Q(Hss>P95vn|VFAkUY1(trn+rRbp^3TKwwq1|Xs>z!}wkj>| zo8%+*UAn(yZyFGt2@w`}6o|iiSzGh%D@KA(Tz<$Grp-ybJKXmbT-oyh#s4hkbsUcc zGnZi3_=9+C*I`VtcG9afATcSc9^UB4JI|8&8^x6M1O=>oqj z5LVCo2%$IVN-yTpe-a925DyRh6yxPl2*}3rOkf%epw6q%C=~0L%YiKva#lNK?Klp! zvS0*R6>1bPb`oiYiDCQi|8-jNi@Y^VmM7ppmAe9k{m)a?K%^_p3Ie`&`0<(agLjU{ zUVnDy!q=TO^BOO%Tl_NTw1T#r#g@O}q`giYKKqSGrs7yLa2@*d55bxk0XDEQ^Ub~u zfGyE?PzVBJE5*FkUS7G5R*EBOGy+G%&dT*f??_C&!ecP2^=KS4>sSmH*5i?(AEUru0<4jl&$WRD zG6j4>!y;J5mSv$)&>$?+Ag6BL_CXl!iZM@ITb`kz?HyqbqF$T%AFX&%8wsY z_#2L?boO$1Kb*WUVoS`e1yzgpNBh=*=qMrqKX&5VIy1k!Y7qbQjRW7bXlc{MCfv+H z;AQU@QG>SRd@SM8Z*(3H4qS$j;tqnVAqG$dRFMa&Jf`wiRr6^8dz6EoUnK~V@tjR( zO0L!pl=uZSP1y!&RLQg|Z(`K`?)q;O|MHegF#M6PA@TKd(Rf!7ezA-;WcI)IsR7Xu z)eDCb-lCJ6l8>#*C5}F${eWd3o^wq+rKeTs_$eqZ$O@KIR<3#vn|V4~n>so3ufz7- zZwY7$)&yWNc|c}!9bC@I$=MGW&UBFzs>?*iAY~kgUx7(_6MkFAgzL}@6krLyB85(( zQmCX`;N`RS;f=r7r+?$Ug{>d?DUx4*`&1)fJ+`(#>`McpA0Yu>W`E*Ur?zMAy?NOA z?;BrnaqG0$YYXGX=L`&wd=?6h(pb=W3g&hnjxA%q=aKI+lmh|Z$5cQS_5jzSnkOJf zJYySy&y^wK!KdOY8&joncX!r_C*|0w5&8u*SM6A68iCvz7RT+jzD@Sc%g-o3OrId$ z=o84tv8nxGFB%Zt2vMC7@M9;xqrG|6Wy6)vt~m7EnG4e&FYLDO0|9!Pmg10Er(mqI zolF1sFb`Gzh9&0dfjoEt@J2c13D59M+M#;&>Qvsuyi#=CoX;dqyFtw8POEOXpCDA@D=W=~fjR z8^V46xVU&Uox(Vs!U_Pk$oKxRhX$xKY84Rhy@QT!N zQ!Q83b|^34OTTsEaEk>5(^@Br^X88wVsY1DndO!sMi6ina3;oZ*WVvld>wBK^~&Wa z^Q-q80F|(NP|Oh0n8mB!?TEeRFL>m;cG#YG(-Gh8oS8TyKj!%zO(0k6=K^5IideK!B?}0dC}3)(CDWQ!lR?z~0I&7a|#VrINCl zSh28Z(KffaB^IOK3CFcTGOe8P5p&ns3(Idf^W)IheI;}Tefdz<+aGEg5Pd`x?)$I% z+m7bnTryPo+?7k-JLiz}=Zh0TKv}w2d=o5iD{W66go9_i2}L`C2L~@gwlqMn6=yQf z`~=ThkO%WfoO4stsFzj2R#myE&XoErV>*;$dDsrqmg6wi(gb42#1DpVGzx{siia%T zWV(8gvc@sV40LWdG=Mk1^~1n5=R^MdXuknq@)6)ArTGe%{^S2LlfLunas7{9nt#-h zuj{zVag}F-aO0;e!JyKUUIz~9J_E^Q6V?o^;0kaj0ZhSg>am!74d4(c;iVqJd|4T$ zf;m|h=GW_pJj<94^R1-8K2U8ZGysEyvk*ydHXT5XvOk8AvBxV5=4>g?oj)E+WE^j# zA9wu2;^JFR`CC}t4NUMmEBp{f`-5mebRiUiKkLxuM1FKEHM|4O-@g5*pEtF-2bJ$H#t)joB+u37QFtPXg*-5jZi1YI;u~Ck=sRY&(>jQU|bH zIl_FFhi%o$t$=R~T-}MZcM!o(!oooFTEaKuYS+-CW_nlO_$!XcHJ2uW+y8!X<*!bB zH_Y#TfN=o(4S-6JAixW0%!P~1h@HkG-;I|I2j~B4>3Q9=lmB;u`#xRk@SY!3I;iCC z%u$%vbpnP;8?bid2gp|hc&gu8Fu>M8ssk6M)udrqwi4z6p*-6qmNg!ea+EPFtI9B+ z=|)~5hLm?GVonDD_DupbSAh|mEv!}9@nSH&D{v0z9{cUbkI$WT_+Ms%2d^F%11Kxj zc)5p22bzkIecyZIJ3E_h`te}!xt|<%*1{!C=a-5BRj#C*B8mVRG{t6P!L&E2c<}FhsBp4gn5%)|R*tL@^E%l`}S+;UW`JSXQqGw#|N-4)ZB5a@9JMOFsxY zB{@coz?6_JtV6!k2c?|AXzlcEEdIx@KHGay_wgCO@6P=OKt+W?@h#vAc_+W6DYa^4 z&ilU~pL5VDZ|}J-7E_rZ5V)vanT4EPF`dC&F8!Hw7uJv5h#eERP~mBU6p(;ANCYed zss@;q`pm1BOMX2~fSC@HJMK3S52wEk8aG3Hrz<6mp#`q%?<$O1x z-RzF?%B#-Z`Khi$q%>Mx!vR887j}df&Z-_w%STzp8lysP{C{xY1l1DOO zew0@$ujMt?HS9O4BN$iHhQmrA=^hHtY2)bFAm1{^vOkK^!XqeG3W3Y}fQcc0!%yG8 z{a;d@UY~I)!^J*iDJrLcm$d;1@InHmLr$XnxFh9Iqy@gvMsP! z!BYEeiGwk#{W#?OUaT9v0^{W@^CJ*339>;w->?xIfyn%7x}p53EdOj{(qO7%NjQrT zcV+=JO11_-!s=0cDzb>?UkuQdU9o8q>!3@bpa1{)JG=lkXsLm~>3Xi`$Va zt+&?DrnOQVZX5gd9p4+dx#uucQsR?btqkGxz-jWL2012N1BV09m7I z89}V2rM#L)*;=0Tv}YZb8^G9xy5tQ{q8iNv|6n+}iC9ty@+pe9k3-1j*TN6VftPTd z9c%J8-nhK~((c3JxxR->yO6*ziWp-hS^nj>fIw<8{jv)3%|g{9Ul#G#y}l#!n-xR) z;=7OjVP{YLb$R*MJPP5`Z>e-bfR5x5=x90=1I2aNICeFaZ6M&=8i*2`23~-cJj?o) z`BTayP242k*2=BqktXYb;Dddz_nNGzp%RFafVw>vqwLge5Wbc>RFU!Br2E@o2y?lZIFJCK5zX0r=`JT?^>nGWNTu~ILA(WE_>egRTQz>?TT05^bW ztpHo4ea?Q7F{)++BCB9I`#onK4PXvO=I}cxM9$g@m;J|)FK#p1^K@aQN|22<%F3m!g|4Bdvz&$7gQibhESryQ!bqv)nWn5(TBFDP5 zHpBr{NFm`I1V^_LE*5a+gY`>X*>k+`2~3%@-_aZI%w2iw`NO}Qc4#cuzsB^&V*YTs z3|aaGd~y6LwFYP~(gIU2gP{10LK<_qw{M0V`I>m-Ge5fbnD6$?O8!N3)eAlr6eTLn zykKcb9*Uvz7Hl57kypL4eNF{21khIVOf9bF5x^;F$`H3nnaT2c9?PqB0Yh*j-dc^g zUIfqS;pil=VQ}9Ma-8|=c-|iiyrk>wdbDuIb^q4?qt1h4xxuv|_(L`D>&HIU4oK2~ zkbEhVFa4nSiJBy{-&^+2T}>;#Gf=tUrX$Z;xFqwX;siHWSSh0jfq_AiPF7p;NbdUu zJf8g(vZXD|r3g`BsL$BF^NIlC31tlT_ucCnLQ>6B)w+~1T$DUL4>9Keg5`326e9S6 zDPW?s5yf&JFMB*^bXWQDD?hO7o9XG!pg*SiO<4_8DizuH1$^=P%d9Nm)65`^FbTGa zm;58(3lTkdZ#ip5YULF>^+&$3_?53Yt?OFXQ!!3`bTf26#rd8RWownK~47w!83zAXKX;In6b4uc}Sl!JVqkj#E}?h&mC zTPi2Ftj#z5Y|RN*wX}PO77Iqs!eSiZtP^Fl#t(zb8@65fN3dgj70Q97>Z{M<);c>#`4QN=VY5wFS<;40V+=GW?k2Bct2K&YDu{$e<)iS4ifD8`nvemf0d zleJbW!?8VZL-sqXFB`kJW08{`dQA0!`##DTAxM%`aW+_WnJZ`Q@KPe?H$0MRuASaUoW=hZpB1pgaEQk zR8O!NF&%+bi&=XQ@F`o(tI5>U3Z~ApjJQT})qE~QVTMQW7eRjK5Bp>vlt!u2kBRbn z1c5P5OzX$*8Nd4GFArVceQ<2NZ>`zY;Pi>ZB1gUotIF(WuhJ4z4M7ytkbFt8@@+En zWp;aK{mZn>72oKuT=2_Ro^{YGnlC7oWEX}OA%ru_z|$>gj=ci8a)nF(9T+KYB-jp# zvmaD{O@$#oypiQ1Z!@{h<(i zKPUxW!g2c67w^96V|_pBTpSx4TxWVcPYpHf`vU%6_kA{14{W{8|0zsZfBAB#HEW<& zte~H?XDd|0o|(^aj*Lr4z<1xctT}npiZS<`ogTUaw@vp0mB{If_IQf`ybk{{p5e{%4~jQz~8(oTn!A{i3T7sllvZw6h1?W#-?#79IQd?ogbdl z^yBlllrOvY*yVEZ-ccOjt{qc=Z<9l1L(Ua9_7uavPEAxIpU#8cBX zVEJWJ$|PNrmwa0@lIl#SAxQZYVmrog#bSU>@WV~mG9ql6>DXVT-N5SUNIuekYT!UFi$!t%KxcB#wFyi0dLs{d(tbu z)f;@{x}~Qd_NvxPY(OZa6i}e|C&)S~iL`qNhVus7#_!|4zk%5V#D?$_Ee4L5f%#y9 zK>!t&nG~DM-z~3NCT&TbsYD#ZdRE%9779tu{6%n73tN$(VN9OjKThz)ARRh=(2w7n zU-`=~4X*4yG&bH>z3Cg`WsjWqBk&`qPxLMgS$XY?)V>P#kuUN>`kdu6Vizvong9NM zN1q-`d)M6a=%}L{sEKTF$!{*&{MpT`iv3HMWzt*jA1@7WKs2b`4PhVM{7Km|E}?Mg zfA#4t$=k2bdT)F8j9KUW$ALE_GfuZ(wnl5NsZsM*BjX*2e5n~*#@1k<@F17l>JW)Z zICUfzMzmPvl_YE6O@bem&*R zgb8M0^XQh)=_~l-L`edNCP9euGD5NhT}h!N$&+Pf1g2y%*^plYmzj}FJ!YFB&zn9= z<#~EOyzrWrwo7@#i=Ha@yI>7xeq8CXet-Lq&)IoFqQmLqkADMZOb=A5`#uK^+4swA zR`eU`mMt;oNdmvJ63k5z?Yjo0Y3nnuMwjs5B%aM7ovp73F&3axbLsn zuK(%Fi)X$0oipxCrk(bQaliDyrkv{=>(BtWjM^aZjdBu7j}2Bf_pZ->>grE#zNvcw zyv|oP`)gK=$!Z{j6^bmD-Gq9T`00?0OGveO%nKBAB@Qu~tgeYm_p&Q38zUv3syLNN>y?WJV?(9?z3A^A`wH6_lGR}2ANuoy>*~HiG;qsuAx2Lr$hd@LZ0->!w_c)j*^OhIC&`TgG9M z@$S5&naw&3VlYgEc@ncyotHETlrg+paMX0f^c*M^qZDAcaE3F#vV&uENH7kUemZ?O z-1x=8mD3L4&%2M9U7n|gE0aI+$V8IC2W&)i$OzJl#7~JtCr>6QGD7-YU+s=x`mw&? zf?pqX;^J4f-W>O|HP))}d|)2il~-H=lpIrp0^h=?G58f5R05-&m{MZ{m37-68UNI^ zpWSlng2OYh#B}YixhY%?d=5fBKq&fvC}z=KgN#WE>PJ5Q%9r7&7+Xn@7zCJlx#Wk$w!=CU z2-6aCWOZKBA+etj{AutAzE(*}f#8>U#k-BPgM0{n;3c%vx1sch73cQ;NSwaxW42d$ z;bo6_{5e)EkQ%{f4>AhoK8>KBf~?4dGi=!><|kHsc@wVNe)9F*Gm<9+CF7UM#+}Fq zcGFN<8Gf8Bv4%fT1U%>XzJXsg1WOM*fMealc!2Cs`I{G?z3yx5d+hjkb$jc7*_R&} zfX{S5ij3gNK1jwTVFD2F8~6Q2eaTRs7Ev`% zC@XaqEXKvn=T z05JqQg%w#1eB)@YdfEQUgIgcUesbmKx8FJMlyoAKaLW%}pOqyo;E3d-ghY=_hM|#{ zF$q--^Y3S+R$R1QEx-8SrLTEY&-L-Rj%g?(*bDd|BjZXrbFgzn+jRqLRIa=gfsw9i z2Ef)CsvXrB+Ys-a=jT#}0pLqyLSidnT+0K&x11bWwT0ko{>*HdXKaz+ZzcG2K#4M! zQ=?q1OA}_|=JN+H*uJ{Bv2~U+G`d~&?e+_w=qZ{o>;*#X_2~yu;uF=9a_8i;=enzY zxY;}Tt*sp&{?-w{YiaRj83SVirAw7O+{yx5F+Fy~uF*w*l%r8B1j%v*Fq2fVyvycp zSLPbQ)nFp)+uTt3D;K}#v2Sq@g{8}SocmYw%F^aDE$Bo>ARj2iY{-~|G!xFkW7^}1 ziBj_6hbPiEZ#n7O_RiR0<)ZO9^dL)eC7iku~QlRV|Z1Y-(XA*REaJSb}kzEZ7->3MK;29j+|BlxoHTUJrZ z@T(uVVf6d=T#~!5b5Sfi_?VGD>ypc!-JL#4!muj|(xk*so75B3kTN0Xod2Gmy6}^m zCN6pKgs;w;pZqYFSDvXIx9oHJa~MZ7f0egs)9r?do7Iqc1ViOVPzuJWG>w4Uf$s-2 z0)ii|0tOYvWg3Cc)j+wxXn* zV#V(YMZ#F*!@7Khyu>UP9(C|r>fi&4D^DJU@)lHrLAK$LEJ+zBru2rpCRXz2-Cs^S z*vs{=wc_+O{=z3#OQj{ zI=)>VH|AX1-*I?biUD!GHFuL1&5X45&TUw2g%6j-*Dyl|M1zx}xq5+^> z39L^;unyeukxC^yQ2Np(@7?hKIG{3b5xE+S%U&m|fnlDIe5@ibv)_I9r+P9!_^;l| z4{kmBZw@)K`MW{c8iKBA0B{^dRfFUrre@*M9TRrL@ETS02O)d0s7)FJ9~&_Ze3UmC z6L^+~^(fDPz49zz5I{)=S`mC_E*#Y)wW*H@6hrWbSx)fT2p0%CeU7v9(c(Q?!3Tcp`u}Z7Tz`4i z`S6z(9(d;22mCUXa+*0O48hlqqd-Fl0ZKUC*g4?ZKkQgb@ON_7RY0qt@=<>xkcowa zFh9z(oW+u1*)v1f=H7V8lgc7vSw|Lh9fCgxP96Ll!Qad=WZC~D_-z z@1?q(!61+Rpr8hP-{0G1Pc#vvD;e4|B2nh<4M?7lKJ(+9$?Lu{Sp4tnk61S6;HJy? zV@lvC&#Bm69FY1e9MSQQ*gmouKl;PpV60d{+--vEBx4UlU^s&uPWPc#FMj(r_Bj%ih zwzd=o{D-+M@XEC`in2I^WdUEvpd9iN8&t|%4SeGoWs&VK{rbV1hCY4Y^8K_~6XcW#8J7zv_YGKRfTB%!l$h0bjY?o8WNva~0sMi&KZw?A`18u_L!m zm4k`u(%^#E_$aQGhe^I7uqiA-DuuwaD2!n>^8+sX?T9Z?zG=2PeH*)&MDN`gx;2ZK%kI}Oe z_|;PylC+TX4j%h1TfSl9%fEW%XL{x&mY4I^=d$W*UBOY){uNE`LfrVsMYwzWcaVuK zrgF!jlp=xTkQ5x9LsxnUj+}iu79Ow=MTcPI@4-lE3zx!xUkfz3!a$w5b&*54( zH(bdM?<_9=*+(~D0l+!q?5^}LemIc(_R6KFzT)`yA1Q0CqcsS;aQT`KA{%5R7ZKlBx0*zG8N@D#?^Wt&?HHaEEMY~W(06_Ct}V41V7C@ zewRN0x#2UW!ZZzpjoE6y;p~6x_VKH4`P%R;bo$2V^!0jP_-9=s?0%diPM?79leY~- z_Sp#>^^}Gr?HspkmUq{R?dlz$pTFR!Q`+x{C!8iK)%qAz2erQg^IKn!)sNqf8`ggU z3B3qV^wn6C0knY{DJTM=wa=|VX_Ek20;!Xum&1%hu$RMeDJK+<9u z#`go`2G(MjwX8^~#ZLZ#b)G?#I8f_@_&4ll3esT=*h}H9k!8#SmuhEe_itm{LYNgsZ0^#$On}i2o1o1P9WoeWJuMR+kx=18d$Cd&cuj6 za_`+EU;NJxE8Bv51$L?bXT@72^P^jFj7q z!)Kq0#j_7aMQ5=y{~#T-T>>rowlc|tehF>SR{ z%5@Z{zXHbyN4Z@3JM8Zt9kv@s)@x%51j8VD>>iAan3=VLFbZFj`C*bThA`8DAX?T5 zV__6L0v~gGpi~S2UHD>UJIa+o&ibl)s|FC(GmfY9L^c@t$@}}hQ_h?1nI1Jb-mCh6 z(BsdtB({h4W;E>#N3YK@rnh(Tv^II9)>Fbji6F9hGHqOo zK%e;@9{VmH9RJXd7JsO7cJh2v0+;@Z+Z6A@lAbd#I#I%JH~tv;au$yESkZtO)Cmd_ zpy=RAKI&=alLsHA6tr>x6wscUkHhDlfUb@XjFh)wAith_W}#*TM7jZdF@_=y!8Iy4 z{pe1WjA^Zu0?k0WLBh*mWF%&8S-U}%?XdELGDQ7od>S(9Y>f${@D*YozQQEI4r8`6 zX$Xejkl=nli_{VJQ?87n$ZzlsC$M!w@U4u@0tOUeEF)gKD!1acZ;#wobNU)@`eff1 zkH5^WBJf#@b(!&81%V|b9{aovH$tEM&s{T*Jf-XQM8fHYRR*f47k9k@P4U_I!`9z% z>HiJlE{ouU`bSuqFX1IY7Qze3cS|e;7BE2gYV42?*j5UQdXB@K8FNsyBk0d>pn;9S z7x4X>5eUx}p+p1FoJnt9+JQvek*1(H`#mQHQ)#ldKD^xy6n1J$kDG4SRGo<`YdH<9 z3=#D-83symgk%_|lv^}W#XEj!4Aj4o_+=SL! zAIPt|e)+()UA*QUSZfHrZoKJ}eP6)$*@z)Ybs@6PUEr5O$RY3v-oA~$_u>P;&^|r! z8NXmFR+YTY^gJ%tufopJemt;aB}Wp661$LEBnGAt5}06s;!6k;Q@xXXO%8zMDeyn7 zxOZF3Xjy`VvkroeS1_1=9J$gkmsC?X1Tlyrjli`QZ#cXiiI@XKHSq_jp2YHVPMOc-iHtiVEsFwJiE5?gpq2jbog8bO-=MVY|U(=YD|4Z&K1 zFSXc+)Mo^1Y~UpvXW;SDBR@O0_mZ}`-sspi)2meI^ohqm#wr5-pT+58UsD3V8k|Gi z``^)?y!*1@!rx!G@N;cF@lW|h&2(Q`|R&5HrJN4&K)^~Y;QD`%ZI?|sweCC@7ti$QnOe5XA#4?D9vuw~>v zxH<*M><0)MzmT;B(2xM;0gyZZl7~hffEYrU50FRrOr|xs7$E63V`lpzw70h+9}Ho% z(2t7mBjB%!l?ruZtbOf4tw_eTa-BHx6P~&0fyY(Jjw!=Sfw0kr7f0EUszfa7hR3$4KF?D_HK#8EUzYxWRr!H@Ls|&q=ln&8;IJ3>&^I=x*W5H_-u|U|%R1+K z7dj>$bhj*WMCY4_A4J(Nz|j=tOqEI@aKbae07(mh2?p%Jvy;g@TP=_B>an7-6_?pK zI?}Vz+1`mj7cf>FWdCLO0jHF2?MVl=peY?w$|UV?AJ}ELPxNa?1zeIXp7g1S)S{|5 z6AXLs)*2dpGZL|x@Z1)n67YC85`rHDMW#7-Sdn0pXLaEvvSE^!l)%F`W-qsAFby76068HN76iU4?MkXuwd&|mfOrntEOL%4(C#Lkq-;dvT%Xz~$^QJGy zpLbsnP9MiMCCJx0%<*O&*PPmZ*I4}6vzv}O@|3whPbNFOe5GQC$F~z+OHe39X#qAQ zCPUDoJSt7{!#tKX@=ugkEu-e5I`vq=)ehqPg|R8wide!!+2)Zi6zv-h?n0gu@P-FB zs<<1dKmg%5u(wB=mNLc+dq~s(tK};$=Z4=JL(H4bncqp?gKzRE6lK33V!6mv#igzl zmM@0Tl93!Kop000K%9B3`~Rp@`U*` zq)8rdXat^Sl zP74yT8HjPom+_SSERAg^mwq{>R~wglY&vYbF=l??#g#L%qq2F$zwEj=)1!yVS=DDM zA^43y@9ysOh0RPE%%3SCUlbZZ>s%y8vuMT$+W-23gDysO7Bjr&R|SCp`$o2D_Qp!#i1=cdY5*3-@s!SIgVCRVqVK}e z2(Nj&)xdaP==4Q5ed6>L*qM0zvX4l|Od@Pfm_Iiub|?nmGETJ5Og0VgD728D`R4zg z`Pt^4*zr6L8Yru@f?_@dm^y_SsOEtWCBibYtTQz~%mXMAzMe1!9||y#VIac*Sq&^f zFm%p19>Qj;?NQ|E1S`T;S`-Wo+f5p|p#DzabQgz1;KrVT_@4!+y_{z-1d^ z4v7ka^2^r0=kno;*4>FMw7m>)mQ_?CEDqclVoinStQooYH{uovDkGYsL2RrehOVSUI$K?dP~8dHlP zeN*XSel?GM;K?2(xK)L$)DmF9rM?r%cn`h&OjuU&$1uVDUaod+C?`h3qFNU=ikT%1 zG8txRrPQOp$*#ETqU>Gm3%%^{21AcuKkqkvGR!9;(S(5NP{<>tNX^L}uhWg0?%dX( z`MCFYo;l;N^xL&d1qQ~55lAH^FA7T>5tAV}VG&=FKbc2WNnV%%kPTl`V(?)G0~rP~ z3}hI{Fpy~o4H*q=gFuyK&s9d=RwM&xoj@{HbM$m9T=I+Mp>WBs@E1MB)3K~J;1ky8 ziy^dz-D2a!dChx&e$}u4d*FIHeYwH)W|v!a`sBDLmpyql^Ss>jvEL^Qq75N209geR zlunac-GtlOG+R#}d90i{d|At}3trRmj#x@3f{FGP#N z$PrIY6p9O-m2nbj280h8hOoUbW+tb8;K?=b#^OWQeX92-ZS%d+u`TrYC$D+srbxiw z-RWa(=|G~VM$eTL)d8X1SAmS@d2KuecC;>3J);{cnOVmq7cYH#$2(H(UT08>jG(Ft z>JUOQNQqU*U`ihNNExGAzOBVTT^@YG1gd%9!!!e#TBs#zQ7y9}YAmqKm$bz67<4%6 zXCN8x;JzmvZ`Ol{3rUGzFI31J^v)?iAan+FR$TX`_!S^*}xoDMgwIr%}s6LJP)f`AU z_|%HX?JJ0cVWC2tmrAJ^_j=BHt;DG-^DciF;{@O5k`FHV+y^Kd;vK$hnBt3!l};^r z2F`q=J*A7eAor`U^nHJLQ?MmHT@6j_Qhl{+URo2x>64i+M~KMdmllNB$_pc?+At&* zinanV0`UiAC1{}$w54b1u8Hk_8(>;r_o<#UI~OKiWh&K$QY&glS-yZexQaaZNK7I( zr7TR>USVqVZP zzMLq)C4UO|;A26vRAd;FuQKEr_>C&4Sabi?!#`Ym{lvqq{Dp6H^Bzv09QOo#pLN9w zr4=D2UpS%qg+LT*K&Ap3R{}ADW*R|jveR}dr91KimGrUy-1*v>OVV%BjyQ+ba8j|Z z1(m}lFeR?%fsXw8&QjwV-IeiiMGW#tX5MuPg3aVi^!bcRX1TmH;Noj5e&9-7&ZPK=9rt7Zkrb=?j zvX&zkoY?YzVksRDLg!F9p`Kx@`ewyy8c&`EAL%`c8hPfSmKJpCY@LY&suL~dwj!O7 zS>H;qTq%UHWzP74+LA90`Bd;l$jVEI41;je9lxf=S{OI3^!5kxxBccT12?oS^l~E` z>K=de+rC`(NC$G+BLfnW2I@^p;)Oy}RLDU|I-D10tmif3}@KF256n(f|!Xauq?f2%o9Z6u_lCY?_RkEuT8sUa@?0=_tXvhNG{J}ZaK)M7?2%%Ju$tSDbJ zCM2stWCXGrL`Kk>o~b)0b_8vpb9nM6dd{G8cvSV~P|jW4ifbKILAI#I%w!J?WH6Zr z?2`wddR<6O!yL=~y#<-%bk6*CXzf9qJQl#Gqn9i6a>?HX`T16XoC-TofnU7He1s@YOvH>2VeGm0bkDh5%|@?O*UHZ_31Lt zwLHJ@3A>PTAx){s2xM(dGA+)boDjr0?0};?3VoII(eLd%Y3AW{4qeVFKXML1r6PQ) zAle2V^)m3OmXoQ4#omZ~?(fXT;Yg*=bqgh_|c-|h@ zyt4260A4iUbF5JEdJ+;DAOHTHNVb;w(rk63VW>)Yb#+RY)T zQMSMU=`oA}sF1Ocr^*Y5T0X+3njZ=>9Y6p|#kKJb?!S5c)vJFr{QI^A zUUp=o>D}#`SN8n~eA)NqgM{#6ctmecWt<`xk!V#kCPpBBffzxPwYD{M4(S*0gplqz zB0hiVSzUjX?(n7sMfwFAf~B+(GGV7oF;v-zAqpZGX7Z&JR}z9JOM4>Lj;7=cq!Vof z&10zn!g?m$<44DL@_gS9gqdLh0ToO@(Bq+h5RVafCl00g8H9=Bri_Il>I`7_VVZN_ z1iGNt-nDVn@6PYLxnn^*H?+}@?)b&ymwjKrF9PsM(h&zFEim;W2~{mUwZV#PMAROH zVyqe(0prLoXlt6IyK-B^In>im`H!BrwjB^(?3ZOm0ZKuEPxK07rX_bOFDcQdft=%; zQZu;McfoVxB26tDD5YTdTm5)p5ZQ_CG=d4{$*_o{4MMq?m*!nS3*xa997l|XWgLot z9fD6O;n`payzthlz`7;p@dr0QaPt?o|Gag6d@Q%g?^Ukq_kBh1EAsR11gi@8a^9ED z4AT;5Rua89NVNQPMjFX$RuXao#IFH290 z4^_teKI5x?r9zKiz!#@aJpM-T*$O-17rdB)sPgHKH>xACOgAAh0-2ggHWoUErefNh zW^{OI-7&GNl3MzA9mmajW%>*^rj_xHp)$pQAm~d&P~n>Z1YF)7WD;FyPS2os-wdUt z3k%M7%JXe21bqmjx$P)ca?F=i))7)x4s zW#IU_Z^zEzYtCK&BMolzPLh7>rZ>oQ9X$TNI}43 zE3zd-Mo_&ujM?_|OdUFh3r=l1Wby0U-j-<5P23U!1?3Pu21V@|$UdKnx1)u?XObOo zG@Im1M>xCLxr@6k(!A96RQW~kx(PSf~@9WSbD-tgO>hj;auI7-CW6JW_YuXec!adZ0e33AeTm*cSJb9-oM>O?Z00<)l07_m0nAhy<8f*kP@Gi@Xm zGEznfsBEqutFK#s#l{=_2U_Mj*(`tHsd@a|_Yr|F9)AT0XFgjYhp?-VdA44Eu6C%Z zXWW&v5s|~zA|nX>0$v}7Cj_}UY?`GzbK8Sfz_h*kW7FT`GJL;CUsTt# zGID7rB0-R7z+=my5iqWw5E32euvWE~hW*Ua_q3k4_}KZcpE+$2Vmi$g*=RvS00dhz zoZT|(hsL(?N>_X@4W+nUa7)l!0ot0UamjCnqDx6}#_UNLL6z`WYr#fCu*t$Q5ypH; z86(1{Ou>xm4fk%o0Ju`@UsQ5s`iI22t0u zHTtM6%?5>JH4r0+GtIRb;v6;`aSjvcNbr;}wz-m8d}iA#mcHr0GiS`4n}{J}psXg* z!{@9Y+r^o`13};ucn1m~pawGAJ6d~?N~W1(L7+$oNkFZVCEy|tz=HMoVlbf*u#7yc zG9`@ZEe~&f__nX~U)6Mgo6YaEeQfU~>G4w&dj!$x9+}S|BWh1#&2}78WCXIYi(k;p z*%w|KrRRmtVLHA5^WOO0!`?b~;ll2?&Y1DSh#DN-rt&4ZW_KWs$;)!)du|L}ZPR(Y zlf}hCarR4@$kc(i2*IC{mON${5(5QPP_{W|`^FucZ~gqvpCmioVZUJeZApm%$PYZy zxy*dI<1h0&e3ButB?ysy@dwe{v;7rOn-WJxAX`F=9Ysb^_X{!y=&tc?wi)odUjLbe zr*|)CKX@!Rj7p_swdM~B)U_hQmixU+?|xc4f9H?OqGF+xln_fcEWr|4227HLC1lk! zc{0{2SPN4!Io-cw*TAnnv-2_y-5YD?ac>xn$Gynui@=wC-?ASeMlX$^p2N?Owj*-b zPhk6Xa8$t$!6m@9N`}V4@@g5vhg*QjSF(C|aA4w|@Av*VyD`|3>UM@oeWp*jYS7>F zf6prWK3k#%hAq${tMNC2KSbJ(NLB+e0@)H0>@@TXT-R;kZwPHVjrK&l+nL)| zN-cUz+u^g1Z$2ZQc9KCUFf;)@jo@ce8EQX(=FCTgzO8|bS{$au0GK9h_ll)O-zghk6u4|`PSd&9!<^gMoT>F6Z`>!FU!6-eR9oPVv_>C%zU=Q6k7`4FQJ!! z^IU&U)Q4tALb4i&5y(0&(^;Iu7S7UEQhSe)I3fu6pdc(fc#Az3li_ z6aBg;XZ;9#@%Uxw7x394?T+2lI(<%~KX>C1IDd|Qr7xjIBZ&M0F@n%9kkueLgMPuz zfPR7LIR4z3Z#>|TX-hN@2ZX^uDb`m|ibEze4AteH8@naohP#F_hF-rxV1n=j5ts@~ z4Yby3+r8tzTyw?XUFie7Tyfhzk9!gLrrHa8Ci7AZqQ*bxcS(Oj1Q9~A5{MDVmXMD8 z0x<%w6P>1_+R9l!bMRT2V`sdg_4V>QLQpZ2za#8x6e}8`O%4D}YRvxze?HK(H z;LCBZ5&Xt|f1gdiR0;%y=j&Udz9kkA#R!DN2*f#*Eg>`lSqY6ZYyoP<10I^aWi^^pbwZF!Qb~;UuOQx3VbR`bSBaBP6Av+ zjYc4=L9`{bgj)i*CCt$D3o02vcO7?b_iI}ZNE{|#V8rdWO1Vj`3I2vVzP#gFI*ubM zi9wT9{dD@|qDRj9GV|rEFULKZ`2xPo_=u#rmj$U8&{xV>B9hg><=EtuAV!cRFGoe$ z5?W+6;Fi!)91L2FZ<2@nRomgyk7znM(c(7CQZTZm_~2dtv-3uRKSuC}Yv9YWFK7L| zfiE-vc?F-}_q+sA`18{7K01xYBXMK|VhCacyB`%>bqcMqHq|zypxHl*|4QXTE^nIP=Bni)Q}I7JTp#MKgLqg=kC?YmUqzi4lljAchca z37qY=g{Oq(_;lTr-xaWoH$N1PPxIi9nQ;^`O7KVN@X1lHapu2lE_(1Jk0|*`M4q=| z8CyhR1VWJ!BsnT^5Hs`(GTahUNFtf&@y99OxaYsD&iefJ zeFjnRJ_}y}1=09qB@hxL5Wk?&5R$w>Owte}Ps(L4_h|sdN~I!3ASBM7kSzO+;L9h9 zyu>d{iWK}p{`hE&5=TZLTS8$;iTN0(d zSUz6H8j%1kvt0}!B1ucWfGQx15eVQBNm}xyT)sJqg@4(SDD=hj@zHoC7O*1{@S{BW z9sxR{M&KjRqgZ5L)+9=Mu_+)Lv&4HN`5pmyZxoeD{DPq8dl3I0ov^(SHH+C{00000 LNkvXXu0mjfF5cNd literal 0 HcmV?d00001 diff --git a/examples/wef-winit/icons/icon32x32.png b/examples/wef-winit/icons/icon32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f170e8089b2114feb0f93eb1738aacfe424db5b1 GIT binary patch literal 2601 zcmY*bcRU+v_m53fwMLDiLCqjmOCkv&6tx?zeeDV&A#nxuioJ@f!)nS)TQwVEwY6KV zR?SkB)-0~wVHCj|z4v#2@B4f{=XuUK&-Z-4=RAKsDK=J@_<-U-006*eW{S3Bo}gpn z=49@~h=@(*!9ujVWC(cDC$Yo~cnGGBL;!$S@Yq-Y`BYIRz!8gaAURlCz`XD{H4ksR zr;i#LM_{4>05}=Oba6f;4-gp_7(|4T5i-9JFs6T;hRA?^K}Z1z83#)nkP$w_2XsMA zTTNXC2?T*a@DOibm>t^W?{a2@kntmt2rvjFEG$edOiK+Pavh=pg+d|fnh;G*RVG4} z7#>9OAgcxuW&a}iKOVFX(JKT?AYt)ApkrPSPkbl|AtQ4v^w0XMPZHMm|CNG>e~-l+ z5ORD2(NI%|{KL&Gg&${O)*)CQrt&dAQUm@A`9Hb8b>NU=@&9V(uStJpnWG|saL7N; zh6Gx?P;dkQxUptv6vlz4{#qmfb4~2153#eZb?8^Ne%(M#a`FILT2;HpX7gY{=4bO9=CGCoJTkwU@h3W6n}! z=ji97+xnmGu$SXm^KV%9rKG>jOvqhR{Q#Fnhcf`HUr|SC8XtE0WcO#@)V4*uA8Qu# zj^zE?IIH64;83cA56UhKa;Znsk45iM(Tr`sF*7&Cy}RDC&qbRbX)*w z%J7;Pypj-P^~bFxuBNVKG#GzeF;B;0L)vx}w<3viK%K{fHUcwB!!2};y+fAsHGF0& z4#4~(q({K#D^H?0o;`X5%{B~Hxsh*oL8Qmm?ZJF+v77aYGrZKuvd{FCn2EJ^Rc@B- zo!*CzlLwPVdX6mhvCjZEZDZTVjKIbe-NWYSe zz22o{*P3%xPF`-B<5#KQgmKhN#e>=;Xt_#Eu4sZTr&H(6TBsCTO`sCw%RID22WBDH zX#b_^!*1E6V0a@KU|&M*?~$PURF3+S(}y>3UoJf4o;4T3Y@S|WI|odi<&gZ%?6Lfh zj&s~eozO;twUUi~-+n+&F=TOmfo3HLG`{qw5PvF%w=pM2y{X5vsiEy`J4#Bl-nhK; z*-T)?{#cr`a@3JdOg6er=UvkHh<;Wc$Hdlhh!1?cH_1!ql!hdaB1Pg}XU^Bv>fqm-v1UDbr-S+>Vd<-*;Rnw@ zyia%V+;V4EG)?8I`{a~23S@MS$t|Yb|60rFW~5xzd(&u4+0qmgz;M+UEQKwj+yHu< zXX5*>rqbqpMI2seD5vjDy($I+Y*XV@q{%NLrQY@wZB|{?Hk1#Id|oV)X+6%8?KbJ8 zBW8T|D8<0ev_Rj{vCSha*iB0SYqx$t?n{DPHGDIs=i@dMpIZc9^h(2SIoQbl-%VPg#;p8 z4nzor?JE`QP>_KXrwNZ%XvhKm}{klB9fdBxL{yg z7$i-94`+Os(-yQ`olpPX#3}oovV+IG6b6-U29Jr6g*q%j?QwJqSJJ28-S;(;BacTm zHaBlP6Cq2fL$|q9 z1v44g;MQkPu8Vdy%<#r$``3(N*xAn;eDf%{hnnuL7IGb)TRyA*{8hDNJ5g$bUHXiz zuLAVXELqv^+3W3gSKK~Vrc_ztm&e(zNGYgVL6R6$xY?lfB0+q8BCk*@D-i*fa%(t` zyEpOj^5R|u$M8k=FMjAp3cMXT%C^{8PDbDEJ3O{9jGug%>T@=*g^ikJirH&e z{C5#3U@ix_iW52T^yCFV9&N^RL6u3R+;VcZ<~mp@_tS z@Ju^!0!22B5)$sO3p4keRSB;9MstZEF7Uy@&D(OaV$U@szqZ14R{U&q`D&) z;@mr>uT;hCxqP3V(9dNo(ff@?U+KhpjNOelM-a)jm?r-I!rd;G4J~A&t|7a5O^%PP ze0g%}84=-CoO}iADSL{a3RTx>h!mi{dNG$b!vLgReyR`6R!iULkqTootdFkPUQe|) zj@iB#=Y&fl<7;&<66D8jUwZ->bWSj)Cu80D9tc$OEQG>m>QTxlHOX1uH714@PHDDi%Kve7I?cOGFygQYCjp zfpTj5>gpfeYca9e)?AJKEI)$zA11_ zuSTxgJRqI|H{F_F+k4q_iSsN?T+=VXq|l`HX2lnY03MOlpQoGoElj|eKQ#8Y=Nja5_3l!H%$4@-2;}Y?C9?W0Ud(G5 z3M${Y!#?)9<9C`1j+U`WeTrOQxVLsSytpMfZz$JF$&8;v1$9H$-~nN)^t)qy{2#3P zSW47|+YvO6;E?&>Tmw-?E7>?rZREI(=#v@|3qm{2=(MWo*3l#)u0-A7DgvuD8; + +enum UserEvent { + WefMessagePump, + Exit, +} + +struct State { + scale_factor: f32, + browser: Browser, +} + +impl State { + fn new(event_loop_proxy: EventLoopProxy, window: Window) -> Result { + let window = Rc::new(window); + let inner_size = window.inner_size(); + let context = softbuffer::Context::new(window.clone())?; + let mut surface = Surface::new(&context, window.clone())?; + + surface.resize( + inner_size.width.try_into().expect("valid surface width"), + inner_size.height.try_into().expect("valid surface height"), + )?; + + let scale_factor = window.scale_factor() as f32; + + // Create the browser instance + let browser = Browser::builder() + .size(inner_size.width, inner_size.height) + .device_scale_factor(scale_factor) + .url("https://www.google.com") + .handler(MyHandler { + scale_factor, + surface, + window, + view_image: None, + popup_rect: Rect::default(), + popup_image: None, + event_loop_proxy, + }) + .build(); + browser.set_focus(true); + + Ok(Self { + scale_factor, + browser, + }) + } +} + +struct App { + event_loop_proxy: EventLoopProxy, + state: Option, + key_modifiers: KeyModifier, +} + +impl App { + fn new(event_loop_proxy: EventLoopProxy) -> Self { + Self { + event_loop_proxy, + state: None, + key_modifiers: KeyModifier::empty(), + } + } + + #[inline] + fn browser(&self) -> Option<&Browser> { + self.state.as_ref().map(|state| &state.browser) + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window = event_loop + .create_window( + Window::default_attributes().with_inner_size(LogicalSize::new(1024, 768)), + ) + .unwrap(); + window.set_ime_allowed(true); + + self.state = + Some(State::new(self.event_loop_proxy.clone(), window).expect("create window")); + } + + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { + match event { + UserEvent::WefMessagePump => wef::do_message_work(), + UserEvent::Exit => event_loop.exit(), + } + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: winit::event::WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + if let Some(browser) = self.browser() { + browser.close(); + } + } + WindowEvent::Resized(size) => { + if let Some(state) = &mut self.state { + state.browser.resize(wef::Size::new( + wef::PhysicalUnit(size.width as i32), + wef::PhysicalUnit(size.height as i32), + )); + } + } + WindowEvent::CursorMoved { position, .. } => { + let scale_factor = self.state.as_ref().unwrap().scale_factor; + let position = position.to_logical::(scale_factor as f64); + if let Some(browser) = self.browser() { + browser.send_mouse_move_event( + wef::Point::new( + wef::LogicalUnit(position.x as i32), + wef::LogicalUnit(position.y as i32), + ), + self.key_modifiers, + ); + } + } + WindowEvent::MouseInput { state, button, .. } => { + let button = match button { + winit::event::MouseButton::Left => MouseButton::Left, + winit::event::MouseButton::Middle => MouseButton::Middle, + winit::event::MouseButton::Right => MouseButton::Right, + _ => return, + }; + let pressed = state.is_pressed(); + if let Some(browser) = self.browser() { + browser.send_mouse_click_event(button, !pressed, 1, self.key_modifiers); + } + } + WindowEvent::MouseWheel { delta, .. } => { + let (delta_x, delta_y) = match delta { + MouseScrollDelta::LineDelta(x, y) => (50 * x as i32, 50 * y as i32), + MouseScrollDelta::PixelDelta(delta) => (delta.x as _, delta.y as _), + }; + if let Some(browser) = self.browser() { + browser.send_mouse_wheel_event(wef::Point::new( + wef::LogicalUnit(delta_x), + wef::LogicalUnit(delta_y), + )); + } + } + WindowEvent::ModifiersChanged(modifiers) => { + self.key_modifiers = convert_key_modifiers(modifiers); + } + WindowEvent::KeyboardInput { event, .. } => match event.logical_key.as_ref() { + winit::keyboard::Key::Named(named_key) => { + if let Some(key_code) = convert_key_code(named_key) { + if let Some(browser) = self.browser() { + browser.send_key_event( + event.state.is_pressed(), + key_code, + self.key_modifiers, + ); + } + } + } + winit::keyboard::Key::Character(s) if event.state.is_pressed() => { + if let Some(browser) = self.browser() { + for ch in s.chars() { + browser.send_char_event(ch as u16); + } + } + } + _ => {} + }, + WindowEvent::Ime(ime) => match ime { + Ime::Preedit(text, range) => { + let (start, end) = range.unwrap_or_default(); + if let Some(browser) = self.browser() { + browser.ime_set_composition(&text, start, end); + } + } + Ime::Commit(text) => { + if let Some(browser) = self.browser() { + browser.ime_commit(&text) + } + } + _ => {} + }, + WindowEvent::Focused(focused) => { + if let Some(browser) = self.browser() { + browser.set_focus(focused); + } + } + _ => (), + } + } +} + +struct MyHandler { + scale_factor: f32, + surface: Surface, Rc>, + window: Rc, + view_image: Option, + popup_rect: Rect>, + popup_image: Option, + event_loop_proxy: EventLoopProxy, +} + +impl BrowserHandler for MyHandler { + fn on_closed(&mut self) { + _ = self.event_loop_proxy.send_event(UserEvent::Exit); + } + + fn on_paint( + &mut self, + type_: PaintElementType, + _dirty_rects: &DirtyRects, + image_buffer: ImageBuffer, + ) { + match type_ { + PaintElementType::View => match &mut self.view_image { + Some(view_image) + if view_image.width() == image_buffer.width() + && view_image.height() == image_buffer.height() => + { + view_image.copy_from_slice(&image_buffer); + } + _ => self.view_image = Some(image_buffer.convert()), + }, + PaintElementType::Popup => { + self.popup_image = Some(image_buffer.convert()); + } + } + + if let Some(view_image) = &self.view_image { + self.surface + .resize( + view_image.width().try_into().expect("valid surface width"), + view_image + .height() + .try_into() + .expect("valid surface height"), + ) + .expect("resize surface"); + let mut buffer = self.surface.buffer_mut().unwrap(); + + let mut dest = image::ImageBuffer::, &mut [u8]>::from_raw( + view_image.width(), + view_image.height(), + unsafe { + std::slice::from_raw_parts_mut( + buffer.as_mut_ptr() as *mut u8, + (view_image.width() * view_image.height() * 4) as usize, + ) + }, + ) + .unwrap(); + dest.copy_from_slice(view_image); + + if let Some(popup_image) = &self.popup_image { + let origin = self + .popup_rect + .origin() + .map(|x| x.to_physical(self.scale_factor)); + dest.copy_from(popup_image, origin.x.0 as u32, origin.y.0 as u32) + .unwrap(); + } + + buffer.present().unwrap(); + } + } + + fn on_title_changed(&mut self, title: &str) { + self.window.set_title(title); + } + + fn on_popup_show(&mut self, show: bool) { + if !show { + self.popup_image = None; + } + } + + fn on_popup_position(&mut self, rect: Rect>) { + self.popup_rect = rect; + } +} + +fn convert_key_code(key: NamedKey) -> Option { + match key { + NamedKey::Backspace => Some(KeyCode::Backspace), + NamedKey::Delete => Some(KeyCode::Delete), + NamedKey::Tab => Some(KeyCode::Tab), + NamedKey::Enter => Some(KeyCode::Enter), + NamedKey::PageUp => Some(KeyCode::PageUp), + NamedKey::PageDown => Some(KeyCode::PageDown), + NamedKey::End => Some(KeyCode::End), + NamedKey::Home => Some(KeyCode::Home), + NamedKey::ArrowLeft => Some(KeyCode::ArrowLeft), + NamedKey::ArrowUp => Some(KeyCode::ArrowUp), + NamedKey::ArrowRight => Some(KeyCode::ArrowRight), + NamedKey::ArrowDown => Some(KeyCode::ArrowDown), + _ => None, + } +} + +fn convert_key_modifiers(modifiers: Modifiers) -> KeyModifier { + let mut key_modifiers = KeyModifier::empty(); + if modifiers.state().contains(ModifiersState::SHIFT) { + key_modifiers |= KeyModifier::SHIFT; + } + if modifiers.state().contains(ModifiersState::CONTROL) { + key_modifiers |= KeyModifier::CONTROL; + } + if modifiers.state().contains(ModifiersState::ALT) { + key_modifiers |= KeyModifier::ALT; + } + key_modifiers +} + +fn run() -> Result<(), Box> { + let event_loop = EventLoop::::with_user_event().build()?; + let event_loop_proxy = event_loop.create_proxy(); + + if cfg!(target_os = "linux") { + std::thread::spawn({ + let event_loop_proxy = event_loop_proxy.clone(); + move || { + loop { + std::thread::sleep(Duration::from_millis(1000 / 60)); + if event_loop_proxy + .send_event(UserEvent::WefMessagePump) + .is_err() + { + break; + } + } + } + }); + } + + let mut app = App::new(event_loop_proxy); + event_loop.run_app(&mut app)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + wef::launch(Settings::new(), run)?; + Ok(()) +} diff --git a/wef/.rustfmt.toml b/wef/.rustfmt.toml new file mode 100644 index 0000000..eec724b --- /dev/null +++ b/wef/.rustfmt.toml @@ -0,0 +1,12 @@ +edition = "2021" +newline_style = "unix" +# comments +normalize_comments = true +wrap_comments = true +format_code_in_doc_comments = true +# imports +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +# report +#report_fixme="Unnumbered" +#report_todo="Unnumbered" \ No newline at end of file diff --git a/wef/Cargo.toml b/wef/Cargo.toml new file mode 100644 index 0000000..c9c7120 --- /dev/null +++ b/wef/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "wef" +version = "0.7.0" +edition = "2024" +authors = ["sunli "] +license = "Apache-2.0" +homepage = "https://github.com/longbridge/wef" +repository = "https://github.com/longbridge/wef" +description = "Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support." + +[build-dependencies] +cc = { version = "1.2.18", features = ["parallel"] } +pkg-config = "0.3.32" +dirs = "6.0.0" + +[dependencies] +image = { version = "0.25.6", default-features = false } +bitflags = "2.9.0" +thiserror = "2.0.12" +num_enum = "0.7.3" +mime = "0.3.17" +raw-window-handle = "0.6.2" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tuple_len = "3.0.0" +futures-util = "0.3.31" + +[lints] +workspace = true \ No newline at end of file diff --git a/wef/LICENSE-APACHE b/wef/LICENSE-APACHE new file mode 100644 index 0000000..b6ef865 --- /dev/null +++ b/wef/LICENSE-APACHE @@ -0,0 +1,191 @@ +Copyright 2024 - 2025 Longbridge + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/wef/README.md b/wef/README.md new file mode 100644 index 0000000..db3b3e7 --- /dev/null +++ b/wef/README.md @@ -0,0 +1,359 @@ +# Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support. + +> The `Wef` name is abbreviation of "Web Embedding Framework", it also inspires by Wry. + +![Wef Example](https://github.com/user-attachments/assets/f677ecb4-dbff-4e0d-86b9-203f6e1004a4) + +## Contents + +- [Introduction](#introduction) +- [Getting Started](#getting-started) +- [Important Concepts](#important-concepts) +- [Application Layout](#application-layout) + - [Windows](#windows) + - [Linux](#linux) + - [macOS](#macos) +- [Application Structure](#application-structure) + - [Entry-Point Function](#entry-point-function) + - [Single Executable](#single-executable) + - [Separate Sub-Process Executable](#separate-sub-process-executable) +- [Examples](#examples) +- [JS Bridge](#js-bridge) + - [Call Rust functions from JavaScript](#call-rust-functions-from-javascript) + - [Post Message from Rust to JavaScript](#post-message-from-rust-to-javascript) +- [Cargo-wef](#cargo-wef) + - [Installation Cargo-wef](#installation-cargo-wef) + - [Build Wef application](#build-wef-application) + - [macOS Bundle Settings](#macos-bundle-settings) + - [Run Wef application](#run-wef-application) + - [Add CEF3 Framework to the application](#add-cef3-framework-to-the-application) + +## Introduction + +`Wef` is a Rust library that provides a simple and efficient way to embed WebView functionality in your applications. It uses the `Chromium Embedded Framework (CEF3)` for rendering web content and supports offscreen rendering, allowing you to create rich web-based user interfaces. + +`CEF3` is the next generation of CEF based on the multi-process Chromium Content API. + +## Getting Started + +To use Wef, you need to download the CEF binary distribution. You can find the latest version of CEF on the [CEF Download page](https://cef-builds.spotifycdn.com/index.html). Make sure to download the appropriate version for your platform (Windows, macOS or Linux). + +After downloading the CEF binary distribution, extract it to a directory of your choice. + +Set the `CEF_ROOT` environment variable to point to the directory where you extracted the CEF binary distribution. This is necessary for Wef to locate the CEF libraries and resources. + +## Important Concepts + +CEF3 runs using multiple processes. The main process which handles window creation, UI and network access is called the `browser` process. This is generally the same process as the host application and the majority of the application logic will run in the browser process. Blink rendering and JavaScript execution occur in a separate `render` process. Some application logic, such as JavaScript bindings and DOM access, will also run in the render process. The default process model will spawn a new render process for each unique origin (scheme + domain). Other processes will be spawned as needed, such as the `gpu` process to handle accelerated compositing. + +By default the main application executable will be spawned multiple times to represent separate processes. This is handled via command-line flags that are passed into the `wef::execute_process` function. If the main application executable is large, takes a long time to load, or is otherwise unsuitable for non-browser processes the host can use a separate executable for those other processes. This can be configured via the `Settings.browser_subprocess_path` variable. + +## Application Layout + +### Windows + +On Windows the default layout places the libcef library and related resources next to the application executable. + +```plain +Application/ + cefclient.exe <= cefclient application executable + libcef.dll <= main CEF library + icudtl.dat <= unicode support data + libEGL.dll, libGLESv2.dll, ... <= accelerated compositing support libraries + chrome_100_percent.pak, chrome_200_percent.pak, resources.pak <= non-localized resources and strings + snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot + locales/ + en-US.pak, ... <= locale-specific resources and strings +``` + +### Linux + +On Linux the default layout places the libcef library and related resources next to the application executable. Note however that there's a discrepancy between where libcef.so is located in the client distribution and where it's located in the binary distribution that you build yourself. The location depends on how the linker rpath value is set when building the application executable. For example, a value of "-Wl,-rpath,." ("." meaning the current directory) will allow you to place libcef.so next to the application executable. The path to libcef.so can also be specified using the LD_LIBRARY_PATH environment variable. + +```plain +Application/ + cefclient <= cefclient application executable + libcef.so <= main CEF library + icudtl.dat <= unicode support data + chrome_100_percent.pak, chrome_200_percent.pak, resources.pak <= non-localized resources and strings + snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot + locales/ + en-US.pak, ... <= locale-specific resources and strings +``` + +### macOS + +The application (app bundle) layout on macOS is mandated by the Chromium implementation and consequently is not very flexible. + +```plain +cefclient.app/ + Contents/ + Frameworks/ + Chromium Embedded Framework.framework/ + Chromium Embedded Framework <= main application library + Resources/ + chrome_100_percent.pak, chrome_200_percent.pak, resources.pak, ... <= non-localized resources and strings + icudtl.dat <= unicode support data + snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot + en.lproj/, ... <= locale-specific resources and strings + cefclient Helper.app/ + Contents/ + Info.plist + MacOS/ + cefclient Helper <= helper executable + cefclient Helper (Alerts).app/ + Contents/ + Info.plist + MacOS/ + cefclient Helper (Alerts) + cefclient Helper (GPU).app/ + Contents/ + Info.plist + MacOS/ + cefclient Helper (GPU) + cefclient Helper (Plugin).app/ + Contents/ + Info.plist + MacOS/ + cefclient Helper (Plugin) + cefclient Helper (Renderer).app/ + Contents/ + Info.plist + MacOS/ + cefclient Helper (Renderer) + Info.plist + MacOS/ + cefclient <= cefclient application executable +``` + +### Application Structure + +Every CEF3 application has the same general structure. + +Provide an entry-point function that initializes CEF and runs either sub-process executable logic or the CEF message loop. + +Provide an implementation of `wef::BrowserHandler` to handle browser-instance-specific callbacks. +Call `BrowserBuilder:build` to create a browser instance. + +#### Entry-Point Function + +As described in the `Important Concepts` section a CEF3 application will run multiple processes. The processes can all use the same executable or a separate executable can be specified for the sub-processes. Execution of the process begins in the entry-point function. + +#### Single Executable + +When running as a single executable the entry-point function is required to differentiate between the different process types. The single executable structure is supported on Windows and Linux but not on macOS. + +```rust, no_run +use wef::Settings; + +fn main(){ + let settings = Settings::new(); + wef::launch(settings, || { + // message loop + }); +} +``` + +#### Separate Sub-Process Executable + +When using a separate sub-process executable you need two separate executable projects and two separate entry-point functions. + +**Main application entry-point function:** + +```rust, no_run +use wef::Settings; + +fn main() { + let settings = Settings::new(); + wef::launch(settings, || { + // message loop + }); +} +``` + +**Sub-process application entry-point function:** + +```rust, no_run +fn main() -> Result<(), Box> { + // Load the CEF framework library at runtime instead of linking directly as required by the macOS implementation. + #[cfg(target_os = "macos")] + let _ = wef::FrameworkLoader::load_in_helper()?; + + wef::exec_process()?; + Ok(()) +} +``` + +## Examples + +- [winit](../examples/wef-winit/) + +## JS Bridge + +The JS Bridge allows you to call Rust functions from JavaScript and post messages from Rust to JavaScript. This is useful for integrating Rust logic into your web-based user interface. + +### Call Rust functions from JavaScript + +**Register Rust functions in the browser instance:** + +```rust, no_run +use wef::{FuncRegistry, Browser}; + +// Create functions registry +let func_registry = FuncRegistry::builder() + .register("addInt", |a: i32, b: i32| a + b) + .register("subInt", |a: f32, b: f32| a - b) + .build(); + +// Build browser instance with the functions registry +let browser = Browser::builder() + .func_registry(func_registry) + .build(); +``` + +**Call Rust functions from JavaScript:** + +The Rust results are returned as promises in JavaScript. You can use the `Promise.then` method to handle the result, and the `Promise.catch` method to handle errors. + +```javascript +jsBridge.addInt(1, 2).then((result) => { + console.log("Result of addInt:", result); + // Result of addInt: 3 +}); +``` + +**Asynchronous functions:** + +Use `FuncRegistry::with_spawn` to create a `AsyncFuncRegistryBuilder` and register asynchronous functions with `AsyncFuncRegistryBuilder::register_async` method. + +```rust, ignore +use std::time::Duration; + +use wef::FuncRegistry; + +let func_registry = FuncRegistry::builder() + .with_spawn(tokio::spawn) // Convert the builder to AsyncFuncRegistryBuilder + .register_async("sleep", |millis: u64| async move { + tokio::sleep(Duration::from_millis(millis)).await; + }) + .build(); +``` + +### Post Message from Rust to JavaScript + +```rust, ignore +let browser: Browser = ...; // browser instance +let Some(frame) = browser.main_frame() { + frame.emit("ok"); // Emit a message to the javascript side +} +``` + +**Subscribe to messages in JavaScript:** + +```javascript +jsBridge.addEventListener((message) => { + console.log("Message from Rust:", message); + // Message from Rust: ok +}); +``` + +## Cargo Wef + +The `cargo-wef` is a command-line tool that helps you set up the necessary directory structure for your CEF3 application. It creates the required directories and copies the necessary files from the CEF binary distribution to the appropriate locations. + +We strongly recommend using `cargo-wef` to build/run your CEF3 application, as it simplifies the process of setting up and building your application. + +### Installation Cargo Wef + +To install the `cargo-wef`, you can use the following command: + +```bash +cargo install cargo-wef +``` + +### Init Wef + +The `init` command used to init and download CEF into your system, default download path is `~/.cef`, you can change it by passing the path to the command. + +```bash +cargo wef init +``` + +### Build Wef application + +Like cargo build, but it will also copy the CEF3 framework to the target directory. + +```bash +cargo wef build +``` + +On macOS, this command will also create an application bundle with the CEF3 framework inside. +On Windows and Linux, it will copy the CEF3 framework to the target directory. + +````bash +If on macOS, this command also create application bundle with the CEF3 framework inside. + +### Run Wef application + +Like cargo run, but it will also copy the CEF3 framework to the target directory. + +```bash +cargo wef run +```` + +#### macOS Bundle Settings + +You can specify the application bundle settings in your `Cargo.toml` file under the `package.metadata.bundle` section, otherwise it will use the default settings. + +```toml +[package.metadata.bundle] +name = "my-wef-app" +identifier = "my.wef.app" +``` + +Settings for a specific binary: + +```toml +[package.metadata.bundle.bin.example1] +name = "my-wef-app" +identifier = "my.wef.app" +``` + +Settings for a specific example: + +```toml +[package.metadata.bundle.example.app1] +name = "my-wef-app" +identifier = "my.wef.app" +``` + +| name | type | optional | description | +| ---------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | +| name | String | No | Bundle name | +| identifier | String | No | Bundle identifier | +| display_name | String | Yes | Display name, If is `None` then use `name` | +| executable_name | String | Yes | Executable name, If is `None` then use `name` | +| region | String | Yes | Region, If is `None` then use `en` | +| bundle_version | String | Yes | Bundle version, If is `None` then use empty string | +| bundle_short_version | String | Yes | Bundle short version, If is `None` then use crate version | +| category | String | Yes | Category | +| minimum_system_version | String | Yes | Minimum system version | +| icons | [String] | Yes | Array of icon paths, base path is the package directory(same as `Cargo.toml`) | +| url_schemes | [String] | Yes | Array of URL schemes | +| agent_app | bool | Yes | If is `true` then indicating whether the app is an agent app that runs in the background and doesn't appear in the Dock. | + +### Add CEF3 Framework to the application + +For macOS + +```bash +cargo wef add-framework /path/to/your/app.bundle +``` + +For Windows or Linux + +```bash +cargo wef add-framework /path/to/app +``` + +Or you can use the `--release` flag to add the framework to a release build of your application. \ No newline at end of file diff --git a/wef/build.rs b/wef/build.rs new file mode 100644 index 0000000..1c41574 --- /dev/null +++ b/wef/build.rs @@ -0,0 +1,305 @@ +use std::path::{Path, PathBuf}; + +use cc::Build; + +/// Return the CEF_ROOT env or default path: `$HOME/.cef` +fn cef_root() -> PathBuf { + if let Ok(path) = std::env::var("CEF_ROOT") { + return Path::new(&path).to_path_buf(); + } + + dirs::home_dir().expect("get home_dir").join(".cef") +} + +fn main() { + let cef_root = cef_root(); + println!("cargo::rerun-if-changed={}", cef_root.display()); + + let profile = match std::env::var("DEBUG") { + Ok(s) if s != "false" => "Debug", + _ => "Release", + }; + + let cef_link_search_path = cef_root.join(profile); + println!( + "cargo:rustc-link-search=native={}", + cef_link_search_path.display() + ); + + if cfg!(target_os = "windows") { + println!("cargo:rustc-link-lib=libcef"); + } else if cfg!(target_os = "linux") { + println!("cargo:rustc-link-lib=cef"); + } else if cfg!(target_os = "macos") { + println!("cargo:rustc-link-lib=framework=AppKit"); + println!("cargo:rustc-link-lib=sandbox"); + + // FIXME: Failed to link to `cef_sandbox.a` on macOS/ + // + // Workaround: copy `cef_sandbox.a` to the output directory and link it as + // `libcef_sandbox.a` + // + // https://github.com/rust-lang/rust/issues/132264 + let outdir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + std::fs::copy( + cef_link_search_path.join("cef_sandbox.a"), + outdir.join("libcef_sandbox.a"), + ) + .expect("copy cef_sandbox.a"); + println!("cargo:rustc-link-search=native={}", outdir.display()); + println!("cargo:rustc-link-lib=static=cef_sandbox"); + } + + build_dll_wrapper(&cef_root); + build_wef_sys(&cef_root); +} + +fn build_dll_wrapper(cef_root: &Path) { + #[allow(unused_mut)] + let mut sources = vec![ + "shutdown_checker.cc", + "transfer_util.cc", + "base/cef_atomic_flag.cc", + "base/cef_callback_helpers.cc", + "base/cef_callback_internal.cc", + "base/cef_dump_without_crashing.cc", + "base/cef_lock.cc", + "base/cef_lock_impl.cc", + "base/cef_logging.cc", + "base/cef_ref_counted.cc", + "base/cef_thread_checker_impl.cc", + "base/cef_weak_ptr.cc", + "cpptoc/accessibility_handler_cpptoc.cc", + "cpptoc/app_cpptoc.cc", + "cpptoc/audio_handler_cpptoc.cc", + "cpptoc/base_ref_counted_cpptoc.cc", + "cpptoc/base_scoped_cpptoc.cc", + "cpptoc/browser_process_handler_cpptoc.cc", + "cpptoc/client_cpptoc.cc", + "cpptoc/command_handler_cpptoc.cc", + "cpptoc/completion_callback_cpptoc.cc", + "cpptoc/context_menu_handler_cpptoc.cc", + "cpptoc/cookie_access_filter_cpptoc.cc", + "cpptoc/cookie_visitor_cpptoc.cc", + "cpptoc/delete_cookies_callback_cpptoc.cc", + "cpptoc/dev_tools_message_observer_cpptoc.cc", + "cpptoc/dialog_handler_cpptoc.cc", + "cpptoc/display_handler_cpptoc.cc", + "cpptoc/domvisitor_cpptoc.cc", + "cpptoc/download_handler_cpptoc.cc", + "cpptoc/download_image_callback_cpptoc.cc", + "cpptoc/drag_handler_cpptoc.cc", + "cpptoc/end_tracing_callback_cpptoc.cc", + "cpptoc/find_handler_cpptoc.cc", + "cpptoc/focus_handler_cpptoc.cc", + "cpptoc/frame_handler_cpptoc.cc", + "cpptoc/jsdialog_handler_cpptoc.cc", + "cpptoc/keyboard_handler_cpptoc.cc", + "cpptoc/life_span_handler_cpptoc.cc", + "cpptoc/load_handler_cpptoc.cc", + "cpptoc/media_observer_cpptoc.cc", + "cpptoc/media_route_create_callback_cpptoc.cc", + "cpptoc/media_sink_device_info_callback_cpptoc.cc", + "cpptoc/menu_model_delegate_cpptoc.cc", + "cpptoc/navigation_entry_visitor_cpptoc.cc", + "cpptoc/pdf_print_callback_cpptoc.cc", + "cpptoc/permission_handler_cpptoc.cc", + "cpptoc/preference_observer_cpptoc.cc", + "cpptoc/print_handler_cpptoc.cc", + "cpptoc/read_handler_cpptoc.cc", + "cpptoc/render_handler_cpptoc.cc", + "cpptoc/render_process_handler_cpptoc.cc", + "cpptoc/request_context_handler_cpptoc.cc", + "cpptoc/request_handler_cpptoc.cc", + "cpptoc/resolve_callback_cpptoc.cc", + "cpptoc/resource_bundle_handler_cpptoc.cc", + "cpptoc/resource_handler_cpptoc.cc", + "cpptoc/resource_request_handler_cpptoc.cc", + "cpptoc/response_filter_cpptoc.cc", + "cpptoc/run_file_dialog_callback_cpptoc.cc", + "cpptoc/scheme_handler_factory_cpptoc.cc", + "cpptoc/server_handler_cpptoc.cc", + "cpptoc/set_cookie_callback_cpptoc.cc", + "cpptoc/setting_observer_cpptoc.cc", + "cpptoc/string_visitor_cpptoc.cc", + "cpptoc/task_cpptoc.cc", + "cpptoc/urlrequest_client_cpptoc.cc", + "cpptoc/v8_accessor_cpptoc.cc", + "cpptoc/v8_array_buffer_release_callback_cpptoc.cc", + "cpptoc/v8_handler_cpptoc.cc", + "cpptoc/v8_interceptor_cpptoc.cc", + "cpptoc/write_handler_cpptoc.cc", + "cpptoc/views/browser_view_delegate_cpptoc.cc", + "cpptoc/views/button_delegate_cpptoc.cc", + "cpptoc/views/menu_button_delegate_cpptoc.cc", + "cpptoc/views/panel_delegate_cpptoc.cc", + "cpptoc/views/textfield_delegate_cpptoc.cc", + "cpptoc/views/view_delegate_cpptoc.cc", + "cpptoc/views/window_delegate_cpptoc.cc", + "ctocpp/auth_callback_ctocpp.cc", + "ctocpp/before_download_callback_ctocpp.cc", + "ctocpp/binary_value_ctocpp.cc", + "ctocpp/browser_ctocpp.cc", + "ctocpp/browser_host_ctocpp.cc", + "ctocpp/callback_ctocpp.cc", + "ctocpp/command_line_ctocpp.cc", + "ctocpp/context_menu_params_ctocpp.cc", + "ctocpp/cookie_manager_ctocpp.cc", + "ctocpp/dictionary_value_ctocpp.cc", + "ctocpp/domdocument_ctocpp.cc", + "ctocpp/domnode_ctocpp.cc", + "ctocpp/download_item_callback_ctocpp.cc", + "ctocpp/download_item_ctocpp.cc", + "ctocpp/drag_data_ctocpp.cc", + "ctocpp/file_dialog_callback_ctocpp.cc", + "ctocpp/frame_ctocpp.cc", + "ctocpp/image_ctocpp.cc", + "ctocpp/jsdialog_callback_ctocpp.cc", + "ctocpp/list_value_ctocpp.cc", + "ctocpp/media_access_callback_ctocpp.cc", + "ctocpp/media_route_ctocpp.cc", + "ctocpp/media_router_ctocpp.cc", + "ctocpp/media_sink_ctocpp.cc", + "ctocpp/media_source_ctocpp.cc", + "ctocpp/menu_model_ctocpp.cc", + "ctocpp/navigation_entry_ctocpp.cc", + "ctocpp/permission_prompt_callback_ctocpp.cc", + "ctocpp/post_data_ctocpp.cc", + "ctocpp/post_data_element_ctocpp.cc", + "ctocpp/preference_manager_ctocpp.cc", + "ctocpp/preference_registrar_ctocpp.cc", + "ctocpp/print_dialog_callback_ctocpp.cc", + "ctocpp/print_job_callback_ctocpp.cc", + "ctocpp/print_settings_ctocpp.cc", + "ctocpp/process_message_ctocpp.cc", + "ctocpp/registration_ctocpp.cc", + "ctocpp/request_context_ctocpp.cc", + "ctocpp/request_ctocpp.cc", + "ctocpp/resource_bundle_ctocpp.cc", + "ctocpp/resource_read_callback_ctocpp.cc", + "ctocpp/resource_skip_callback_ctocpp.cc", + "ctocpp/response_ctocpp.cc", + "ctocpp/run_context_menu_callback_ctocpp.cc", + "ctocpp/run_quick_menu_callback_ctocpp.cc", + "ctocpp/scheme_registrar_ctocpp.cc", + "ctocpp/select_client_certificate_callback_ctocpp.cc", + "ctocpp/server_ctocpp.cc", + "ctocpp/shared_memory_region_ctocpp.cc", + "ctocpp/shared_process_message_builder_ctocpp.cc", + "ctocpp/sslinfo_ctocpp.cc", + "ctocpp/sslstatus_ctocpp.cc", + "ctocpp/stream_reader_ctocpp.cc", + "ctocpp/stream_writer_ctocpp.cc", + "ctocpp/task_manager_ctocpp.cc", + "ctocpp/task_runner_ctocpp.cc", + "ctocpp/thread_ctocpp.cc", + "ctocpp/unresponsive_process_callback_ctocpp.cc", + "ctocpp/urlrequest_ctocpp.cc", + "ctocpp/v8_context_ctocpp.cc", + "ctocpp/v8_exception_ctocpp.cc", + "ctocpp/v8_stack_frame_ctocpp.cc", + "ctocpp/v8_stack_trace_ctocpp.cc", + "ctocpp/v8_value_ctocpp.cc", + "ctocpp/value_ctocpp.cc", + "ctocpp/waitable_event_ctocpp.cc", + "ctocpp/x509_cert_principal_ctocpp.cc", + "ctocpp/x509_certificate_ctocpp.cc", + "ctocpp/xml_reader_ctocpp.cc", + "ctocpp/zip_reader_ctocpp.cc", + "ctocpp/views/box_layout_ctocpp.cc", + "ctocpp/views/browser_view_ctocpp.cc", + "ctocpp/views/button_ctocpp.cc", + "ctocpp/views/display_ctocpp.cc", + "ctocpp/views/fill_layout_ctocpp.cc", + "ctocpp/views/label_button_ctocpp.cc", + "ctocpp/views/layout_ctocpp.cc", + "ctocpp/views/menu_button_ctocpp.cc", + "ctocpp/views/menu_button_pressed_lock_ctocpp.cc", + "ctocpp/views/overlay_controller_ctocpp.cc", + "ctocpp/views/panel_ctocpp.cc", + "ctocpp/views/scroll_view_ctocpp.cc", + "ctocpp/views/textfield_ctocpp.cc", + "ctocpp/views/view_ctocpp.cc", + "ctocpp/views/window_ctocpp.cc", + "wrapper/cef_byte_read_handler.cc", + "wrapper/cef_closure_task.cc", + "wrapper/cef_message_router.cc", + "wrapper/cef_message_router_utils.cc", + "wrapper/cef_resource_manager.cc", + "wrapper/cef_scoped_temp_dir.cc", + "wrapper/cef_stream_resource_handler.cc", + "wrapper/cef_xml_object.cc", + "wrapper/cef_zip_archive.cc", + "wrapper/libcef_dll_wrapper.cc", + "wrapper/libcef_dll_wrapper2.cc", + ]; + + if cfg!(target_os = "macos") { + sources.extend([ + "wrapper/cef_library_loader_mac.mm", + "wrapper/libcef_dll_dylib.cc", + ]); + } + + let sources = sources + .into_iter() + .map(|path| cef_root.join("libcef_dll").join(path)); + + Build::new() + .cpp(true) + .std("c++17") + .cargo_warnings(false) + .include(cef_root) + .define("NOMINMAX", None) + .define("WRAPPING_CEF_SHARED", None) + .files(sources) + .compile("cef-dll-wrapper"); +} + +fn build_wef_sys(cef_root: &Path) { + println!("cargo::rerun-if-changed=cpp"); + + #[allow(unused_mut)] + let mut sources = vec![ + "cpp/wef.cpp", + "cpp/client.cpp", + "cpp/dirty_rect.cpp", + "cpp/frame.cpp", + "cpp/file_dialog.cpp", + "cpp/cursor.cpp", + "cpp/js_dialog.cpp", + "cpp/query.cpp", + "cpp/external_pump.cpp", + ]; + + if cfg!(target_os = "windows") { + sources.extend(["cpp/external_pump_win.cpp"]); + } else if cfg!(target_os = "macos") { + sources.extend([ + "cpp/load_library.cpp", + "cpp/sandbox_context.cpp", + "cpp/external_pump_mac.mm", + ]); + } else if cfg!(target_os = "linux") { + sources.extend(["cpp/external_pump_linux.cpp"]); + } + + let mut build = Build::new(); + + if cfg!(target_os = "linux") { + build.includes( + pkg_config::probe_library("glib-2.0") + .unwrap_or_else(|err| panic!("failed to find glib-2.0: {}", err)) + .include_paths, + ); + } + + build + .cpp(true) + .std("c++17") + .cargo_warnings(false) + .files(sources) + .include(cef_root) + .define("NOMINMAX", None) + .compile("wef-sys"); +} \ No newline at end of file diff --git a/wef/cpp/app.h b/wef/cpp/app.h new file mode 100644 index 0000000..18060e8 --- /dev/null +++ b/wef/cpp/app.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include "app_callbacks.h" +#include "external_pump.h" +#include "include/cef_app.h" +#include "include/wrapper/cef_message_router.h" +#include "utils.h" + +const int64_t MAX_TIMER_DELAY = 1000 / 60; + +class WefApp : public CefApp, public CefBrowserProcessHandler { + IMPLEMENT_REFCOUNTING(WefApp); + + private: + std::optional> external_pump_; + AppCallbacks callbacks_; + void* userdata_; + DestroyFn destroy_userdata_; + + public: + WefApp(AppCallbacks callbacks, void* userdata, DestroyFn destroy_userdata) + : callbacks_(callbacks), + userdata_(userdata), + destroy_userdata_(destroy_userdata), + external_pump_(std::make_optional(ExternalPump::Create())) {} + + virtual ~WefApp() { + if (destroy_userdata_) { + destroy_userdata_(userdata_); + userdata_ = nullptr; + } + } + + ///////////////////////////////////////////////////////////////// + // CefApp methods + ///////////////////////////////////////////////////////////////// + virtual void OnBeforeCommandLineProcessing( + const CefString& process_type, + CefRefPtr command_line) override { + if (process_type.empty()) { + // 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. + // + // NOTE: If GPU rendering is not disabled, sometimes there will be issues + // with incorrect dimensions when changing the window size. + command_line->AppendSwitch("disable-gpu"); + command_line->AppendSwitch("disable-gpu-compositing"); + } + +#ifdef __APPLE__ + command_line->AppendSwitch("use-mock-keychain"); +#endif + } + + CefRefPtr GetBrowserProcessHandler() override { + return this; + } + + ///////////////////////////////////////////////////////////////// + // CefBrowserProcessHandler methods + ///////////////////////////////////////////////////////////////// + bool OnAlreadyRunningAppRelaunch( + CefRefPtr command_line, + const CefString& current_directory) override { + return true; + } + + void OnScheduleMessagePumpWork(int64_t delay_ms) override { + if (external_pump_) { + (*external_pump_)->OnScheduleMessagePumpWork(delay_ms); + } + + callbacks_.on_schedule_message_pump_work( + userdata_, static_cast(std::min(delay_ms, MAX_TIMER_DELAY))); + } +}; diff --git a/wef/cpp/app_callbacks.h b/wef/cpp/app_callbacks.h new file mode 100644 index 0000000..3fb4716 --- /dev/null +++ b/wef/cpp/app_callbacks.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +struct AppCallbacks { + void (*on_schedule_message_pump_work)(void* userdata, int delay_ms); +}; diff --git a/wef/cpp/app_render_process.h b/wef/cpp/app_render_process.h new file mode 100644 index 0000000..3d3c243 --- /dev/null +++ b/wef/cpp/app_render_process.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include "include/cef_app.h" +#include "include/wrapper/cef_message_router.h" + +class WefRenderProcessHandler : public CefRenderProcessHandler { + IMPLEMENT_REFCOUNTING(WefRenderProcessHandler); + + private: + CefRefPtr message_router_; + std::map inject_javascript_map_; + + public: + WefRenderProcessHandler() { + CefMessageRouterConfig config; + message_router_ = CefMessageRouterRendererSide::Create(config); + } + + void OnBrowserCreated(CefRefPtr browser, + CefRefPtr extra_info) override { + auto inject_javascript = extra_info->GetString("__wef_inject_javascript"); + inject_javascript_map_[browser->GetIdentifier()] = inject_javascript; + } + + void OnBrowserDestroyed(CefRefPtr browser) override { + inject_javascript_map_.erase(browser->GetIdentifier()); + } + + void OnContextCreated(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) override { + if (frame->IsMain()) { + auto it = inject_javascript_map_.find(browser->GetIdentifier()); + if (it != inject_javascript_map_.end()) { + auto inject_javascript = it->second; + if (!inject_javascript.empty()) { + frame->ExecuteJavaScript(inject_javascript, frame->GetURL(), 0); + } + } + } + message_router_->OnContextCreated(browser, frame, context); + } + + void OnContextReleased(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) override { + message_router_->OnContextReleased(browser, frame, context); + } + + bool OnProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override { + return message_router_->OnProcessMessageReceived(browser, frame, + source_process, message); + } +}; + +class WefRenderProcessApp : public CefApp, public CefRenderProcessHandler { + IMPLEMENT_REFCOUNTING(WefRenderProcessApp); + + private: + CefRefPtr render_process_handler_; + + public: + WefRenderProcessApp() + : render_process_handler_(new WefRenderProcessHandler()) {} + + CefRefPtr GetRenderProcessHandler() { + return render_process_handler_; + } +}; \ No newline at end of file diff --git a/wef/cpp/browser_callbacks.h b/wef/cpp/browser_callbacks.h new file mode 100644 index 0000000..c0f3d66 --- /dev/null +++ b/wef/cpp/browser_callbacks.h @@ -0,0 +1,67 @@ +#pragma once + +#include "include/cef_client.h" +#include "include/cef_frame.h" +#include "include/wrapper/cef_message_router.h" + +struct _ContextMenuParams { + int x_crood; + int y_crood; + int type_flags; + const char* link_url; + const char* unfiltered_link_url; + const char* source_url; + bool has_image_contents; + const char* title_text; + const char* page_url; + const char* frame_url; + int media_type; + int media_state_flags; + const char* selection_text; + bool is_editable; + int edit_state_flags; +}; + +struct BrowserCallbacks { + void (*on_created)(void* userdata); + void (*on_closed)(void* userdata); + void (*on_popup_show)(void* userdata, bool show); + void (*on_popup_position)(void* userdata, const CefRect* rect); + void (*on_paint)(void* userdata, int type, const void* dirty_rects, + const void* buffer, unsigned int width, unsigned int height); + void (*on_address_changed)(void* userdata, void* frame, const char* url); + void (*on_title_changed)(void* userdata, const char* title); + void (*on_favicon_url_change)(void* userdata, const char** urls, int); + void (*on_tooltip)(void* userdata, const char* text); + void (*on_status_message)(void* userdata, const char* text); + void (*on_console_message)(void* userdata, const char* message, int level, + const char* source, int line); + bool (*on_cursor_changed)(void* userdata, int cursor_type, + const void* custom_cursor_info); + void (*on_before_popup)(void* userdata, const char* url); + void (*on_loading_progress_changed)(void* userdata, float progress); + void (*on_loading_state_changed)(void* userdata, bool is_loading, + bool can_go_back, bool can_go_forward); + void (*on_load_start)(void* userdata, void* frame); + void (*on_load_end)(void* userdata, void* frame); + void (*on_load_error)(void* userdata, void* frame, const char* error_text, + const char* failed_url); + void (*on_ime_composition_range_changed)(void* userdata, const CefRect* rect); + bool (*on_file_dialog)(void* userdata, int mode, const char* title, + const char* default_file_path, + const char* accept_filters, + const char* accept_extensions, + const char* accept_descriptions, + CefRefPtr* callback); + void (*on_context_menu)(void* userdata, void* frame, + const _ContextMenuParams* params); + void (*on_find_result)(void* userdata, int identifier, int count, + const CefRect* selection_rect, + int active_match_ordinal, bool final_update); + bool (*on_js_dialog)(void* userdata, int type, const char* message_text, + const char* default_prompt_text, + CefRefPtr* callback); + void (*on_query)( + void* userdata, void* frame, const char* payload, + CefRefPtr* callback); +}; \ No newline at end of file diff --git a/wef/cpp/client.cpp b/wef/cpp/client.cpp new file mode 100644 index 0000000..48f02bf --- /dev/null +++ b/wef/cpp/client.cpp @@ -0,0 +1,487 @@ +#include "client.h" + +#include + +#include "include/base/cef_bind.h" +#include "include/base/cef_callback.h" +#include "include/cef_browser.h" +#include "include/cef_task.h" +#include "include/wrapper/cef_closure_task.h" + +WefClient::WefClient(std::shared_ptr state) + : state_(state) {} + +WefClient::~WefClient() { + if (state_->browser) { + (*state_->browser)->GetHost()->CloseBrowser(true); + } +} + +///////////////////////////////////////////////////////////////// +// CefRenderHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnPopupShow(CefRefPtr browser, bool show) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_popup_show(userdata, show); + }); +} + +void WefClient::OnPopupSize(CefRefPtr browser, + const CefRect& rect) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_popup_position(userdata, &rect); + }); +} + +void WefClient::OnPaint(CefRefPtr browser, PaintElementType type, + const RectList& dirtyRects, const void* buffer, + int width, int height) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_paint(userdata, static_cast(type), &dirtyRects, + buffer, static_cast(width), + static_cast(height)); + }); +} + +void WefClient::OnImeCompositionRangeChanged(CefRefPtr browser, + const CefRange& selected_range, + const RectList& character_bounds) { + DCHECK(CefCurrentlyOn(TID_UI)); + + int xmin = std::numeric_limits::max(); + int ymin = std::numeric_limits::max(); + int xmax = std::numeric_limits::min(); + int ymax = std::numeric_limits::min(); + + for (const auto& r : character_bounds) { + if (r.x < xmin) { + xmin = r.x; + } + if (r.y < ymin) { + ymin = r.y; + } + if (r.x + r.width > xmax) { + xmax = r.x + r.width; + } + if (r.y + r.height > ymax) { + ymax = r.y + r.height; + } + } + + CefRect rect{int(float(xmin)), int(float(ymin)), int(float(xmax - xmin)), + int(float(ymax - ymin))}; + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_ime_composition_range_changed(userdata, &rect); + }); +} + +bool WefClient::OnCursorChange(CefRefPtr browser, + CefCursorHandle cursor, cef_cursor_type_t type, + const CefCursorInfo& custom_cursor_info) { + DCHECK(CefCurrentlyOn(TID_UI)); + + bool result = false; + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + result = callbacks.on_cursor_changed( + userdata, static_cast(type), + type == CT_CUSTOM ? &custom_cursor_info : nullptr); + }); + return result; +} + +///////////////////////////////////////////////////////////////// +// CefDisplayHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnAddressChange(CefRefPtr browser, + CefRefPtr frame, + const CefString& url) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto url_str = url.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_address_changed(userdata, new WefFrame{frame}, + url_str.c_str()); + }); +} + +void WefClient::OnTitleChange(CefRefPtr browser, + const CefString& title) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto title_str = title.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_title_changed(userdata, title_str.c_str()); + }); +} + +void WefClient::OnFaviconURLChange(CefRefPtr browser, + const std::vector& icon_urls) { + DCHECK(CefCurrentlyOn(TID_UI)); + + std::vector str_urls; + std::transform(icon_urls.begin(), icon_urls.end(), + std::back_inserter(str_urls), + [](const CefString& url) { return url.ToString(); }); + + std::vector cstr_urls; + std::transform(str_urls.begin(), str_urls.end(), + std::back_inserter(cstr_urls), + [](const std::string& url) { return url.c_str(); }); + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_favicon_url_change(userdata, cstr_urls.data(), + static_cast(cstr_urls.size())); + }); +} + +bool WefClient::OnTooltip(CefRefPtr browser, CefString& text) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto text_str = text.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_tooltip(userdata, text_str.c_str()); + }); + return true; +} + +void WefClient::OnStatusMessage(CefRefPtr browser, + const CefString& value) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto text_str = value.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_status_message(userdata, text_str.c_str()); + }); +} + +bool WefClient::OnConsoleMessage(CefRefPtr browser, + cef_log_severity_t level, + const CefString& message, + const CefString& source, int line) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto message_str = message.ToString(); + auto source_str = source.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_console_message(userdata, message_str.c_str(), + static_cast(level), + source_str.c_str(), line); + }); + return false; +} + +void WefClient::OnLoadingProgressChange(CefRefPtr browser, + double progress) { + DCHECK(CefCurrentlyOn(TID_UI)); + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_loading_progress_changed(userdata, + static_cast(progress)); + }); +} + +///////////////////////////////////////////////////////////////// +// CefLifeSpanHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnAfterCreated(CefRefPtr browser) { + CefMessageRouterConfig config; + message_router_ = CefMessageRouterBrowserSide::Create(config); + message_router_->AddHandler(this, false); + + state_->browser = browser; + state_->browser_state = BrowserState::Created; + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_created(userdata); + }); + + if (state_->browser_state == BrowserState::Closed) { + CefPostTask(TID_UI, base::BindOnce(&CefBrowserHost::CloseBrowser, + browser->GetHost(), false)); + } +} + +bool WefClient::OnBeforePopup( + CefRefPtr browser, CefRefPtr frame, int popup_id, + const CefString& target_url, const CefString& target_frame_name, + CefLifeSpanHandler::WindowOpenDisposition target_disposition, + bool user_gesture, const CefPopupFeatures& popupFeatures, + CefWindowInfo& windowInfo, CefRefPtr& client, + CefBrowserSettings& settings, CefRefPtr& extra_info, + bool* no_javascript_access) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto target_url_str = target_url.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_before_popup(userdata, target_url_str.c_str()); + }); + return true; +} + +bool WefClient::DoClose(CefRefPtr browser) { return false; } + +void WefClient::OnBeforeClose(CefRefPtr browser) { + DCHECK(CefCurrentlyOn(TID_UI)); + + message_router_->OnBeforeClose(browser); + + state_->browser_state = BrowserState::Closed; + state_->browser = std::nullopt; + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_closed(userdata); + }); +} + +///////////////////////////////////////////////////////////////// +// CefLoadHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnLoadingStateChange(CefRefPtr browser, + bool isLoading, bool canGoBack, + bool canGoForward) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_loading_state_changed(userdata, isLoading, canGoBack, + canGoForward); + }); +} + +void WefClient::OnLoadStart(CefRefPtr browser, + CefRefPtr frame, + TransitionType transition_type) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_load_start(userdata, new WefFrame{frame}); + }); +} + +void WefClient::OnLoadEnd(CefRefPtr browser, + CefRefPtr frame, int httpStatusCode) { + DCHECK(CefCurrentlyOn(TID_UI)); + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_load_end(userdata, new WefFrame{frame}); + }); + + if (state_->browser) { + (*state_->browser)->GetHost()->SetFocus(state_->focus); + } +} + +void WefClient::OnLoadError(CefRefPtr browser, + CefRefPtr frame, ErrorCode errorCode, + const CefString& errorText, + const CefString& failedUrl) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto error_text_str = errorText.ToString(); + auto failed_url_str = failedUrl.ToString(); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_load_error(userdata, new WefFrame{frame}, + error_text_str.c_str(), failed_url_str.c_str()); + }); +} + +///////////////////////////////////////////////////////////////// +// CefDialogHandler methods +///////////////////////////////////////////////////////////////// +bool WefClient::OnFileDialog(CefRefPtr browser, FileDialogMode mode, + const CefString& title, + const CefString& default_file_path, + const std::vector& accept_filters, + const std::vector& accept_extensions, + const std::vector& accept_descriptions, + CefRefPtr callback) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto title_str = title.ToString(); + auto default_file_path_str = default_file_path.ToString(); + auto accept_filters_str = join_strings(accept_filters, "@@@"); + auto accept_extensions_str = join_strings(accept_extensions, "@@@"); + auto accept_descriptions_str = join_strings(accept_descriptions, "@@@"); + CefRefPtr* callback_ptr = + new CefRefPtr(callback); + bool result = false; + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + result = callbacks.on_file_dialog( + userdata, static_cast(mode), title_str.c_str(), + default_file_path_str.c_str(), accept_filters_str.c_str(), + accept_extensions_str.c_str(), accept_descriptions_str.c_str(), + callback_ptr); + }); + return result; +} + +///////////////////////////////////////////////////////////////// +// CefContextMenuHandler methods +///////////////////////////////////////////////////////////////// +bool WefClient::RunContextMenu(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr params, + CefRefPtr model, + CefRefPtr callback) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto link_url_str = params->GetLinkUrl().ToString(); + auto unfiltered_link_url_str = params->GetUnfilteredLinkUrl().ToString(); + auto source_url_str = params->GetSourceUrl().ToString(); + auto title_text_str = params->GetTitleText().ToString(); + auto page_url_str = params->GetPageUrl().ToString(); + auto frame_url_str = params->GetFrameUrl().ToString(); + auto selection_text_str = params->GetSelectionText().ToString(); + + _ContextMenuParams params_{ + params->GetXCoord(), + params->GetYCoord(), + static_cast(params->GetTypeFlags()), + !link_url_str.empty() ? link_url_str.c_str() : nullptr, + !unfiltered_link_url_str.empty() ? unfiltered_link_url_str.c_str() + : nullptr, + !source_url_str.empty() ? source_url_str.c_str() : nullptr, + params->HasImageContents(), + !title_text_str.empty() ? title_text_str.c_str() : nullptr, + page_url_str.c_str(), + frame_url_str.c_str(), + static_cast(params->GetMediaType()), + static_cast(params->GetMediaStateFlags()), + selection_text_str.c_str(), + params->IsEditable(), + static_cast(params->GetEditStateFlags()), + }; + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_context_menu(userdata, new WefFrame{frame}, ¶ms_); + }); + return true; +} + +///////////////////////////////////////////////////////////////// +// CefFindHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnFindResult(CefRefPtr browser, int identifier, + int count, const CefRect& selectionRect, + int activeMatchOrdinal, bool finalUpdate) { + DCHECK(CefCurrentlyOn(TID_UI)); + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + callbacks.on_find_result(userdata, identifier, count, &selectionRect, + activeMatchOrdinal, finalUpdate); + }); +} + +///////////////////////////////////////////////////////////////// +// CefJSDialogHandler methods +///////////////////////////////////////////////////////////////// +bool WefClient::OnJSDialog(CefRefPtr browser, + const CefString& origin_url, + JSDialogType dialog_type, + const CefString& message_text, + const CefString& default_prompt_text, + CefRefPtr callback, + bool& suppress_message) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto message_text_str = message_text.ToString(); + auto default_prompt_text_str = default_prompt_text.ToString(); + CefRefPtr* callback_ptr = + new CefRefPtr(callback); + + bool result = false; + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + result = callbacks.on_js_dialog( + userdata, static_cast(dialog_type), message_text_str.c_str(), + default_prompt_text_str.c_str(), callback_ptr); + }); + return result; +} + +bool WefClient::OnBeforeUnloadDialog(CefRefPtr browser, + const CefString& message_text, + bool is_reload, + CefRefPtr callback) { + callback->Continue(true, ""); + return true; +} + +///////////////////////////////////////////////////////////////// +// CefRequestHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnRenderProcessTerminated(CefRefPtr browser, + TerminationStatus status, + int error_code, + const CefString& error_string) { + message_router_->OnRenderProcessTerminated(browser); +} + +bool WefClient::OnBeforeBrowse(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr request, bool user_gesture, + bool is_redirect) { + message_router_->OnBeforeBrowse(browser, frame); + return false; +} + +///////////////////////////////////////////////////////////////// +// CefFocusHandler methods +///////////////////////////////////////////////////////////////// +void WefClient::OnTakeFocus(CefRefPtr browser, bool next) {} + +bool WefClient::OnSetFocus(CefRefPtr browser, FocusSource source) { + return false; +} + +///////////////////////////////////////////////////////////////// +// CefPermissionHandler methods +///////////////////////////////////////////////////////////////// +bool WefClient::OnRequestMediaAccessPermission( + CefRefPtr browser, CefRefPtr frame, + const CefString& requesting_origin, uint32_t requested_permissions, + CefRefPtr callback) { + callback->Continue(CEF_MEDIA_PERMISSION_NONE); + return true; +} + +///////////////////////////////////////////////////////////////// +// CefMessageRouterBrowserSide::Handler methods +///////////////////////////////////////////////////////////////// +bool WefClient::OnQuery( + CefRefPtr browser, CefRefPtr frame, int64_t query_id, + const CefString& request, bool persistent, + CefRefPtr callback) { + DCHECK(CefCurrentlyOn(TID_UI)); + + auto request_str = request.ToString(); + CefRefPtr* callback_ptr = + new CefRefPtr(callback); + + state_->callbacks_target.call( + [&](const BrowserCallbacks& callbacks, void* userdata) { + return callbacks.on_query(userdata, new WefFrame{frame}, + request_str.c_str(), callback_ptr); + }); + return true; +} diff --git a/wef/cpp/client.h b/wef/cpp/client.h new file mode 100644 index 0000000..6bef086 --- /dev/null +++ b/wef/cpp/client.h @@ -0,0 +1,291 @@ +#pragma once + +#if defined(_WIN32) || defined(_WIN64) +#define NOMINMAX +#endif + +#include +#include +#include +#include + +#include "browser_callbacks.h" +#include "frame.h" +#include "include/cef_browser.h" +#include "include/cef_client.h" +#include "include/wrapper/cef_message_router.h" +#include "utils.h" + +struct WefBrowser; + +enum class BrowserState { + Creating, + Created, + Closing, + Closed, +}; + +class BrowserCallbacksTarget { + private: + bool disable_; + BrowserCallbacks callbacks_; + void* userdata_; + DestroyFn destroy_userdata_; + + public: + BrowserCallbacksTarget(BrowserCallbacks callbacks, void* userdata, + DestroyFn destroy_userdata) + : disable_(false), + callbacks_(callbacks), + userdata_(userdata), + destroy_userdata_(destroy_userdata) {} + + BrowserCallbacksTarget(const BrowserCallbacksTarget& other) = delete; + BrowserCallbacksTarget& operator=(const BrowserCallbacksTarget& other) = + delete; + + BrowserCallbacksTarget(BrowserCallbacksTarget&& other) + : disable_(other.disable_), + callbacks_(std::move(other.callbacks_)), + userdata_(other.userdata_), + destroy_userdata_(other.destroy_userdata_) { + other.disable_ = true; + other.userdata_ = nullptr; + other.destroy_userdata_ = nullptr; + } + + ~BrowserCallbacksTarget() { + if (destroy_userdata_) { + destroy_userdata_(userdata_); + } + } + + void disable() { disable_ = true; } + + template + void call(Callable func) const { + if (disable_) { + return; + } + func(callbacks_, userdata_); + } +}; + +struct BrowserSharedState { + bool focus; + int cursorX, cursorY; + BrowserState browser_state; + std::optional> browser; + int width, height; + float device_scale_factor; + BrowserCallbacksTarget callbacks_target; + + BrowserSharedState(BrowserCallbacksTarget&& other) + : focus(false), + cursorX(0), + cursorY(0), + browser_state(BrowserState::Creating), + width(800), + height(600), + device_scale_factor(1.0f), + callbacks_target(std::move(other)) {} + + BrowserSharedState(const BrowserSharedState& other) = delete; + BrowserSharedState& operator=(const BrowserSharedState& other) = delete; +}; + +class WefClient : public CefClient, + public CefRenderHandler, + public CefDisplayHandler, + public CefLifeSpanHandler, + public CefLoadHandler, + public CefDialogHandler, + public CefFindHandler, + public CefContextMenuHandler, + public CefRequestHandler, + public CefJSDialogHandler, + public CefFocusHandler, + public CefPermissionHandler, + public CefMessageRouterBrowserSide::Handler { + IMPLEMENT_REFCOUNTING(WefClient); + + private: + std::shared_ptr state_; + CefRefPtr message_router_; + + public: + WefClient(std::shared_ptr state); + + virtual ~WefClient(); + + ///////////////////////////////////////////////////////////////// + // CefClient methods + ///////////////////////////////////////////////////////////////// + + bool GetScreenInfo(CefRefPtr browser, + CefScreenInfo& screen_info) override { + screen_info.device_scale_factor = state_->device_scale_factor; + return true; + } + + void GetViewRect(CefRefPtr browser, CefRect& rect) override { + rect.Set(0, 0, + static_cast(static_cast(state_->width) / + state_->device_scale_factor), + static_cast(static_cast(state_->height) / + state_->device_scale_factor)); + } + + CefRefPtr GetRenderHandler() override { return this; } + CefRefPtr GetDisplayHandler() override { return this; } + CefRefPtr GetLifeSpanHandler() override { return this; } + CefRefPtr GetLoadHandler() override { return this; } + CefRefPtr GetDialogHandler() override { return this; } + CefRefPtr GetContextMenuHandler() override { + return this; + } + CefRefPtr GetFindHandler() override { return this; } + CefRefPtr GetJSDialogHandler() override { return this; } + CefRefPtr GetFocusHandler() override { return this; } + CefRefPtr GetPermissionHandler() override { + return this; + } + + bool OnProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override { + return message_router_->OnProcessMessageReceived(browser, frame, + source_process, message); + } + + ///////////////////////////////////////////////////////////////// + // CefRenderHandler methods + ///////////////////////////////////////////////////////////////// + void OnPopupShow(CefRefPtr browser, bool show) override; + void OnPopupSize(CefRefPtr browser, const CefRect& rect) override; + void OnPaint(CefRefPtr browser, PaintElementType type, + const RectList& dirtyRects, const void* buffer, int width, + int height) override; + void OnImeCompositionRangeChanged(CefRefPtr browser, + const CefRange& selected_range, + const RectList& character_bounds) override; + bool OnCursorChange(CefRefPtr browser, CefCursorHandle cursor, + cef_cursor_type_t type, + const CefCursorInfo& custom_cursor_info) override; + + ///////////////////////////////////////////////////////////////// + // CefDisplayHandler methods + ///////////////////////////////////////////////////////////////// + void OnAddressChange(CefRefPtr browser, CefRefPtr frame, + const CefString& url) override; + void OnTitleChange(CefRefPtr browser, + const CefString& title) override; + void OnFaviconURLChange(CefRefPtr browser, + const std::vector& icon_urls) override; + bool OnTooltip(CefRefPtr browser, CefString& text) override; + void OnStatusMessage(CefRefPtr browser, + const CefString& value) override; + bool OnConsoleMessage(CefRefPtr browser, cef_log_severity_t level, + const CefString& message, const CefString& source, + int line) override; + void OnLoadingProgressChange(CefRefPtr browser, + double progress) override; + + ///////////////////////////////////////////////////////////////// + // CefLifeSpanHandler methods + ///////////////////////////////////////////////////////////////// + void OnAfterCreated(CefRefPtr browser) override; + bool OnBeforePopup( + CefRefPtr browser, CefRefPtr frame, int popup_id, + const CefString& target_url, const CefString& target_frame_name, + CefLifeSpanHandler::WindowOpenDisposition target_disposition, + bool user_gesture, const CefPopupFeatures& popupFeatures, + CefWindowInfo& windowInfo, CefRefPtr& client, + CefBrowserSettings& settings, CefRefPtr& extra_info, + bool* no_javascript_access) override; + bool DoClose(CefRefPtr browser) override; + void OnBeforeClose(CefRefPtr browser) override; + + ///////////////////////////////////////////////////////////////// + // CefLoadHandler methods + ///////////////////////////////////////////////////////////////// + void OnLoadingStateChange(CefRefPtr browser, bool isLoading, + bool canGoBack, bool canGoForward) override; + void OnLoadStart(CefRefPtr browser, CefRefPtr frame, + TransitionType transition_type) override; + void OnLoadEnd(CefRefPtr browser, CefRefPtr frame, + int httpStatusCode) override; + void OnLoadError(CefRefPtr browser, CefRefPtr frame, + ErrorCode errorCode, const CefString& errorText, + const CefString& failedUrl) override; + + ///////////////////////////////////////////////////////////////// + // CefDialogHandler methods + ///////////////////////////////////////////////////////////////// + bool OnFileDialog(CefRefPtr browser, FileDialogMode mode, + const CefString& title, const CefString& default_file_path, + const std::vector& accept_filters, + const std::vector& accept_extensions, + const std::vector& accept_descriptions, + CefRefPtr callback) override; + + ///////////////////////////////////////////////////////////////// + // CefContextMenuHandler methods + ///////////////////////////////////////////////////////////////// + bool RunContextMenu(CefRefPtr browser, CefRefPtr frame, + CefRefPtr params, + CefRefPtr model, + CefRefPtr callback) override; + + ///////////////////////////////////////////////////////////////// + // CefFindHandler methods + ///////////////////////////////////////////////////////////////// + void OnFindResult(CefRefPtr browser, int identifier, int count, + const CefRect& selectionRect, int activeMatchOrdinal, + bool finalUpdate) override; + + ///////////////////////////////////////////////////////////////// + // CefJSDialogHandler methods + ///////////////////////////////////////////////////////////////// + bool OnJSDialog(CefRefPtr browser, const CefString& origin_url, + JSDialogType dialog_type, const CefString& message_text, + const CefString& default_prompt_text, + CefRefPtr callback, + bool& suppress_message) override; + bool OnBeforeUnloadDialog(CefRefPtr browser, + const CefString& message_text, bool is_reload, + CefRefPtr callback) override; + + ///////////////////////////////////////////////////////////////// + // CefRequestHandler methods + ///////////////////////////////////////////////////////////////// + void OnRenderProcessTerminated(CefRefPtr browser, + TerminationStatus status, int error_code, + const CefString& error_string) override; + bool OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, + CefRefPtr request, bool user_gesture, + bool is_redirect) override; + + ///////////////////////////////////////////////////////////////// + // CefFocusHandler methods + ///////////////////////////////////////////////////////////////// + void OnTakeFocus(CefRefPtr browser, bool next) override; + bool OnSetFocus(CefRefPtr browser, FocusSource source) override; + + ///////////////////////////////////////////////////////////////// + // CefPermissionHandler methods + ///////////////////////////////////////////////////////////////// + bool OnRequestMediaAccessPermission( + CefRefPtr browser, CefRefPtr frame, + const CefString& requesting_origin, uint32_t requested_permissions, + CefRefPtr callback) override; + + ///////////////////////////////////////////////////////////////// + // CefMessageRouterBrowserSide::Handler methods + ///////////////////////////////////////////////////////////////// + bool OnQuery(CefRefPtr browser, CefRefPtr frame, + int64_t query_id, const CefString& request, bool persistent, + CefRefPtr + callback) override; +}; diff --git a/wef/cpp/cursor.cpp b/wef/cpp/cursor.cpp new file mode 100644 index 0000000..63a172d --- /dev/null +++ b/wef/cpp/cursor.cpp @@ -0,0 +1,21 @@ +#include "include/cef_render_handler.h" + +extern "C" { + +void wef_cursor_info_hotspot(const CefCursorInfo* info, cef_point_t* point) { + *point = info->hotspot; +} + +float wef_cursor_info_image_scale_factor(const CefCursorInfo* info) { + return info->image_scale_factor; +} + +const void* wef_cursor_info_buffer(const CefCursorInfo* info) { + return info->buffer; +} + +void wef_cursor_info_size(const CefCursorInfo* info, cef_size_t* size) { + *size = info->size; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/dirty_rect.cpp b/wef/cpp/dirty_rect.cpp new file mode 100644 index 0000000..5337b52 --- /dev/null +++ b/wef/cpp/dirty_rect.cpp @@ -0,0 +1,14 @@ +#include "include/cef_render_handler.h" + +extern "C" { + +int wef_dirty_rects_len(const CefRenderHandler::RectList* dirtyRects) { + return static_cast(dirtyRects->size()); +} + +void wef_dirty_rects_get(const CefRenderHandler::RectList* dirtyRects, int i, + CefRect* rect) { + *rect = dirtyRects->at(i); +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/external_pump.cpp b/wef/cpp/external_pump.cpp new file mode 100644 index 0000000..ee4ee67 --- /dev/null +++ b/wef/cpp/external_pump.cpp @@ -0,0 +1,74 @@ +#include "external_pump.h" + +#include + +#include "include/cef_app.h" + +// Special timer delay placeholder value. Intentionally 32-bit for Windows and +// OS X platform API compatibility. +const int32_t TIMER_DELAY_PLACE_HOLDER = INT_MAX; + +// The maximum number of milliseconds we're willing to wait between calls to +// DoWork(). +const int64_t MAX_TIMER_DELAY = 1000 / 60; // 60fps + +void ExternalPump::OnScheduleWork(int64_t delay_ms) { + if (delay_ms == TIMER_DELAY_PLACE_HOLDER && IsTimerPending()) { + // Don't set the maximum timer requested from DoWork() if a timer event is + // currently pending. + return; + } + + KillTimer(); + + if (delay_ms <= 0) { + // Execute the work immediately. + DoWork(); + } else { + // Never wait longer than the maximum allowed time. + if (delay_ms > MAX_TIMER_DELAY) { + delay_ms = MAX_TIMER_DELAY; + } + + // Results in call to OnTimerTimeout() after the specified delay. + SetTimer(delay_ms); + } +} + +void ExternalPump::OnTimerTimeout() { + KillTimer(); + DoWork(); +} + +void ExternalPump::DoWork() { + const bool was_reentrant = PerformMessageLoopWork(); + if (was_reentrant) { + // Execute the remaining work as soon as possible. + OnScheduleMessagePumpWork(0); + } else if (!IsTimerPending()) { + // Schedule a timer event at the maximum allowed time. This may be dropped + // in OnScheduleWork() if another timer event is already in-flight. + OnScheduleMessagePumpWork(TIMER_DELAY_PLACE_HOLDER); + } +} + +bool ExternalPump::PerformMessageLoopWork() { + if (is_active_) { + // When CefDoMessageLoopWork() 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. + reentrancy_detected_ = true; + return false; + } + + reentrancy_detected_ = false; + + is_active_ = true; + CefDoMessageLoopWork(); + is_active_ = false; + + // |reentrancy_detected_| may have changed due to re-entrant calls to this + // method. + return reentrancy_detected_; +} \ No newline at end of file diff --git a/wef/cpp/external_pump.h b/wef/cpp/external_pump.h new file mode 100644 index 0000000..4efb838 --- /dev/null +++ b/wef/cpp/external_pump.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +class ExternalPump { + public: + ExternalPump() {} + virtual ~ExternalPump() {} + ExternalPump(const ExternalPump&) = delete; + ExternalPump& operator=(const ExternalPump&) = delete; + static std::unique_ptr Create(); + virtual void OnScheduleMessagePumpWork(int64_t delay_ms) = 0; + + protected: + void OnScheduleWork(int64_t delay_ms); + void OnTimerTimeout(); + + virtual void SetTimer(int64_t delay_ms) = 0; + virtual void KillTimer() = 0; + virtual bool IsTimerPending() = 0; + + private: + void DoWork(); + bool PerformMessageLoopWork(); + + bool is_active_ = false; + bool reentrancy_detected_ = false; +}; diff --git a/wef/cpp/external_pump_linux.cpp b/wef/cpp/external_pump_linux.cpp new file mode 100644 index 0000000..dbcc29d --- /dev/null +++ b/wef/cpp/external_pump_linux.cpp @@ -0,0 +1,212 @@ +#include +#include +#include +#include + +#include + +#include "external_pump.h" +#include "include/base/cef_logging.h" +#include "include/cef_app.h" + +#if !defined(HANDLE_EINTR) +#if !DCHECK_IS_ON() + +#define HANDLE_EINTR(x) \ + ({ \ + decltype(x) eintr_wrapper_result; \ + do { \ + eintr_wrapper_result = (x); \ + } while (eintr_wrapper_result == -1 && errno == EINTR); \ + eintr_wrapper_result; \ + }) + +#else + +#define HANDLE_EINTR(x) \ + ({ \ + int eintr_wrapper_counter = 0; \ + decltype(x) eintr_wrapper_result; \ + do { \ + eintr_wrapper_result = (x); \ + } while (eintr_wrapper_result == -1 && errno == EINTR && \ + eintr_wrapper_counter++ < 100); \ + eintr_wrapper_result; \ + }) + +#endif // !DCHECK_IS_ON() +#endif // !defined(HANDLE_EINTR) + +class ExternalPumpLinux : public ExternalPump { + public: + ExternalPumpLinux(); + ~ExternalPumpLinux(); + + // MainMessageLoopExternalPump methods: + void OnScheduleMessagePumpWork(int64_t delay_ms) override; + + int HandlePrepare(); + bool HandleCheck(); + void HandleDispatch(); + + protected: + void SetTimer(int64_t delay_ms) override; + void KillTimer() override; + bool IsTimerPending() override; + + private: + GMainContext* context_; + GSource* work_source_; + CefTime delayed_work_time_; + int wakeup_pipe_read_; + int wakeup_pipe_write_; + std::unique_ptr wakeup_gpollfd_; +}; + +// Return a timeout suitable for the glib loop, -1 to block forever, +// 0 to return right away, or a timeout in milliseconds from now. +int GetTimeIntervalMilliseconds(const CefTime& from) { + if (from.GetDoubleT() == 0.0) { + return -1; + } + + CefTime now; + now.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. + int delay = + static_cast(ceil((from.GetDoubleT() - now.GetDoubleT()) * 1000.0)); + + // If this value is negative, then we need to run delayed work soon. + return delay < 0 ? 0 : delay; +} + +struct WorkSource : public GSource { + ExternalPumpLinux* pump; +}; + +gboolean WorkSourcePrepare(GSource* source, gint* timeout_ms) { + *timeout_ms = static_cast(source)->pump->HandlePrepare(); + // We always return FALSE, so that our timeout is honored. If we were + // to return TRUE, the timeout would be considered to be 0 and the poll + // would never block. Once the poll is finished, Check will be called. + return FALSE; +} + +gboolean WorkSourceCheck(GSource* source) { + // Only return TRUE if Dispatch should be called. + return static_cast(source)->pump->HandleCheck(); +} + +gboolean WorkSourceDispatch(GSource* source, GSourceFunc unused_func, + gpointer unused_data) { + static_cast(source)->pump->HandleDispatch(); + // Always return TRUE so our source stays registered. + return TRUE; +} + +// I wish these could be const, but g_source_new wants non-const. +GSourceFuncs WorkSourceFuncs = {WorkSourcePrepare, WorkSourceCheck, + WorkSourceDispatch, nullptr}; + +ExternalPumpLinux::ExternalPumpLinux() + : context_(g_main_context_default()), wakeup_gpollfd_(new GPollFD) { + // Create our wakeup pipe, which is used to flag when work was scheduled. + int fds[2]; + int ret = pipe(fds); + DCHECK_EQ(ret, 0); + (void)ret; // Prevent warning in release mode. + + wakeup_pipe_read_ = fds[0]; + wakeup_pipe_write_ = fds[1]; + wakeup_gpollfd_->fd = wakeup_pipe_read_; + wakeup_gpollfd_->events = G_IO_IN; + + work_source_ = g_source_new(&WorkSourceFuncs, sizeof(WorkSource)); + static_cast(work_source_)->pump = this; + g_source_add_poll(work_source_, wakeup_gpollfd_.get()); + // Use a low priority so that we let other events in the queue go first. + g_source_set_priority(work_source_, G_PRIORITY_DEFAULT_IDLE); + // This is needed to allow Run calls inside Dispatch. + g_source_set_can_recurse(work_source_, TRUE); + g_source_attach(work_source_, context_); +} + +ExternalPumpLinux::~ExternalPumpLinux() { + g_source_destroy(work_source_); + g_source_unref(work_source_); + close(wakeup_pipe_read_); + close(wakeup_pipe_write_); +} + +void ExternalPumpLinux::OnScheduleMessagePumpWork(int64_t delay_ms) { + // This can be called on any thread, so we don't want to touch any state + // variables as we would then need locks all over. This ensures that if we + // are sleeping in a poll that we will wake up. + if (HANDLE_EINTR(write(wakeup_pipe_write_, &delay_ms, sizeof(int64_t))) != + sizeof(int64_t)) { + NOTREACHED() << "Could not write to the UI message loop wakeup pipe!"; + } +} + +// Return the timeout we want passed to poll. +int ExternalPumpLinux::HandlePrepare() { + // We don't think we have work to do, but make sure not to block longer than + // the next time we need to run delayed work. + return GetTimeIntervalMilliseconds(delayed_work_time_); +} + +bool ExternalPumpLinux::HandleCheck() { + // 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 (wakeup_gpollfd_->revents & G_IO_IN) { + int64_t delay_ms[2]; + const size_t num_bytes = + HANDLE_EINTR(read(wakeup_pipe_read_, delay_ms, sizeof(int64_t) * 2)); + if (num_bytes < sizeof(int64_t)) { + NOTREACHED() << "Error reading from the wakeup pipe."; + } + if (num_bytes == sizeof(int64_t)) { + OnScheduleWork(delay_ms[0]); + } + if (num_bytes == sizeof(int64_t) * 2) { + OnScheduleWork(delay_ms[1]); + } + } + + if (GetTimeIntervalMilliseconds(delayed_work_time_) == 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. + return true; + } + + return false; +} + +void ExternalPumpLinux::HandleDispatch() { OnTimerTimeout(); } + +void ExternalPumpLinux::SetTimer(int64_t delay_ms) { + DCHECK_GT(delay_ms, 0); + + CefTime now; + now.Now(); + + delayed_work_time_ = + CefTime(now.GetDoubleT() + static_cast(delay_ms) / 1000.0); +} + +void ExternalPumpLinux::KillTimer() { delayed_work_time_ = CefTime(); } + +bool ExternalPumpLinux::IsTimerPending() { + return GetTimeIntervalMilliseconds(delayed_work_time_) > 0; +} + +// static +std::unique_ptr ExternalPump::Create() { + return std::make_unique(); +} diff --git a/wef/cpp/external_pump_mac.mm b/wef/cpp/external_pump_mac.mm new file mode 100644 index 0000000..9e68cef --- /dev/null +++ b/wef/cpp/external_pump_mac.mm @@ -0,0 +1,123 @@ +#import +#import + +#include "external_pump.h" + +@class EventHandler; + +class ExternalPumpMac : public ExternalPump { + public: + ExternalPumpMac(); + ~ExternalPumpMac(); + + void OnScheduleMessagePumpWork(int64_t delay_ms) override; + void HandleScheduleWork(int64_t delay_ms); + void HandleTimerTimeout(); + + protected: + void SetTimer(int64_t delay_ms) override; + void KillTimer() override; + bool IsTimerPending() override { return timer_ != nil; } + + private: + NSThread* owner_thread_; + NSTimer* timer_; + EventHandler* event_handler_; +}; + +// Object that handles event callbacks on the owner thread. +@interface EventHandler : NSObject { + @private + ExternalPumpMac* pump_; +} + +- (id)initWithPump:(ExternalPumpMac*)pump; +- (void)scheduleWork:(NSNumber*)delay_ms; +- (void)timerTimeout:(id)obj; +@end + +@implementation EventHandler + +- (id)initWithPump:(ExternalPumpMac*)pump { + if (self = [super init]) { + pump_ = pump; + } + return self; +} + +- (void)scheduleWork:(NSNumber*)delay_ms { + pump_->HandleScheduleWork([delay_ms integerValue]); +} + +- (void)timerTimeout:(id)obj { + pump_->HandleTimerTimeout(); +} + +@end + +ExternalPumpMac::ExternalPumpMac() + : owner_thread_([NSThread currentThread]), timer_(nil) { +#if !__has_feature(objc_arc) + [owner_thread_ retain]; +#endif // !__has_feature(objc_arc) + event_handler_ = [[EventHandler alloc] initWithPump:this]; +} + +ExternalPumpMac::~ExternalPumpMac() { + KillTimer(); +#if !__has_feature(objc_arc) + [owner_thread_ release]; + [event_handler_ release]; +#endif // !__has_feature(objc_arc) + owner_thread_ = nil; + event_handler_ = nil; +} + +void ExternalPumpMac::OnScheduleMessagePumpWork( + int64_t delay_ms) { + // This method may be called on any thread. + NSNumber* number = [NSNumber numberWithInt:static_cast(delay_ms)]; + [event_handler_ performSelector:@selector(scheduleWork:) + onThread:owner_thread_ + withObject:number + waitUntilDone:NO]; +} + +void ExternalPumpMac::HandleScheduleWork(int64_t delay_ms) { + OnScheduleWork(delay_ms); +} + +void ExternalPumpMac::HandleTimerTimeout() { + OnTimerTimeout(); +} + +void ExternalPumpMac::SetTimer(int64_t delay_ms) { + const double delay_s = static_cast(delay_ms) / 1000.0; + timer_ = [NSTimer timerWithTimeInterval:delay_s + target:event_handler_ + selector:@selector(timerTimeout:) + userInfo:nil + repeats:NO]; +#if !__has_feature(objc_arc) + [timer_ retain]; +#endif // !__has_feature(objc_arc) + + // Add the timer to default and tracking runloop modes. + NSRunLoop* owner_runloop = [NSRunLoop currentRunLoop]; + [owner_runloop addTimer:timer_ forMode:NSRunLoopCommonModes]; + [owner_runloop addTimer:timer_ forMode:NSEventTrackingRunLoopMode]; +} + +void ExternalPumpMac::KillTimer() { + if (timer_ != nil) { + [timer_ invalidate]; +#if !__has_feature(objc_arc) + [timer_ release]; +#endif // !__has_feature(objc_arc) + timer_ = nil; + } +} + +std::unique_ptr ExternalPump::Create() { + return std::make_unique(); +} diff --git a/wef/cpp/external_pump_win.cpp b/wef/cpp/external_pump_win.cpp new file mode 100644 index 0000000..48da5bb --- /dev/null +++ b/wef/cpp/external_pump_win.cpp @@ -0,0 +1,81 @@ +#include + +#include "external_pump.h" + +static const int MSG_HAVE_WORK = WM_USER + 1; + +class ExternalPumpWin : public ExternalPump { + public: + ExternalPumpWin() { + HINSTANCE hInstance = GetModuleHandle(nullptr); + const char* const className = "CEFMainTargetHWND"; + + WNDCLASSEX wcex = {}; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.lpfnWndProc = WndProc; + wcex.hInstance = hInstance; + wcex.lpszClassName = className; + RegisterClassEx(&wcex); + + main_thread_target_ = + CreateWindow(className, nullptr, WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, hInstance, nullptr); + + SetWindowLongPtr(main_thread_target_, GWLP_USERDATA, + reinterpret_cast(this)); + } + + ~ExternalPumpWin() override { + KillTimer(); + if (main_thread_target_) { + DestroyWindow(main_thread_target_); + } + } + + void OnScheduleMessagePumpWork(int64_t delay_ms) override { + // This method may be called on any thread. + PostMessage(main_thread_target_, MSG_HAVE_WORK, 0, + static_cast(delay_ms)); + } + + protected: + void SetTimer(int64_t delay_ms) override { + timer_pending_ = true; + ::SetTimer(main_thread_target_, 1, static_cast(delay_ms), nullptr); + } + + void KillTimer() override { + if (timer_pending_) { + ::KillTimer(main_thread_target_, 1); + timer_pending_ = false; + } + } + + bool IsTimerPending() override { return timer_pending_; } + + private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, + LPARAM lparam) { + if (msg == WM_TIMER || msg == MSG_HAVE_WORK) { + ExternalPumpWin* message_loop = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (msg == MSG_HAVE_WORK) { + const int64_t delay_ms = static_cast(lparam); + message_loop->OnScheduleWork(delay_ms); + } else { + message_loop->OnTimerTimeout(); + } + } + return DefWindowProc(hwnd, msg, wparam, lparam); + } + + // True if a timer event is currently pending. + bool timer_pending_ = false; + + // HWND owned by the thread that CefDoMessageLoopWork should be invoked on. + HWND main_thread_target_ = nullptr; +}; + +std::unique_ptr ExternalPump::Create() { + return std::make_unique(); +} diff --git a/wef/cpp/file_dialog.cpp b/wef/cpp/file_dialog.cpp new file mode 100644 index 0000000..a27c48f --- /dev/null +++ b/wef/cpp/file_dialog.cpp @@ -0,0 +1,21 @@ +#include "include/cef_dialog_handler.h" +#include "utils.h" + +extern "C" { + +void wef_file_dialog_callback_continue( + CefRefPtr* callback, const char* file_paths) { + (*callback)->Continue(split_string(file_paths, ";")); +} + +void wef_file_dialog_callback_cancel( + CefRefPtr* callback) { + (*callback)->Cancel(); +} + +void wef_file_dialog_callback_destroy( + CefRefPtr* callback) { + delete callback; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/frame.cpp b/wef/cpp/frame.cpp new file mode 100644 index 0000000..9a36303 --- /dev/null +++ b/wef/cpp/frame.cpp @@ -0,0 +1,65 @@ +#include "frame.h" + +#include + +#include "include/cef_frame.h" + +extern "C" { +void wef_frame_destroy(WefFrame* frame) { delete frame; } + +bool wef_frame_is_valid(WefFrame* frame) { return frame->frame->IsValid(); } + +bool wef_frame_is_main(WefFrame* frame) { return frame->frame->IsMain(); } + +void wef_frame_name(WefFrame* frame, void* userdata, + void (*callback)(void*, const char*)) { + auto name = frame->frame->GetName().ToString(); + callback(userdata, name.c_str()); +} + +void wef_frame_identifier(WefFrame* frame, void* userdata, + void (*callback)(void*, const char*)) { + auto id = frame->frame->GetIdentifier().ToString(); + callback(userdata, id.c_str()); +} + +void wef_frame_get_url(WefFrame* frame, void* userdata, + void (*callback)(void*, const char*)) { + auto url = frame->frame->GetURL().ToString(); + callback(userdata, url.c_str()); +} + +void wef_frame_load_url(WefFrame* frame, const char* url) { + if (strlen(url) > 0) { + frame->frame->LoadURL(url); + } +} + +WefFrame* wef_frame_parent(WefFrame* frame) { + auto parent = frame->frame->GetParent(); + return parent ? new WefFrame{parent} : nullptr; +} + +void wef_frame_undo(WefFrame* frame) { frame->frame->Undo(); } + +void wef_frame_redo(WefFrame* frame) { frame->frame->Redo(); } + +void wef_frame_cut(WefFrame* frame) { frame->frame->Cut(); } + +void wef_frame_copy(WefFrame* frame) { frame->frame->Copy(); } + +void wef_frame_paste(WefFrame* frame) { frame->frame->Paste(); } + +void wef_frame_paste_and_match_style(WefFrame* frame) { + frame->frame->PasteAndMatchStyle(); +} + +void wef_frame_delete(WefFrame* frame) { frame->frame->Delete(); } + +void wef_frame_select_all(WefFrame* frame) { frame->frame->SelectAll(); } + +void wef_frame_execute_javascript(WefFrame* frame, const char* code) { + frame->frame->ExecuteJavaScript(code, frame->frame->GetURL(), 0); +} + +} // extern "C" diff --git a/wef/cpp/frame.h b/wef/cpp/frame.h new file mode 100644 index 0000000..e1147a2 --- /dev/null +++ b/wef/cpp/frame.h @@ -0,0 +1,7 @@ +#pragma once + +#include "include/cef_frame.h" + +struct WefFrame { + CefRefPtr frame; +}; diff --git a/wef/cpp/js_dialog.cpp b/wef/cpp/js_dialog.cpp new file mode 100644 index 0000000..0bf03b0 --- /dev/null +++ b/wef/cpp/js_dialog.cpp @@ -0,0 +1,15 @@ +#include "include/cef_jsdialog_handler.h" +#include "utils.h" + +extern "C" { + +void wef_js_dialog_callback_continue(CefRefPtr* callback, + bool success, const char* user_input) { + (*callback)->Continue(success, user_input); +} + +void wef_js_dialog_callback_destroy(CefRefPtr* callback) { + delete callback; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/load_library.cpp b/wef/cpp/load_library.cpp new file mode 100644 index 0000000..86acefb --- /dev/null +++ b/wef/cpp/load_library.cpp @@ -0,0 +1,27 @@ +#include "include/wrapper/cef_library_loader.h" + +extern "C" { + +void* wef_load_library(bool helper) { + CefScopedLibraryLoader* library_loader = new CefScopedLibraryLoader(); + if (!helper) { + if (!library_loader->LoadInMain()) { + delete library_loader; + return nullptr; + } + } else { + if (!library_loader->LoadInHelper()) { + delete library_loader; + return nullptr; + } + } + return library_loader; +} + +void wef_unload_library(void* loader) { + CefScopedLibraryLoader* library_loader = + static_cast(loader); + delete library_loader; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/query.cpp b/wef/cpp/query.cpp new file mode 100644 index 0000000..7e49539 --- /dev/null +++ b/wef/cpp/query.cpp @@ -0,0 +1,25 @@ +#include + +#include "include/wrapper/cef_message_router.h" +#include "utils.h" + +extern "C" { + +void wef_query_callback_success( + CefRefPtr* callback, + const char* response) { + (*callback)->Success(response); +} + +void wef_query_callback_failure( + CefRefPtr* callback, + const char* error) { + (*callback)->Failure(-1, error); +} + +void wef_query_callback_destroy( + CefRefPtr* callback) { + delete callback; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/sandbox_context.cpp b/wef/cpp/sandbox_context.cpp new file mode 100644 index 0000000..d7f3d14 --- /dev/null +++ b/wef/cpp/sandbox_context.cpp @@ -0,0 +1,19 @@ +#include "include/cef_sandbox_mac.h" + +extern "C" { + +void* wef_sandbox_context_create(char* argv[], int argc) { + CefScopedSandboxContext* ctx = new CefScopedSandboxContext(); + if (!ctx->Initialize(argc, argv)) { + delete ctx; + return nullptr; + } + return ctx; +} + +void wef_sandbox_context_destroy(void* p) { + CefScopedSandboxContext* ctx = static_cast(p); + delete ctx; +} + +} // extern "C" \ No newline at end of file diff --git a/wef/cpp/utils.h b/wef/cpp/utils.h new file mode 100644 index 0000000..94171f2 --- /dev/null +++ b/wef/cpp/utils.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "include/internal/cef_string.h" + +typedef void (*DestroyFn)(void*); + +inline std::string join_strings(const std::vector& strings, + const std::string& delimiter) { + return std::accumulate(strings.begin(), strings.end(), std::string(), + [&](const std::string& a, const CefString& b) { + auto b_ = b.ToString(); + return a.empty() ? b_ : a + delimiter + b_; + }); +} + +inline std::vector split_string(const std::string& str, + const std::string& delimiter) { + std::vector result; + std::string::size_type start = 0; + std::string::size_type end; + + while ((end = str.find(delimiter, start)) != std::string::npos) { + result.emplace_back(CefString(str.substr(start, end - start))); + start = end + delimiter.length(); + } + auto last = str.substr(start); + if (!last.empty()) { + result.emplace_back(CefString(last)); + } + return result; +} \ No newline at end of file diff --git a/wef/cpp/wef.cpp b/wef/cpp/wef.cpp new file mode 100644 index 0000000..8df6c86 --- /dev/null +++ b/wef/cpp/wef.cpp @@ -0,0 +1,417 @@ +#if defined(_WIN32) || defined(_WIN64) +#define NOMINMAX +#endif + +#include +#include +#include +#include + +#include "app.h" +#include "app_render_process.h" +#include "browser_callbacks.h" +#include "client.h" +#include "frame.h" +#include "include/base/cef_bind.h" +#include "include/base/cef_callback.h" +#include "include/cef_app.h" +#include "include/cef_command_line.h" +#include "include/cef_render_handler.h" +#include "include/cef_task.h" +#include "include/wrapper/cef_closure_task.h" + +const uint32_t ALL_MOUSE_BUTTONS = EVENTFLAG_LEFT_MOUSE_BUTTON; + +struct WefSettings { + const char* locale; + const char* cache_path; + const char* root_cache_path; + const char* browser_subprocess_path; + AppCallbacks callbacks; + void* userdata; + DestroyFn destroy_userdata; +}; + +struct WefBrowserSettings { + void* parent; + float device_scale_factor; + int width; + int height; + int frame_rate; + const char* url; + const char* inject_javascript; + BrowserCallbacks callbacks; + void* userdata; + DestroyFn destroy_userdata; +}; + +struct WefBrowser { + std::shared_ptr state; +}; + +inline void apply_key_modifiers(uint32_t& m, int modifiers) { + if (modifiers & 0x1) { + m |= EVENTFLAG_SHIFT_DOWN; + } + + if (modifiers & 0x2) { + m |= EVENTFLAG_CONTROL_DOWN; + } + + if (modifiers & 0x4) { + m |= EVENTFLAG_ALT_DOWN; + } +} + +extern "C" { + +bool wef_init(const WefSettings* wef_settings) { + CefSettings settings; + settings.windowless_rendering_enabled = true; + settings.external_message_pump = true; + +#ifdef __APPLE__ + settings.no_sandbox = false; +#else + settings.no_sandbox = true; +#endif + + if (wef_settings->locale) { + CefString(&settings.locale) = wef_settings->locale; + } + + if (wef_settings->cache_path) { + CefString(&settings.cache_path) = wef_settings->cache_path; + } + + if (wef_settings->root_cache_path) { + CefString(&settings.root_cache_path) = wef_settings->root_cache_path; + } + + if (wef_settings->browser_subprocess_path) { + CefString(&settings.browser_subprocess_path) = + wef_settings->browser_subprocess_path; + } + + CefRefPtr app(new WefApp(wef_settings->callbacks, + wef_settings->userdata, + wef_settings->destroy_userdata)); + return CefInitialize(CefMainArgs(), settings, app, nullptr); +} + +bool wef_exec_process(char* argv[], int argc) { +#ifdef WIN32 + CefMainArgs args(GetModuleHandle(NULL)); +#else + CefMainArgs args(argc, argv); +#endif + + CefRefPtr app(new WefRenderProcessApp()); + return CefExecuteProcess(args, app, nullptr) >= 0; +} + +void wef_shutdown() { CefShutdown(); } + +void wef_do_message_work() { CefDoMessageLoopWork(); } + +WefBrowser* wef_browser_create(const WefBrowserSettings* settings) { + CefWindowInfo window_info; + window_info.SetAsWindowless( + reinterpret_cast(settings->parent)); + window_info.runtime_style = CEF_RUNTIME_STYLE_ALLOY; + + CefBrowserSettings browser_settings; + browser_settings.windowless_frame_rate = settings->frame_rate; + browser_settings.background_color = CefColorSetARGB(255, 255, 255, 255); + + WefBrowser* wef_browser = new WefBrowser; + CefRefPtr extra_info = CefDictionaryValue::Create(); + extra_info->SetString("__wef_inject_javascript", settings->inject_javascript); + + wef_browser->state = + std::make_shared(BrowserCallbacksTarget{ + settings->callbacks, settings->userdata, settings->destroy_userdata}); + wef_browser->state->width = settings->width; + wef_browser->state->height = settings->height; + wef_browser->state->device_scale_factor = settings->device_scale_factor; + + CefRefPtr client(new WefClient(wef_browser->state)); + CefBrowserHost::CreateBrowser(window_info, client, settings->url, + browser_settings, extra_info, nullptr); + return wef_browser; +} + +void wef_browser_close(WefBrowser* browser) { + if (browser->state->browser_state == BrowserState::Creating) { + browser->state->browser_state = BrowserState::Closed; + } else if (browser->state->browser_state == BrowserState::Created) { + browser->state->browser_state = BrowserState::Closing; + (*browser->state->browser)->GetHost()->CloseBrowser(false); + } +} + +void wef_browser_destroy(WefBrowser* browser) { + if (browser->state->browser_state == BrowserState::Creating) { + browser->state->browser_state = BrowserState::Closed; + } else if (browser->state->browser_state == BrowserState::Created) { + browser->state->browser_state = BrowserState::Closed; + (*browser->state->browser)->GetHost()->CloseBrowser(true); + } + browser->state->callbacks_target.disable(); + delete browser; +} + +bool wef_browser_is_created(WefBrowser* browser) { + return browser->state->browser_state == BrowserState::Created; +} + +void wef_browser_set_size(WefBrowser* browser, int width, int height) { + browser->state->width = width; + browser->state->height = height; + if (browser->state->browser) { + (*browser->state->browser)->GetHost()->WasResized(); + } +} + +void wef_browser_load_url(WefBrowser* browser, const char* url) { + if (strlen(url) == 0) { + return; + } + + if (!browser->state->browser) { + return; + } + + CefPostTask(TID_UI, base::BindOnce(&CefFrame::LoadURL, + (*browser->state->browser)->GetMainFrame(), + CefString(url))); +} + +bool wef_browser_can_go_forward(WefBrowser* browser) { + if (!browser->state->browser) { + return false; + } + return (*browser->state->browser)->CanGoForward(); +} + +bool wef_browser_can_go_back(WefBrowser* browser) { + if (!browser->state->browser) { + return false; + } + return (*browser->state->browser)->CanGoBack(); +} + +void wef_browser_go_forward(WefBrowser* browser) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser)->GoForward(); +} + +void wef_browser_go_back(WefBrowser* browser) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser)->GoBack(); +} + +void wef_browser_reload(WefBrowser* browser) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser)->Reload(); +} + +void wef_browser_reload_ignore_cache(WefBrowser* browser) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser)->ReloadIgnoreCache(); +} + +void wef_browser_send_mouse_click_event(WefBrowser* browser, + int mouse_button_type, bool mouse_up, + int click_count, int modifiers) { + if (!browser->state->browser) { + return; + } + + CefMouseEvent mouse_event; + mouse_event.x = browser->state->cursorX; + mouse_event.y = browser->state->cursorY; + mouse_event.modifiers = EVENTFLAG_NONE; + + CefBrowserHost::MouseButtonType btn_type; + switch (mouse_button_type) { + case 1: + btn_type = MBT_MIDDLE; + mouse_event.modifiers |= EVENTFLAG_MIDDLE_MOUSE_BUTTON; + break; + case 2: + btn_type = MBT_RIGHT; + mouse_event.modifiers |= EVENTFLAG_RIGHT_MOUSE_BUTTON; + break; + default: + btn_type = MBT_LEFT; + mouse_event.modifiers |= EVENTFLAG_LEFT_MOUSE_BUTTON; + } + + apply_key_modifiers(mouse_event.modifiers, modifiers); + + (*browser->state->browser) + ->GetHost() + ->SendMouseClickEvent(mouse_event, btn_type, mouse_up, + std::max(click_count, 3)); +} + +void wef_browser_send_mouse_move_event(WefBrowser* browser, int x, int y, + int modifiers) { + if (!browser->state->browser) { + return; + } + + CefMouseEvent mouse_event; + mouse_event.x = x; + mouse_event.y = y; + mouse_event.modifiers = ALL_MOUSE_BUTTONS; + apply_key_modifiers(mouse_event.modifiers, modifiers); + (*browser->state->browser)->GetHost()->SendMouseMoveEvent(mouse_event, false); + + browser->state->cursorX = mouse_event.x; + browser->state->cursorY = mouse_event.y; +} + +void wef_browser_send_mouse_wheel_event(WefBrowser* browser, int delta_x, + int delta_y) { + if (!browser->state->browser) { + return; + } + + CefMouseEvent mouse_event; + mouse_event.x = browser->state->cursorX; + mouse_event.y = browser->state->cursorY; + mouse_event.modifiers = ALL_MOUSE_BUTTONS; + (*browser->state->browser) + ->GetHost() + ->SendMouseWheelEvent(mouse_event, delta_x, delta_y); +} + +void wef_browser_send_key_event(WefBrowser* browser, bool is_down, int key_code, + int modifiers) { + if (!browser->state->browser) { + return; + } + + CefKeyEvent key_event; + key_event.type = is_down ? KEYEVENT_KEYDOWN : KEYEVENT_KEYUP; + key_event.modifiers = EVENTFLAG_NONE; + key_event.focus_on_editable_field = false; + key_event.is_system_key = false; + key_event.windows_key_code = key_code; + key_event.native_key_code = key_code; + key_event.modifiers = 0; + apply_key_modifiers(key_event.modifiers, modifiers); + (*browser->state->browser)->GetHost()->SendKeyEvent(key_event); +} + +void wef_browser_send_char_event(WefBrowser* browser, char16_t ch) { + if (!browser->state->browser) { + return; + } + + CefKeyEvent key_event; + key_event.type = KEYEVENT_CHAR; + key_event.modifiers = EVENTFLAG_NONE; + key_event.windows_key_code = static_cast(ch); + key_event.native_key_code = static_cast(ch); + key_event.character = static_cast(ch); + (*browser->state->browser)->GetHost()->SendKeyEvent(key_event); +} + +void wef_browser_ime_set_composition(WefBrowser* browser, const char* text, + uint32_t cursor_begin, + uint32_t cursor_end) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser) + ->GetHost() + ->ImeSetComposition(text, {}, CefRange::InvalidRange(), + CefRange(cursor_begin, cursor_end)); +} + +void wef_browser_ime_commit(WefBrowser* browser, const char* text) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser) + ->GetHost() + ->ImeCommitText(text, CefRange::InvalidRange(), 0); +} + +WefFrame* wef_browser_get_main_frame(WefBrowser* browser) { + if (!browser->state->browser) { + return nullptr; + } + auto main_frame = (*browser->state->browser)->GetMainFrame(); + return main_frame ? new WefFrame{main_frame} : nullptr; +} + +WefFrame* wef_browser_get_focused_frame(WefBrowser* browser) { + if (!browser->state->browser) { + return nullptr; + } + auto frame = (*browser->state->browser)->GetFocusedFrame(); + return frame ? new WefFrame{frame} : nullptr; +} + +WefFrame* wef_browser_get_frame_by_name(WefBrowser* browser, const char* name) { + if (!browser->state->browser) { + return nullptr; + } + auto frame = (*browser->state->browser)->GetFrameByName(name); + return frame ? new WefFrame{frame} : nullptr; +} + +WefFrame* wef_browser_get_frame_by_identifier(WefBrowser* browser, + const char* id) { + if (!browser->state->browser) { + return nullptr; + } + auto frame = (*browser->state->browser)->GetFrameByIdentifier(id); + return frame ? new WefFrame{frame} : nullptr; +} + +bool wef_browser_is_audio_muted(WefBrowser* browser, bool mute) { + if (browser->state->browser) { + return false; + } + return (*browser->state->browser)->GetHost()->IsAudioMuted(); +} + +void wef_browser_set_audio_mute(WefBrowser* browser, bool mute) { + if (browser->state->browser) { + return; + } + (*browser->state->browser)->GetHost()->SetAudioMuted(mute); +} + +void wef_browser_find(WefBrowser* browser, const char* search_text, + bool forward, bool match_case, bool find_next) { + if (!browser->state->browser) { + return; + } + (*browser->state->browser) + ->GetHost() + ->Find(search_text, forward, match_case, find_next); +} + +void wef_browser_set_focus(WefBrowser* browser, bool focus) { + if (!browser->state->browser) { + browser->state->focus = true; + return; + } + (*browser->state->browser)->GetHost()->SetFocus(focus); +} + +} // extern "C" \ No newline at end of file diff --git a/wef/src/app_handler.rs b/wef/src/app_handler.rs new file mode 100644 index 0000000..60e485d --- /dev/null +++ b/wef/src/app_handler.rs @@ -0,0 +1,37 @@ +use std::ffi::c_void; + +/// Represents a handler for application events. +#[allow(unused_variables)] +pub trait ApplicationHandler: Send + Sync { + /// Called from any thread when work has been scheduled for the browser + /// process main (UI) thread. + /// + /// This callback is used in combination with [`crate::do_message_work`] in + /// cases where the CEF message loop must be integrated into an existing + /// application message loop. + /// + /// This callback should schedule a [`crate::do_message_work`] call to + /// happen on the main (UI) thread. + /// + /// `delay_ms` is the requested delay in milliseconds. If `delay_ms` is <= 0 + /// then the call should happen reasonably soon. If `delay_ms` is > 0 + /// then the call should be scheduled to happen after the specified + /// delay and any currently pending scheduled call should be cancelled. + fn on_schedule_message_pump_work(&mut self, delay_ms: i32) {} +} + +impl ApplicationHandler for () {} + +pub(crate) struct ApplicationState { + pub(crate) handler: T, +} + +pub(crate) extern "C" fn on_schedule_message_pump_work( + userdata: *mut c_void, + delay_ms: i32, +) { + unsafe { + let state = &mut *(userdata as *mut ApplicationState); + state.handler.on_schedule_message_pump_work(delay_ms); + } +} diff --git a/wef/src/browser.rs b/wef/src/browser.rs new file mode 100644 index 0000000..f84e922 --- /dev/null +++ b/wef/src/browser.rs @@ -0,0 +1,224 @@ +use std::{ffi::CString, fmt}; + +use crate::{ + BrowserBuilder, Frame, KeyCode, KeyModifier, LogicalUnit, MouseButton, PhysicalUnit, Point, + Size, ffi::*, +}; + +/// A browser instance. +pub struct Browser { + pub(crate) wef_browser: *mut wef_browser_t, +} + +impl fmt::Debug for Browser { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Browser").finish() + } +} + +impl Drop for Browser { + fn drop(&mut self) { + unsafe { wef_browser_destroy(self.wef_browser) }; + } +} + +impl Browser { + /// Creates a new [`BrowserBuilder`] instance. + pub fn builder() -> BrowserBuilder<()> { + BrowserBuilder::new() + } + + /// Closes the browser. + /// + /// When the browser has been closed, the callback + /// [`crate::BrowserHandler::on_closed`] will be called. + /// + /// If the browser is not created or in creating state, this method does + /// nothing. + pub fn close(&self) { + unsafe { wef_browser_close(self.wef_browser) } + } + + /// Returns `true` if the browser is created. + pub fn is_created(&self) -> bool { + unsafe { wef_browser_is_created(self.wef_browser) } + } + + /// Sets the size of the render target. + pub fn resize(&self, sz: Size>) { + unsafe { wef_browser_set_size(self.wef_browser, sz.width.0.max(1), sz.height.0.max(1)) }; + } + + /// Loads a URL. + pub fn load_url(&self, url: &str) { + let c_url = CString::new(url).unwrap(); + unsafe { wef_browser_load_url(self.wef_browser, c_url.as_ptr()) }; + } + + /// Returns `true`` if the browser can navigate forwards. + pub fn can_forward(&self) -> bool { + unsafe { wef_browser_can_go_forward(self.wef_browser) } + } + + /// Returns `true`` if the browser can navigate backwards. + pub fn can_back(&self) -> bool { + unsafe { wef_browser_can_go_back(self.wef_browser) } + } + + /// Navigates forward in history. + pub fn forward(&self) { + unsafe { wef_browser_go_forward(self.wef_browser) }; + } + + /// Navigates back in history. + pub fn back(&self) { + unsafe { wef_browser_go_back(self.wef_browser) }; + } + + /// Reloads the current page. + pub fn reload(&self) { + unsafe { wef_browser_reload(self.wef_browser) }; + } + + /// Reloads the current page ignoring any cached data. + pub fn reload_ignore_cache(&self) { + unsafe { wef_browser_reload_ignore_cache(self.wef_browser) }; + } + + /// Sends a mouse click event. + pub fn send_mouse_click_event( + &self, + mouse_button_type: MouseButton, + mouse_up: bool, + click_count: usize, + modifiers: KeyModifier, + ) { + unsafe { + wef_browser_send_mouse_click_event( + self.wef_browser, + mouse_button_type as i32, + mouse_up, + click_count as i32, + modifiers.bits(), + ) + }; + } + + /// Sends a mouse move event. + pub fn send_mouse_move_event(&self, pt: Point>, modifiers: KeyModifier) { + unsafe { + wef_browser_send_mouse_move_event(self.wef_browser, pt.x.0, pt.y.0, modifiers.bits()) + }; + } + + /// Sends a mouse wheel event. + pub fn send_mouse_wheel_event(&self, delta: Point>) { + unsafe { wef_browser_send_mouse_wheel_event(self.wef_browser, delta.x.0, delta.y.0) }; + } + + /// Sends a key event. + pub fn send_key_event(&self, is_down: bool, key_code: KeyCode, modifiers: KeyModifier) { + unsafe { + wef_browser_send_key_event( + self.wef_browser, + is_down, + key_code as i32, + modifiers.bits(), + ); + if let Some(ch) = key_code.as_char() { + wef_browser_send_char_event(self.wef_browser, ch); + } + } + } + + /// Sends a character event. + pub fn send_char_event(&self, ch: u16) { + unsafe { wef_browser_send_char_event(self.wef_browser, ch) }; + } + + /// Sets the composition text for the IME (Input Method Editor). + pub fn ime_set_composition(&self, text: &str, cursor_begin: usize, cursor_end: usize) { + let c_text = CString::new(text).unwrap(); + unsafe { + wef_browser_ime_set_composition( + self.wef_browser, + c_text.as_ptr(), + cursor_begin as u32, + cursor_end as u32, + ) + }; + } + + /// Commits the composition text for the IME (Input Method Editor). + pub fn ime_commit(&self, text: &str) { + let c_text = CString::new(text).unwrap(); + unsafe { wef_browser_ime_commit(self.wef_browser, c_text.as_ptr()) }; + } + + /// Returns the main (top-level) frame for the browser. + pub fn main_frame(&self) -> Option { + let frame = unsafe { wef_browser_get_main_frame(self.wef_browser) }; + (!frame.is_null()).then_some(Frame(frame)) + } + + /// Returns the focused frame for the browser. + pub fn focused_frame(&self) -> Option { + let frame = unsafe { wef_browser_get_focused_frame(self.wef_browser) }; + (!frame.is_null()).then_some(Frame(frame)) + } + + /// Returns a frame by its name. + pub fn frame_by_name(&self, name: &str) -> Option { + let c_name = CString::new(name).unwrap(); + let frame = unsafe { wef_browser_get_frame_by_name(self.wef_browser, c_name.as_ptr()) }; + (!frame.is_null()).then_some(Frame(frame)) + } + + /// Returns a frame by its identifier. + pub fn frame_by_identifier(&self, id: &str) -> Option { + let c_id = CString::new(id).unwrap(); + let frame = unsafe { wef_browser_get_frame_by_identifier(self.wef_browser, c_id.as_ptr()) }; + (!frame.is_null()).then_some(Frame(frame)) + } + + /// Returns `true` if the browser's audio is muted. + pub fn is_audio_muted(&self) -> bool { + unsafe { wef_browser_is_audio_muted(self.wef_browser) } + } + + /// Set whether the browser's audio is muted. + pub fn set_audio_mute(&self, mute: bool) { + unsafe { wef_browser_set_audio_mute(self.wef_browser, mute) }; + } + + /// Search for `searchText``. + /// + /// `forward`` indicates whether to search forward + /// or backward within the page. + /// `matchCase`` indicates whether the search should be case-sensitive. + /// `findNext`` indicates whether this is the first request or a + /// follow-up. + /// + /// The search will be restarted if `searchText` or `matchCase` change. The + /// search will be stopped if `searchText` is empty. + /// + /// The find results will be reported via the + /// [`crate::BrowserHandler::on_find_result`]. + pub fn find(&self, search_text: &str, forward: bool, match_case: bool, find_next: bool) { + unsafe { + let c_search_text = CString::new(search_text).unwrap(); + wef_browser_find( + self.wef_browser, + c_search_text.as_ptr(), + forward, + match_case, + find_next, + ) + }; + } + + /// Set whether the browser is focused. + pub fn set_focus(&self, focus: bool) { + unsafe { wef_browser_set_focus(self.wef_browser, focus) }; + } +} diff --git a/wef/src/browser_handler.rs b/wef/src/browser_handler.rs new file mode 100644 index 0000000..32cee22 --- /dev/null +++ b/wef/src/browser_handler.rs @@ -0,0 +1,667 @@ +use std::{ + ffi::{CStr, c_char, c_void}, + mem::MaybeUninit, +}; + +use num_enum::TryFromPrimitive; +use serde::Deserialize; +use serde_json::Value; + +use crate::{ + Accept, ContextMenuEditStateFlags, ContextMenuMediaStateFlags, ContextMenuMediaType, + ContextMenuParams, ContextMenuTypeFlags, CursorType, DirtyRects, FileDialogCallback, + FileDialogMode, Frame, JsDialogCallback, JsDialogType, LogicalUnit, Point, Rect, Size, + builder::BrowserState, cursor::CursorInfo, ffi::*, file_dialog::AcceptFilter, + query::QueryCallback, +}; + +/// A type alias for the image buffer. +pub type ImageBuffer<'a> = image::ImageBuffer, &'a [u8]>; + +/// Paint element types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)] +#[allow(missing_docs)] +#[repr(i32)] +pub enum PaintElementType { + View = 0, + Popup = 1, +} + +/// Log severity levels. +#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)] +#[repr(i32)] +pub enum LogSeverity { + /// Default logging (currently INFO logging). + Default = 0, + /// DEBUG logging. + Debug = 1, + /// INFO logging. + Info = 2, + /// WARNING logging. + Warning = 3, + /// ERROR logging. + Error = 4, + /// FATAL logging. + Fatal = 5, + /// Disable logging to file for all messages, and to stderr for messages + /// with severity less than FATAL. + Disable = 99, +} + +/// Represents a handler for browser events. +/// +/// **All the functions are called on the main thread.** +#[allow(unused_variables)] +pub trait BrowserHandler { + /// Called when the browser is created. + fn on_created(&mut self) {} + + /// Called when the browser is closed. + fn on_closed(&mut self) {} + + /// Called when the browser wants to show or hide the popup widget. + fn on_popup_show(&mut self, show: bool) {} + + /// Called when the browser wants to move or resize the popup widget. + fn on_popup_position(&mut self, rect: Rect>) {} + + /// Called when an element should be painted. + /// + /// Pixel values passed to this method are scaled relative to view + /// coordinates based on the value of + /// [`crate::BrowserBuilder::device_scale_factor`]. + /// + /// `type` indicates whether the element is the view or the popup widget. + /// + /// `image_buffer` contains the pixel data for the whole image. + /// + /// `dirty_rects` contains the set of rectangles in pixel coordinates that + /// need to be repainted. + fn on_paint( + &mut self, + type_: PaintElementType, + dirty_rects: &DirtyRects, + image_buffer: ImageBuffer, + ) { + } + + /// Called when the address of the frame changes. + fn on_address_changed(&mut self, frame: Frame, url: &str) {} + + /// Called when the title changes. + fn on_title_changed(&mut self, title: &str) {} + + /// Called when the page icon changes. + fn on_favicon_url_changed(&mut self, urls: &[&str]) {} + + /// Called when the browser is about to display a tooltip. + fn on_tooltip(&mut self, text: &str) {} + + /// Called when the browser receives a status message. + fn on_status_message(&mut self, text: &str) {} + + /// Called to display a console message. + fn on_console_message( + &mut self, + message: &str, + level: LogSeverity, + source: &str, + line_number: i32, + ) { + } + + /// Called when the cursor changes. + /// + /// Return `true` if the cursor change was handled or false for default + /// handling. + fn on_cursor_changed( + &mut self, + cursor_type: CursorType, + cursor_info: Option, + ) -> bool { + false + } + + /// Called when preparing to open a popup browser window. + fn on_before_popup(&mut self, url: &str) {} + + /// Called when the overall page loading progress changes. + /// + /// `progress` ranges from 0.0 to 1.0. + fn on_loading_progress_changed(&mut self, progress: f32) {} + + /// Called when the loading state changes. + /// + /// This callback will be executed twice -- once when loading is initiated + /// either programmatically or by user action, and once when loading is + /// terminated due to completion, cancellation of failure. + /// + /// It will be called before any calls to `on_load_start` and after all + /// calls to `on_load_error` and/or `on_load_end`. + fn on_loading_state_changed( + &mut self, + is_loading: bool, + can_go_back: bool, + can_go_forward: bool, + ) { + } + + /// Called after a navigation has been committed and before the browser + /// begins loading contents in the frame. + fn on_load_start(&mut self, frame: Frame) {} + + /// Called when the browser is done loading a frame. + fn on_load_end(&mut self, frame: Frame) {} + + /// Called when a navigation fails or is canceled. + /// + /// This method may be called by itself if before commit or in combination + /// with `on_load_start`/`on_load_end` if after commit. + fn on_load_error(&mut self, frame: Frame, error_text: &str, failed_url: &str) {} + + /// Called when the IME composition range changes. + fn on_ime_composition_range_changed(&mut self, bounds: Rect>) {} + + /// Called when a file dialog is requested. + /// + /// To display the default dialog return `false`. + fn on_file_dialog( + &mut self, + mode: FileDialogMode, + title: Option<&str>, + default_file_path: Option<&str>, + accepts: &[Accept], + callback: FileDialogCallback, + ) -> bool { + false + } + + /// Called when before displaying a context menu. + fn on_context_menu(&mut self, frame: Frame, params: ContextMenuParams) {} + + /// Called to report find results returned by [`crate::Browser::find`]. + /// + /// `identifier`` is a unique incremental identifier for the currently + /// active search. + /// `count`` is the number of matches currently identified. + /// `selection_rect` is the location of where the match was found (in window + /// coordinates). + /// `active_match_ordinal` is the current position in the search results. + /// `final_update` is `true` if this is the last find notification. + fn on_find_result( + &mut self, + identifier: i32, + count: i32, + selection_rect: Rect>, + active_match_ordinal: i32, + final_update: bool, + ) { + } + + /// Called to run a JavaScript dialog. + /// + /// The `default_prompt_text` value will be specified for prompt dialogs + /// only. + /// + /// Return `true` if the application will use a custom + /// dialog or if the callback has been executed immediately. Custom dialogs + /// may be either modal or modeless. + /// + /// If a custom dialog is used the application must execute `callback` once + /// the custom dialog is dismissed. + fn on_js_dialog( + &mut self, + type_: JsDialogType, + message_text: &str, + callback: JsDialogCallback, + ) -> bool { + false + } +} + +impl BrowserHandler for () {} + +pub(crate) extern "C" fn on_created(userdata: *mut c_void) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_created(); + } +} + +pub(crate) extern "C" fn on_closed(userdata: *mut c_void) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_closed(); + } +} + +pub(crate) extern "C" fn on_popup_show(userdata: *mut c_void, show: bool) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_popup_show(show); + } +} + +pub(crate) extern "C" fn on_popup_position( + userdata: *mut c_void, + rect: *const Rect, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_popup_position((*rect).map(LogicalUnit)); + } +} + +pub(crate) extern "C" fn on_paint( + userdata: *mut c_void, + type_: i32, + dirty_rects: *const c_void, + image_buffer: *const c_void, + width: u32, + height: u32, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let type_ = PaintElementType::try_from(type_).expect("BUG: invalid paint element type"); + let dirty_rects = DirtyRects::new(dirty_rects); + let image_buffer = + std::slice::from_raw_parts(image_buffer as *const u8, (width * height * 4) as usize); + state.handler.on_paint( + type_, + &dirty_rects, + ImageBuffer::from_raw(width, height, image_buffer).unwrap(), + ); + } +} + +pub(crate) extern "C" fn on_address_changed( + userdata: *mut c_void, + frame: *mut wef_frame_t, + url: *const c_char, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let frame = Frame(frame); + let url = CStr::from_ptr(url).to_string_lossy(); + state.handler.on_address_changed(frame, &url); + } +} + +pub(crate) extern "C" fn on_title_changed( + userdata: *mut c_void, + title: *const c_char, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let title = CStr::from_ptr(title).to_string_lossy(); + state.handler.on_title_changed(&title); + } +} + +pub(crate) extern "C" fn on_favicon_url_changed( + userdata: *mut c_void, + urls: *const *const c_char, + num_urls: i32, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let urls = std::slice::from_raw_parts(urls, num_urls as usize) + .iter() + .filter_map(|url| CStr::from_ptr(*url).to_str().ok()) + .collect::>(); + state.handler.on_favicon_url_changed(&urls); + } +} + +pub(crate) extern "C" fn on_tooltip(userdata: *mut c_void, text: *const c_char) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let text = CStr::from_ptr(text).to_string_lossy(); + state.handler.on_tooltip(&text); + } +} + +pub(crate) extern "C" fn on_status_message( + userdata: *mut c_void, + text: *const c_char, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let text = CStr::from_ptr(text).to_string_lossy(); + state.handler.on_status_message(&text); + } +} + +pub(crate) extern "C" fn on_console_message( + userdata: *mut c_void, + message: *const c_char, + level: i32, + source: *const c_char, + line: i32, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let message = CStr::from_ptr(message).to_string_lossy(); + let source = CStr::from_ptr(source).to_string_lossy(); + let level = LogSeverity::try_from(level).expect("BUG: invalid log severity"); + state + .handler + .on_console_message(&message, level, &source, line); + } +} + +pub(crate) extern "C" fn on_cursor_changed( + userdata: *mut c_void, + cursor_type: i32, + custom_cursor_info: *const c_void, +) -> bool { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let cursor_type = CursorType::try_from(cursor_type).expect("BUG: invalid file dialog mode"); + let cursor_info = if !custom_cursor_info.is_null() { + let mut hotspot: MaybeUninit> = MaybeUninit::uninit(); + let mut size: MaybeUninit> = MaybeUninit::uninit(); + + wef_cursor_info_hotspot(custom_cursor_info, hotspot.as_mut_ptr()); + wef_cursor_info_size(custom_cursor_info, size.as_mut_ptr()); + + let hotspot = hotspot.assume_init(); + let size = size.assume_init(); + + let image_buffer = std::slice::from_raw_parts( + wef_cursor_info_buffer(custom_cursor_info) as *const u8, + (size.width * size.height * 4) as usize, + ); + + Some(CursorInfo { + hotspot, + scale_factor: wef_cursor_info_image_scale_factor(custom_cursor_info), + image: ImageBuffer::from_raw(size.width as u32, size.height as u32, image_buffer) + .expect("BUG: invalid image buffer size"), + }) + } else { + None + }; + state.handler.on_cursor_changed(cursor_type, cursor_info) + } +} + +pub(crate) extern "C" fn on_before_popup( + userdata: *mut c_void, + url: *const c_char, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let url = CStr::from_ptr(url).to_string_lossy(); + state.handler.on_before_popup(&url); + } +} + +pub(crate) extern "C" fn on_loading_progress_changed( + userdata: *mut c_void, + progress: f32, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_loading_progress_changed(progress); + } +} + +pub(crate) extern "C" fn on_loading_state_changed( + userdata: *mut c_void, + is_loading: bool, + can_go_back: bool, + can_go_forward: bool, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state + .handler + .on_loading_state_changed(is_loading, can_go_back, can_go_forward); + } +} + +pub(crate) extern "C" fn on_load_start( + userdata: *mut c_void, + frame: *mut wef_frame_t, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let frame = Frame(frame); + state.handler.on_load_start(frame); + } +} + +pub(crate) extern "C" fn on_load_end( + userdata: *mut c_void, + frame: *mut wef_frame_t, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let frame = Frame(frame); + state.handler.on_load_end(frame); + } +} + +pub(crate) extern "C" fn on_load_error( + userdata: *mut c_void, + frame: *mut wef_frame_t, + error_text: *const c_char, + failed_url: *const c_char, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let frame = Frame(frame); + let error_text = CStr::from_ptr(error_text).to_string_lossy(); + let failed_url = CStr::from_ptr(failed_url).to_string_lossy(); + state.handler.on_load_error(frame, &error_text, &failed_url); + } +} + +pub(crate) extern "C" fn on_ime_composition_range_changed( + userdata: *mut c_void, + bounds: *const Rect, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state + .handler + .on_ime_composition_range_changed((*bounds).map(LogicalUnit)); + } +} + +pub(crate) extern "C" fn on_file_dialog( + userdata: *mut c_void, + mode: i32, + title: *const c_char, + default_file_path: *const c_char, + accept_filters: *const c_char, + accept_extensions: *const c_char, + accept_descriptions: *const c_char, + callback: *mut c_void, +) -> bool { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let mode = FileDialogMode::try_from(mode).expect("BUG: invalid file dialog mode"); + let title = CStr::from_ptr(title).to_string_lossy(); + let default_file_path = CStr::from_ptr(default_file_path).to_string_lossy(); + let accept_filters = CStr::from_ptr(accept_filters).to_string_lossy(); + let accept_extensions = CStr::from_ptr(accept_extensions).to_string_lossy(); + let accept_descriptions = CStr::from_ptr(accept_descriptions).to_string_lossy(); + let mut extensions_vec = vec![]; + let mut accepts = vec![]; + const DELIMITER: &str = "@@@"; + + for ((filter, extensions), description) in accept_filters + .split(DELIMITER) + .zip(accept_extensions.split(DELIMITER)) + .zip(accept_descriptions.split(DELIMITER)) + { + let filter = if filter.starts_with('.') { + AcceptFilter::Extension(filter) + } else { + let Ok(mime) = filter.parse() else { + continue; + }; + AcceptFilter::Mime(mime) + }; + + let extensions = (!extensions.is_empty()).then(|| { + extensions_vec.push(extensions.split(';').collect::>()); + extensions_vec.len() - 1 + }); + + let description = (!description.is_empty()).then_some(description); + accepts.push((filter, extensions, description)); + } + + let accepts = accepts + .into_iter() + .map(|(filter, extensions, description)| Accept { + filters: filter, + extensions: extensions.map(|idx| &*extensions_vec[idx]), + description, + }) + .collect::>(); + + let title = (!title.is_empty()).then_some(&*title); + let default_file_path = (!default_file_path.is_empty()).then_some(&*default_file_path); + + state.handler.on_file_dialog( + mode, + title, + default_file_path, + &accepts, + FileDialogCallback::new(callback), + ) + } +} + +pub(crate) extern "C" fn on_context_menu( + userdata: *mut c_void, + frame: *mut wef_frame_t, + params: *const CContextMenuParams, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let frame = Frame(frame); + let link_url = (!(*params).link_url.is_null()) + .then(|| CStr::from_ptr((*params).link_url).to_string_lossy()); + let unfiltered_link_url = (!(*params).unfiltered_link_url.is_null()) + .then(|| CStr::from_ptr((*params).unfiltered_link_url).to_string_lossy()); + let source_url = (!(*params).source_url.is_null()) + .then(|| CStr::from_ptr((*params).source_url).to_string_lossy()); + let title_text = (!(*params).title_text.is_null()) + .then(|| CStr::from_ptr((*params).title_text).to_string_lossy()); + let page_url = CStr::from_ptr((*params).page_url).to_string_lossy(); + let frame_url = CStr::from_ptr((*params).frame_url).to_string_lossy(); + let selection_text = (!(*params).selection_text.is_null()) + .then(|| CStr::from_ptr((*params).selection_text).to_string_lossy()); + + state.handler.on_context_menu( + frame, + ContextMenuParams { + crood: Point::new( + LogicalUnit((*params).x_crood), + LogicalUnit((*params).y_crood), + ), + type_: ContextMenuTypeFlags::from_bits_truncate((*params).type_flags), + link_url: link_url.as_deref(), + unfiltered_link_url: unfiltered_link_url.as_deref(), + source_url: source_url.as_deref(), + has_image_contents: (*params).has_image_contents, + title_text: title_text.as_deref(), + page_url: &page_url, + frame_url: &frame_url, + media_type: ContextMenuMediaType::try_from((*params).media_type) + .unwrap_or_default(), + media_state_flags: ContextMenuMediaStateFlags::from_bits_truncate( + (*params).media_state_flags, + ), + selection_text: selection_text.as_deref(), + is_editable: (*params).is_editable, + edit_state_flags: ContextMenuEditStateFlags::from_bits_truncate( + (*params).edit_state_flags, + ), + }, + ); + } +} + +pub(crate) extern "C" fn on_find_result( + userdata: *mut c_void, + identifier: i32, + count: i32, + selection_rect: *const Rect, + active_match_ordinal: i32, + final_update: bool, +) { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + state.handler.on_find_result( + identifier, + count, + (*selection_rect).map(LogicalUnit), + active_match_ordinal, + final_update, + ); + } +} + +pub(crate) extern "C" fn on_js_dialog( + userdata: *mut c_void, + type_: i32, + message_text: *const c_char, + default_prompt_text: *const c_char, + callback: *mut c_void, +) -> bool { + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let default_prompt_text = CStr::from_ptr(default_prompt_text).to_string_lossy(); + let type_ = match type_ { + 0 => JsDialogType::Alert, + 1 => JsDialogType::Confirm, + 2 => JsDialogType::Prompt { + default_prompt_text: &default_prompt_text, + }, + _ => panic!("BUG: invalid js dialog type"), + }; + let message_text = CStr::from_ptr(message_text).to_string_lossy(); + state + .handler + .on_js_dialog(type_, &message_text, JsDialogCallback::new(callback)) + } +} + +pub(crate) extern "C" fn on_query( + userdata: *mut c_void, + frame: *mut wef_frame_t, + query: *const c_char, + callback: *mut wef_query_callback_t, +) { + #[derive(Debug, Deserialize)] + struct Request { + method: String, + args: Vec, + } + + let frame = Frame(frame); + + unsafe { + let state = &mut *(userdata as *mut BrowserState); + let Some(request) = CStr::from_ptr(query) + .to_str() + .ok() + .and_then(|value| serde_json::from_str::(value).ok()) + else { + return; + }; + + state.func_registry.call( + frame, + &request.method, + request.args, + QueryCallback::new(callback), + ) + } +} diff --git a/wef/src/builder.rs b/wef/src/builder.rs new file mode 100644 index 0000000..025ad22 --- /dev/null +++ b/wef/src/builder.rs @@ -0,0 +1,186 @@ +use std::ffi::{CString, c_void}; + +use raw_window_handle::RawWindowHandle; + +use crate::{Browser, BrowserHandler, FuncRegistry, ffi::*}; + +/// A builder for creating a browser instance. +pub struct BrowserBuilder { + parent: Option, + width: u32, + height: u32, + device_scale_factor: f32, + frame_rate: u32, + url: String, + handler: T, + func_registry: FuncRegistry, +} + +impl BrowserBuilder<()> { + pub(crate) fn new() -> BrowserBuilder<()> { + BrowserBuilder { + parent: None, + width: 100, + height: 100, + device_scale_factor: 1.0, + frame_rate: 60, + url: "about:blank".to_string(), + handler: (), + func_registry: Default::default(), + } + } +} + +pub(crate) struct BrowserState { + pub(crate) handler: T, + pub(crate) func_registry: FuncRegistry, +} + +impl BrowserBuilder +where + T: BrowserHandler, +{ + /// Sets the parent window handle. + /// + /// Default is `None`. + pub fn parent(self, parent: impl Into>) -> Self { + Self { + parent: parent.into(), + ..self + } + } + + /// Sets the size of the render target. + pub fn size(self, width: u32, height: u32) -> Self { + Self { + width, + height, + ..self + } + } + + /// Sets the device scale factor. + /// + /// Default is `1.0`. + #[inline] + pub fn device_scale_factor(self, device_scale_factor: f32) -> Self { + Self { + device_scale_factor, + ..self + } + } + + /// Sets the frame rate. + /// + /// Default is `60`. + pub fn frame_rate(self, frame_rate: u32) -> Self { + Self { frame_rate, ..self } + } + + /// Sets the URL to load. + /// + /// Default is `about:blank`. + pub fn url(self, url: impl Into) -> Self { + Self { + url: url.into(), + ..self + } + } + + /// Sets the event handler. + #[inline] + pub fn handler(self, handler: Q) -> BrowserBuilder + where + Q: BrowserHandler, + { + BrowserBuilder { + parent: self.parent, + width: self.width, + height: self.height, + device_scale_factor: self.device_scale_factor, + frame_rate: self.frame_rate, + url: self.url, + handler, + func_registry: self.func_registry, + } + } + + /// Sets the function registry. + /// + /// See also [`FuncRegistry`] for more details. + pub fn func_registry(self, func_registry: FuncRegistry) -> Self { + Self { + func_registry, + ..self + } + } + + /// Cosumes the builder and creates a [`Browser`] instance. + /// + /// The creation of the browser is asynchronous, and the + /// [`BrowserHandler::on_created`] method will be called upon completion. + pub fn build(self) -> Browser { + let callbacks = CBrowserCallbacks { + on_created: crate::browser_handler::on_created::, + on_closed: crate::browser_handler::on_closed::, + on_popup_show: crate::browser_handler::on_popup_show::, + on_popup_position: crate::browser_handler::on_popup_position::, + on_paint: crate::browser_handler::on_paint::, + on_address_changed: crate::browser_handler::on_address_changed::, + on_title_changed: crate::browser_handler::on_title_changed::, + on_favicon_url_changed: crate::browser_handler::on_favicon_url_changed::, + on_tooltip: crate::browser_handler::on_tooltip::, + on_status_message: crate::browser_handler::on_status_message::, + on_console_message: crate::browser_handler::on_console_message::, + on_cursor_changed: crate::browser_handler::on_cursor_changed::, + on_before_popup: crate::browser_handler::on_before_popup::, + on_loading_progress_changed: crate::browser_handler::on_loading_progress_changed::, + on_loading_state_changed: crate::browser_handler::on_loading_state_changed::, + on_load_start: crate::browser_handler::on_load_start::, + on_load_end: crate::browser_handler::on_load_end::, + on_load_error: crate::browser_handler::on_load_error::, + on_ime_composition_range_changed: + crate::browser_handler::on_ime_composition_range_changed::, + on_file_dialog: crate::browser_handler::on_file_dialog::, + on_context_menu: crate::browser_handler::on_context_menu::, + on_find_result: crate::browser_handler::on_find_result::, + on_js_dialog: crate::browser_handler::on_js_dialog::, + on_query: crate::browser_handler::on_query::, + }; + let handler = Box::into_raw(Box::new(BrowserState { + handler: self.handler, + func_registry: self.func_registry.clone(), + })); + let parent_window_handle: *const c_void = match self.parent { + Some(RawWindowHandle::Win32(handle)) => handle.hwnd.get() as *const c_void, + Some(RawWindowHandle::AppKit(handle)) => handle.ns_view.as_ptr(), + Some(RawWindowHandle::Xcb(handle)) => handle.window.get() as *const c_void, + _ => std::ptr::null(), + }; + + extern "C" fn destroy_handler(user_data: *mut c_void) { + unsafe { _ = Box::from_raw(user_data as *mut T) } + } + + let url_cstr = CString::new(self.url).unwrap(); + let inject_javascript = CString::new(self.func_registry.javascript()).unwrap(); + let settings = CBrowserSettings { + parent: parent_window_handle, + device_scale_factor: self.device_scale_factor, + width: self.width as i32, + height: self.height as i32, + frame_rate: self.frame_rate as i32, + url: url_cstr.as_ptr(), + inject_javascript: inject_javascript.as_ptr(), + callbacks, + userdata: handler as *mut c_void, + destroy_userdata: destroy_handler::, + }; + + unsafe { + Browser { + wef_browser: wef_browser_create(&settings), + } + } + } +} diff --git a/wef/src/context_menu.rs b/wef/src/context_menu.rs new file mode 100644 index 0000000..9ce6b67 --- /dev/null +++ b/wef/src/context_menu.rs @@ -0,0 +1,147 @@ +use num_enum::TryFromPrimitive; + +use crate::{LogicalUnit, Point}; + +bitflags::bitflags! { + /// The type of node that is selected. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct ContextMenuTypeFlags: i32 { + /// No node is selected. + const NONE = 0; + /// The top page is selected. + const PAGE = 1 << 0; + /// A subframe page is selected. + const FRAME = 1 << 1; + /// A link is selected. + const LINK = 1 << 2; + /// A media node is selected. + const MEDIA = 1 << 3; + /// There is a textual or mixed selection that is selected. + const SELECTION = 1 << 4; + /// An editable element is selected. + const EDITABLE = 1 << 5; + } +} + +/// Supported context menu media types. +#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, Default)] +#[repr(i32)] +pub enum ContextMenuMediaType { + /// No special node is in context. + #[default] + None = 0, + /// An image node is selected. + Image = 1, + /// A video node is selected. + Video = 2, + /// An audio node is selected. + Audio = 3, + /// A canvas node is selected. + Canvas = 4, + /// A file node is selected. + File = 5, + /// A plugin node is selected. + Plugin = 6, +} + +bitflags::bitflags! { + /// Supported context menu media state bit flags. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct ContextMenuMediaStateFlags: i32 { + /// No special state is in context. + const NONE = 0; + /// The media is in error. + const IN_ERROR = 1 << 0; + /// The media is paused. + const PAUSED = 1 << 1; + /// The media is muted. + const MUTED = 1 << 2; + /// The media is set to loop. + const LOOP = 1 << 3; + /// The media can be saved. + const CAN_SAVE = 1 << 4; + /// The media has audio. + const HAS_AUDIO = 1 << 5; + /// The media can toggle controls. + const CAN_TOGGLE_CONTROLS = 1 << 6; + /// The media has controls enabled. + const CONTROLS = 1 << 7; + /// The media can be printed. + const CAN_PRINT = 1 << 8; + /// The media can be rotated. + const CAN_ROTATE = 1 << 9; + /// The media can be displayed in picture-in-picture mode. + const CAN_PICTURE_IN_PICTURE = 1 << 10; + /// The media is in picture-in-picture mode. + const PICTURE_IN_PICTURE = 1 << 11; + /// The media can be looped. + const CAN_LOOP = 1 << 12; + } +} + +bitflags::bitflags! { + /// Supported context menu type bit flags. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct ContextMenuEditStateFlags: i32 { + /// No special state is in context. + const NONE = 0; + /// The edit control can be undone. + const CAN_UNDO = 1 << 0; + /// The edit control can be redone. + const CAN_REDO = 1 << 1; + /// The edit control can be cut. + const CAN_CUT = 1 << 2; + /// The edit control can be copied. + const CAN_COPY = 1 << 3; + /// The edit control can be pasted. + const CAN_PASTE = 1 << 4; + /// The edit control can be deleted. + const CAN_DELETE = 1 << 5; + /// The edit control can select all text. + const CAN_SELECT_ALL = 1 << 6; + /// The edit control can be translated. + const CAN_TRANSLATE = 1 << 7; + /// The edit control can be edited richly. + const CAN_EDIT_RICHLY = 1 << 8; + } +} + +/// Provides information about the context menu state. +#[derive(Debug)] +pub struct ContextMenuParams<'a> { + /// The X coordinate of the mouse where the context menu was invoked. + /// + /// Coords are relative to the associated RenderView's origin. + pub crood: Point>, + /// The flags representing the type of node that the context menu was + /// invoked on. + pub type_: ContextMenuTypeFlags, + /// The URL of the link + pub link_url: Option<&'a str>, + /// The link URL to be used ONLY for "copy link address". + pub unfiltered_link_url: Option<&'a str>, + /// The source URL for the element that the context menu was invoked on. + /// + /// Example of elements with source URLs are img, audio, and video. + pub source_url: Option<&'a str>, + /// Whether the element that the context menu was invoked on has image + /// contents. + pub has_image_contents: bool, + /// The title text or the alt text if the context menu was invoked on + /// an image. + pub title_text: Option<&'a str>, + /// The URL of the top level page that the context menu was invoked on. + pub page_url: &'a str, + /// The URL of the subframe that the context menu was invoked on. + pub frame_url: &'a str, + /// The type of context node that the context menu was invoked on. + pub media_type: ContextMenuMediaType, + /// The flags representing the actions supported by the media element. + pub media_state_flags: ContextMenuMediaStateFlags, + /// The text of the selection, if any, that the context menu was invoked on. + pub selection_text: Option<&'a str>, + /// Whether the context menu was invoked on an editable element. + pub is_editable: bool, + /// The flags representing the actions supported by the editable node. + pub edit_state_flags: ContextMenuEditStateFlags, +} diff --git a/wef/src/cursor.rs b/wef/src/cursor.rs new file mode 100644 index 0000000..0806997 --- /dev/null +++ b/wef/src/cursor.rs @@ -0,0 +1,72 @@ +use num_enum::TryFromPrimitive; + +use crate::{ImageBuffer, Point}; + +/// Cursor type values. +#[derive(Debug, Clone, Copy, TryFromPrimitive)] +#[repr(i32)] +#[allow(missing_docs)] +pub enum CursorType { + Pointer = 0, + Cross = 1, + Hand = 2, + IBeam = 3, + Wait = 4, + Help = 5, + EastResize = 6, + NorthResize = 7, + NorthEastResize = 8, + NorthWestResize = 9, + SouthResize = 10, + SouthEastResize = 11, + SouthWestResize = 12, + WestResize = 13, + NorthSouthResize = 14, + EastWestResize = 15, + NorthEastSouthWestResize = 16, + NorthWestSouthEastResize = 17, + ColumnResize = 18, + RowResize = 19, + MiddlePanning = 20, + EastPanning = 21, + NorthPanning = 22, + NorthEastPanning = 23, + NorthWestPanning = 24, + SouthPanning = 25, + SouthEastPanning = 26, + SouthWestPanning = 27, + WestPanning = 28, + Move = 29, + VerticalText = 30, + Cell = 31, + ContextMenu = 32, + Alias = 33, + Progress = 34, + NoDrop = 35, + Copy = 36, + None = 37, + NotAllowed = 38, + ZoomIn = 39, + ZoomOut = 40, + Grab = 41, + Grabbing = 42, + MiddlePanningVertical = 43, + MiddlePanningHorizontal = 44, + Custom = 45, + DndNone = 46, + DndMove = 47, + DndCopy = 48, + DndLink = 49, +} + +/// Representing cursor information. +#[derive(Debug)] +pub struct CursorInfo<'a> { + /// The hotspot of the cursor, which is the point in the image that will be + /// used as the actual cursor position. + pub hotspot: Point, + /// The scale factor of the cursor. + pub scale_factor: f32, + /// The image buffer of the cursor. + pub image: ImageBuffer<'a>, +} diff --git a/wef/src/dirty_rects.rs b/wef/src/dirty_rects.rs new file mode 100644 index 0000000..9af92c2 --- /dev/null +++ b/wef/src/dirty_rects.rs @@ -0,0 +1,95 @@ +use std::{ffi::c_void, fmt, marker::PhantomData, mem::MaybeUninit}; + +use crate::{PhysicalUnit, Rect, ffi::*}; + +/// Dirty rectangles for CEF. +pub struct DirtyRects<'a> { + ptr: *const c_void, + _mark: PhantomData<&'a ()>, +} + +impl fmt::Debug for DirtyRects<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ls = f.debug_list(); + for rect in self.iter() { + ls.entry(&rect); + } + ls.finish() + } +} + +impl<'a> DirtyRects<'a> { + #[inline] + pub(crate) fn new(dirty_rects: *const c_void) -> Self { + DirtyRects { + ptr: dirty_rects, + _mark: PhantomData, + } + } + + /// Returns the number of dirty rectangles. + #[inline] + pub fn len(&self) -> usize { + unsafe { wef_dirty_rects_len(self.ptr) as usize } + } + + /// Returns `true` if there are no dirty rectangles. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the dirty rectangle at the specified index. + #[inline] + pub fn get(&self, i: usize) -> Rect> { + unsafe { + debug_assert!(i < self.len(), "index out of bounds"); + let mut rect: MaybeUninit> = MaybeUninit::uninit(); + wef_dirty_rects_get(self.ptr, i as i32, rect.as_mut_ptr()); + rect.assume_init().map(PhysicalUnit) + } + } + + /// Returns an iterator over the dirty rectangles. + #[inline] + pub fn iter(&self) -> DirtyRectsIter<'a> { + DirtyRectsIter { + ptr: self.ptr, + index: 0, + _mark: PhantomData, + } + } +} + +impl<'a> IntoIterator for &'a DirtyRects<'a> { + type Item = Rect>; + type IntoIter = DirtyRectsIter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// An iterator over the dirty rectangles. +pub struct DirtyRectsIter<'a> { + ptr: *const c_void, + index: usize, + _mark: PhantomData<&'a ()>, +} + +impl Iterator for DirtyRectsIter<'_> { + type Item = Rect>; + + fn next(&mut self) -> Option { + unsafe { + if self.index < wef_dirty_rects_len(self.ptr) as usize { + let mut rect: MaybeUninit> = MaybeUninit::uninit(); + wef_dirty_rects_get(self.ptr, self.index as i32, rect.as_mut_ptr()); + self.index += 1; + Some(rect.assume_init().map(PhysicalUnit)) + } else { + None + } + } + } +} diff --git a/wef/src/dpi.rs b/wef/src/dpi.rs new file mode 100644 index 0000000..170197e --- /dev/null +++ b/wef/src/dpi.rs @@ -0,0 +1,43 @@ +/// A physical pixel unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PhysicalUnit(pub T); + +macro_rules! impl_to_logical { + ($($unit:ty),*) => { + $( + impl PhysicalUnit<$unit> { + /// Converts the physical unit to a logical unit using the device scale + /// factor. + #[inline] + pub fn to_logical(&self, device_scale_factor: f32) -> LogicalUnit<$unit> { + LogicalUnit((self.0 as f32 / device_scale_factor) as $unit) + } + } + )* + }; + () => {}; +} + +impl_to_logical!(i32, u32, i64, u64, f32, f64); + +/// A logical pixel unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct LogicalUnit(pub T); + +macro_rules! impl_to_physical { + ($($unit:ty),*) => { + $( + impl LogicalUnit<$unit> { + /// Converts the logical unit to a physical unit using the device scale + /// factor. + #[inline] + pub fn to_physical(&self, device_scale_factor: f32) -> PhysicalUnit<$unit> { + PhysicalUnit((self.0 as f32 * device_scale_factor) as $unit) + } + } + )* + }; + () => {}; +} + +impl_to_physical!(i32, u32, i64, u64, f32, f64); diff --git a/wef/src/error.rs b/wef/src/error.rs new file mode 100644 index 0000000..2c0c035 --- /dev/null +++ b/wef/src/error.rs @@ -0,0 +1,15 @@ +/// Error type. +#[derive(thiserror::Error, Debug, Clone)] +pub enum Error { + /// Failed to load the CEF framework. + #[cfg(target_os = "macos")] + #[error("failed to load CEF library")] + LoadLibrary, + /// Failed to create the sandbox context. + #[cfg(target_os = "macos")] + #[error("failed to create the sandbox context")] + SandboxContextCreate, + /// Failed to initialize the CEF browser process. + #[error("failed to initialize the CEF browser process")] + InitializeBrowserProcess, +} diff --git a/wef/src/ffi.rs b/wef/src/ffi.rs new file mode 100644 index 0000000..033337e --- /dev/null +++ b/wef/src/ffi.rs @@ -0,0 +1,337 @@ +#![allow(non_camel_case_types)] + +use std::ffi::{CStr, c_char, c_void}; + +use crate::{Point, Rect, Size}; + +pub(crate) type wef_browser_t = c_void; +pub(crate) type wef_frame_t = c_void; +pub(crate) type wef_cursor_info_t = c_void; +pub(crate) type wef_file_dialog_callback_t = c_void; +pub(crate) type wef_js_dialog_callback_t = c_void; +pub(crate) type wef_query_callback_t = c_void; + +type DestroyFn = extern "C" fn(*mut c_void); + +#[repr(C)] +pub(crate) struct CAppCallbacks { + pub(crate) on_schedule_message_pump_work: extern "C" fn(*mut c_void, i32), +} + +#[repr(C)] +pub(crate) struct CSettings { + pub(crate) locale: *const c_char, + pub(crate) cache_path: *const c_char, + pub(crate) root_cache_path: *const c_char, + pub(crate) browser_subprocess_path: *const c_char, + pub(crate) callbacks: CAppCallbacks, + pub(crate) userdata: *mut c_void, + pub(crate) destroy_userdata: DestroyFn, +} + +#[repr(C)] +pub(crate) struct CBrowserSettings { + pub(crate) parent: *const c_void, + pub(crate) device_scale_factor: f32, + pub(crate) width: i32, + pub(crate) height: i32, + pub(crate) frame_rate: i32, + pub(crate) url: *const c_char, + pub(crate) inject_javascript: *const c_char, + pub(crate) callbacks: CBrowserCallbacks, + pub(crate) userdata: *mut c_void, + pub(crate) destroy_userdata: DestroyFn, +} + +#[repr(C)] +pub(crate) struct CContextMenuParams { + pub(crate) x_crood: i32, + pub(crate) y_crood: i32, + pub(crate) type_flags: i32, + pub(crate) link_url: *const c_char, + pub(crate) unfiltered_link_url: *const c_char, + pub(crate) source_url: *const c_char, + pub(crate) has_image_contents: bool, + pub(crate) title_text: *const c_char, + pub(crate) page_url: *const c_char, + pub(crate) frame_url: *const c_char, + pub(crate) media_type: i32, + pub(crate) media_state_flags: i32, + pub(crate) selection_text: *const c_char, + pub(crate) is_editable: bool, + pub(crate) edit_state_flags: i32, +} + +#[repr(C)] +pub(crate) struct CBrowserCallbacks { + pub(crate) on_created: extern "C" fn(*mut c_void), + pub(crate) on_closed: extern "C" fn(*mut c_void), + pub(crate) on_popup_show: extern "C" fn(*mut c_void, bool), + pub(crate) on_popup_position: extern "C" fn(*mut c_void, *const Rect), + pub(crate) on_paint: extern "C" fn(*mut c_void, i32, *const c_void, *const c_void, u32, u32), + pub(crate) on_address_changed: extern "C" fn(*mut c_void, *mut wef_frame_t, *const c_char), + pub(crate) on_title_changed: extern "C" fn(*mut c_void, *const c_char), + pub(crate) on_favicon_url_changed: extern "C" fn(*mut c_void, *const *const c_char, i32), + pub(crate) on_tooltip: extern "C" fn(*mut c_void, *const c_char), + pub(crate) on_status_message: extern "C" fn(*mut c_void, *const c_char), + pub(crate) on_console_message: + extern "C" fn(*mut c_void, *const c_char, i32, *const c_char, i32), + pub(crate) on_cursor_changed: extern "C" fn( + *mut c_void, + cursor_type: i32, + custom_cursor_info: *const wef_cursor_info_t, + ) -> bool, + pub(crate) on_before_popup: extern "C" fn(*mut c_void, *const c_char), + pub(crate) on_loading_progress_changed: extern "C" fn(*mut c_void, f32), + pub(crate) on_loading_state_changed: extern "C" fn(*mut c_void, bool, bool, bool), + pub(crate) on_load_start: extern "C" fn(*mut c_void, *mut wef_frame_t), + pub(crate) on_load_end: extern "C" fn(*mut c_void, *mut wef_frame_t), + pub(crate) on_load_error: + extern "C" fn(*mut c_void, *mut wef_frame_t, *const c_char, *const c_char), + pub(crate) on_ime_composition_range_changed: extern "C" fn(*mut c_void, *const Rect), + pub(crate) on_file_dialog: extern "C" fn( + *mut c_void, + i32, + *const c_char, + *const c_char, + *const c_char, + *const c_char, + *const c_char, + *mut wef_file_dialog_callback_t, + ) -> bool, + pub(crate) on_context_menu: + extern "C" fn(*mut c_void, *mut wef_frame_t, *const CContextMenuParams), + pub(crate) on_find_result: extern "C" fn(*mut c_void, i32, i32, *const Rect, i32, bool), + pub(crate) on_js_dialog: extern "C" fn( + *mut c_void, + i32, + *const c_char, + *const c_char, + *mut wef_js_dialog_callback_t, + ) -> bool, + pub(crate) on_query: + extern "C" fn(*mut c_void, *mut wef_frame_t, *const c_char, *mut wef_query_callback_t), +} + +#[inline] +pub(crate) fn to_cstr_ptr_opt(s: Option<&CStr>) -> *const c_char { + s.map(|s| s.as_ptr()).unwrap_or(std::ptr::null()) +} + +unsafe extern "C" { + #[cfg(target_os = "macos")] + pub(crate) unsafe fn wef_load_library(helper: bool) -> *mut c_void; + + #[cfg(target_os = "macos")] + pub(crate) unsafe fn wef_unload_library(loader: *mut c_void); + + #[cfg(target_os = "macos")] + pub(crate) unsafe fn wef_sandbox_context_create( + args: *const *const c_char, + count: i32, + ) -> *mut c_void; + + #[cfg(target_os = "macos")] + pub(crate) unsafe fn wef_sandbox_context_destroy(loader: *mut c_void); + + pub(crate) unsafe fn wef_init(settings: *const CSettings) -> bool; + + pub(crate) unsafe fn wef_exec_process(args: *const *const c_char, count: i32) -> bool; + + pub(crate) unsafe fn wef_shutdown(); + + pub(crate) unsafe fn wef_do_message_work(); + + pub(crate) unsafe fn wef_browser_create( + settings: *const CBrowserSettings, + ) -> *mut wef_browser_t; + + pub(crate) unsafe fn wef_browser_close(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_destroy(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_is_created(browser: *mut wef_browser_t) -> bool; + + pub(crate) unsafe fn wef_browser_set_size(browser: *mut wef_browser_t, width: i32, height: i32); + + pub(crate) unsafe fn wef_browser_load_url(cebrowserf: *mut wef_browser_t, url: *const c_char); + + pub(crate) unsafe fn wef_browser_can_go_forward(browser: *const wef_browser_t) -> bool; + + pub(crate) unsafe fn wef_browser_can_go_back(browser: *const wef_browser_t) -> bool; + + pub(crate) unsafe fn wef_browser_go_forward(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_go_back(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_reload(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_reload_ignore_cache(browser: *mut wef_browser_t); + + pub(crate) unsafe fn wef_browser_send_mouse_click_event( + browser: *mut wef_browser_t, + mouse_button_type: i32, + mouse_up: bool, + click_count: i32, + modifiers: i32, + ); + + pub(crate) unsafe fn wef_browser_send_mouse_move_event( + browser: *mut wef_browser_t, + x: i32, + y: i32, + modifiers: i32, + ); + + pub(crate) unsafe fn wef_browser_send_mouse_wheel_event( + browser: *mut wef_browser_t, + delta_x: i32, + delta_y: i32, + ); + + pub(crate) unsafe fn wef_browser_send_key_event( + browser: *mut wef_browser_t, + is_press: bool, + key_code: i32, + modifiers: i32, + ); + + pub(crate) unsafe fn wef_browser_send_char_event(browser: *mut wef_browser_t, ch: u16); + + pub(crate) unsafe fn wef_browser_ime_set_composition( + browser: *mut wef_browser_t, + text: *const c_char, + cursor_begin: u32, + cursor_end: u32, + ); + + pub(crate) unsafe fn wef_browser_ime_commit(browser: *mut wef_browser_t, text: *const c_char); + + pub(crate) unsafe fn wef_browser_get_main_frame( + browser: *mut wef_browser_t, + ) -> *mut wef_frame_t; + + pub(crate) unsafe fn wef_browser_get_focused_frame( + browser: *mut wef_browser_t, + ) -> *mut wef_frame_t; + + pub(crate) unsafe fn wef_browser_get_frame_by_name( + browser: *mut wef_browser_t, + name: *const c_char, + ) -> *mut wef_frame_t; + + pub(crate) unsafe fn wef_browser_get_frame_by_identifier( + browser: *mut wef_browser_t, + id: *const c_char, + ) -> *mut wef_frame_t; + + pub(crate) unsafe fn wef_browser_is_audio_muted(browser: *mut wef_browser_t) -> bool; + + pub(crate) unsafe fn wef_browser_set_audio_mute(browser: *mut wef_browser_t, mute: bool); + + pub(crate) unsafe fn wef_browser_find( + browser: *mut wef_browser_t, + search_text: *const c_char, + forward: bool, + match_case: bool, + find_next: bool, + ); + + pub(crate) unsafe fn wef_browser_set_focus(browser: *mut wef_browser_t, focus: bool); + + pub(crate) unsafe fn wef_dirty_rects_len(dirty_rects: *const c_void) -> i32; + + pub(crate) unsafe fn wef_dirty_rects_get( + dirty_rects: *const c_void, + i: i32, + rect: *mut Rect, + ); + + pub(crate) unsafe fn wef_frame_destroy(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_is_valid(frame: *mut wef_frame_t) -> bool; + + pub(crate) unsafe fn wef_frame_is_main(frame: *mut wef_frame_t) -> bool; + + pub(crate) unsafe fn wef_frame_name( + frame: *mut wef_frame_t, + userdata: *mut c_void, + callback: extern "C" fn(*mut c_void, *const c_char), + ) -> i32; + + pub(crate) unsafe fn wef_frame_identifier( + frame: *mut wef_frame_t, + userdata: *mut c_void, + callback: extern "C" fn(*mut c_void, *const c_char), + ) -> i32; + + pub(crate) unsafe fn wef_frame_get_url( + frame: *mut wef_frame_t, + userdata: *mut c_void, + callback: extern "C" fn(*mut c_void, *const c_char), + ) -> i32; + + pub(crate) unsafe fn wef_frame_load_url(frame: *mut wef_frame_t, url: *const c_char); + + pub(crate) unsafe fn wef_frame_parent(frame: *mut wef_frame_t) -> *mut c_void; + + pub(crate) unsafe fn wef_frame_undo(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_redo(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_cut(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_copy(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_paste(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_paste_and_match_style(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_delete(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_select_all(frame: *mut wef_frame_t); + + pub(crate) unsafe fn wef_frame_execute_javascript(frame: *mut wef_frame_t, code: *const c_char); + + pub(crate) unsafe fn wef_file_dialog_callback_continue( + callback: *mut wef_file_dialog_callback_t, + file_paths: *const c_char, + ); + + pub(crate) unsafe fn wef_file_dialog_callback_cancel(callback: *mut wef_file_dialog_callback_t); + + pub(crate) unsafe fn wef_file_dialog_callback_destroy( + callback: *mut wef_file_dialog_callback_t, + ); + + pub(crate) unsafe fn wef_cursor_info_hotspot( + info: *const wef_cursor_info_t, + point: *mut Point, + ); + + pub(crate) unsafe fn wef_cursor_info_image_scale_factor(info: *const wef_cursor_info_t) -> f32; + + pub(crate) unsafe fn wef_cursor_info_buffer(info: *const wef_cursor_info_t) -> *const c_void; + + pub(crate) unsafe fn wef_cursor_info_size(info: *const wef_cursor_info_t, size: *mut Size); + + pub(crate) unsafe fn wef_js_dialog_callback_continue( + callback: *mut wef_js_dialog_callback_t, + success: bool, + user_input: *const c_char, + ); + + pub(crate) unsafe fn wef_js_dialog_callback_destroy(callback: *mut wef_js_dialog_callback_t); + + pub(crate) unsafe fn wef_query_callback_success( + callback: *mut wef_query_callback_t, + response: *const c_char, + ); + + pub(crate) unsafe fn wef_query_callback_failure( + callback: *mut wef_query_callback_t, + error: *const c_char, + ); + + pub(crate) unsafe fn wef_query_callback_destroy(callback: *mut wef_query_callback_t); +} diff --git a/wef/src/file_dialog.rs b/wef/src/file_dialog.rs new file mode 100644 index 0000000..33fda8f --- /dev/null +++ b/wef/src/file_dialog.rs @@ -0,0 +1,73 @@ +use std::ffi::CString; + +use mime::Mime; +use num_enum::TryFromPrimitive; + +use crate::ffi::*; + +/// Supported file dialog modes. +#[derive(Debug, Clone, Copy, TryFromPrimitive)] +#[repr(i32)] +pub enum FileDialogMode { + /// Requires that the file exists before allowing the user to pick it. + Open = 0, + /// Like Open, but allows picking multiple files to open. + OpenMultiple = 1, + /// Like Open, but selects a folder to open. + OpenFolder = 2, + /// Allows picking a nonexistent file, and prompts to overwrite if the file + /// already exists. + Save = 3, +} + +#[derive(Debug)] +pub enum AcceptFilter<'a> { + Mime(Mime), + Extension(&'a str), +} + +/// Accept filter for file dialogs. +#[derive(Debug)] +pub struct Accept<'a> { + /// Used to restrict the selectable file types. + /// + /// May be any combination of valid lower-cased MIME types (e.g. "text/*" or + /// "image/*") and individual file extensions (e.g. ".txt" or ".png"). + pub filters: AcceptFilter<'a>, + /// Provides the expansion of MIME types to file extensions. + pub extensions: Option<&'a [&'a str]>, + /// Provides the descriptions for MIME types + /// + /// For example, the "image/*" mime type might have extensions + /// [".png", ".jpg", ".bmp", ...] and description "Image Files". + pub description: Option<&'a str>, +} + +/// File dialog callback. +pub struct FileDialogCallback(*mut wef_file_dialog_callback_t); + +unsafe impl Send for FileDialogCallback {} +unsafe impl Sync for FileDialogCallback {} + +impl Drop for FileDialogCallback { + fn drop(&mut self) { + unsafe { wef_file_dialog_callback_destroy(self.0) }; + } +} + +impl FileDialogCallback { + pub(crate) fn new(callback: *mut wef_file_dialog_callback_t) -> Self { + FileDialogCallback(callback) + } + + /// Continue the file dialog with the selected file paths. + pub fn continue_(&self, file_paths: &[&str]) { + let paths = CString::new(file_paths.join(";")).unwrap(); + unsafe { wef_file_dialog_callback_continue(self.0, paths.as_ptr()) }; + } + + /// Cancel the file dialog. + pub fn cancel(&self) { + unsafe { wef_file_dialog_callback_cancel(self.0) }; + } +} diff --git a/wef/src/frame.rs b/wef/src/frame.rs new file mode 100644 index 0000000..a2f7291 --- /dev/null +++ b/wef/src/frame.rs @@ -0,0 +1,153 @@ +use std::{ + ffi::{CStr, c_char, c_void}, + fmt, +}; + +use serde::Serialize; + +use crate::ffi::*; + +/// A frame in the browser window. +pub struct Frame(pub(crate) *mut wef_frame_t); + +unsafe impl Send for Frame {} +unsafe impl Sync for Frame {} + +impl fmt::Debug for Frame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Frame") + .field("name", &self.name()) + .field("identifier", &self.identifier()) + .field("url", &self.url()) + .finish() + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { wef_frame_destroy(self.0) }; + } +} + +impl Frame { + /// Returns `true` if this object is currently attached to a valid frame. + pub fn is_valid(&self) -> bool { + unsafe { wef_frame_is_valid(self.0) } + } + + /// Returns `true` if this is the main (top-level) frame. + pub fn is_main(&self) -> bool { + unsafe { wef_frame_is_main(self.0) } + } + + /// Returns the name for this frame. + /// + /// If the frame has an assigned name (for example, set via the iframe + /// "name" attribute) then that value will be returned. Otherwise a + /// unique name will be constructed based on the frame parent hierarchy. + /// + /// Returns `None` if the frame is main (top-level). + pub fn name(&self) -> Option { + let mut name = String::new(); + unsafe { wef_frame_name(self.0, &mut name as *mut _ as _, get_string_callback) }; + (!name.is_empty()).then_some(name) + } + + /// Returns the globally unique identifier for this frame or `None` if the + /// underlying frame does not yet exist. + pub fn identifier(&self) -> Option { + let mut id = String::new(); + unsafe { wef_frame_identifier(self.0, &mut id as *mut _ as _, get_string_callback) }; + (!id.is_empty()).then_some(id) + } + + /// Returns the URL currently loaded in this frame. + pub fn url(&self) -> String { + let mut url = String::new(); + unsafe { wef_frame_get_url(self.0, &mut url as *mut _ as _, get_string_callback) }; + url + } + + /// Loads the specified URL in this frame. + pub fn load_url(&self, url: &str) { + let c_url = std::ffi::CString::new(url).unwrap(); + unsafe { wef_frame_load_url(self.0, c_url.as_ptr()) }; + } + + /// Returns the parent of this frame or `None` if this is the main + /// (top-level) frame. + pub fn parent(&self) -> Option { + let frame = unsafe { wef_frame_parent(self.0) }; + if !frame.is_null() { + Some(Frame(frame)) + } else { + None + } + } + + /// Execute undo in this frame. + pub fn undo(&self) { + unsafe { wef_frame_undo(self.0) }; + } + + /// Execute redo in this frame. + pub fn redo(&self) { + unsafe { wef_frame_redo(self.0) }; + } + + /// Execute cut in this frame. + pub fn cut(&self) { + unsafe { wef_frame_cut(self.0) }; + } + + /// Execute copy in this frame. + pub fn copy(&self) { + unsafe { wef_frame_copy(self.0) }; + } + + /// Execute paste in this frame. + pub fn paste(&self) { + unsafe { wef_frame_paste(self.0) }; + } + + /// Execute paste and match style in this frame. + pub fn paste_and_match_style(&self) { + unsafe { wef_frame_paste_and_match_style(self.0) }; + } + + /// Execute delete in this frame. + pub fn delete(&self) { + unsafe { wef_frame_delete(self.0) }; + } + + /// Execute select all in this frame. + pub fn select_all(&self) { + unsafe { wef_frame_select_all(self.0) }; + } + + /// Execute javascript in this frame. + pub fn execute_javascript(&self, script: &str) { + if script.is_empty() { + return; + } + let c_script = std::ffi::CString::new(script).unwrap(); + unsafe { wef_frame_execute_javascript(self.0, c_script.as_ptr()) }; + } + + /// Emits a message to the JavaScript side. + pub fn emit(&self, message: impl Serialize) { + let Ok(message) = serde_json::to_string(&message) else { + return; + }; + self.execute_javascript(&format!("window.jsBridge.__internal.emit({})", message)); + } +} + +extern "C" fn get_string_callback(output: *mut c_void, value: *const c_char) { + unsafe { + *(output as *mut String) = CStr::from_ptr(value) + .to_str() + .unwrap_or_default() + .to_string(); + } +} diff --git a/wef/src/framework_loader.rs b/wef/src/framework_loader.rs new file mode 100644 index 0000000..1f98f94 --- /dev/null +++ b/wef/src/framework_loader.rs @@ -0,0 +1,38 @@ +use std::os::raw::c_void; + +use crate::{Error, ffi::*}; + +/// Scoped helper for loading and unloading the CEF framework library at +/// runtime from the expected location in the app bundle. +#[derive(Debug)] +pub struct FrameworkLoader(*mut c_void); + +impl Drop for FrameworkLoader { + fn drop(&mut self) { + unsafe { wef_unload_library(self.0) }; + } +} + +impl FrameworkLoader { + fn load(helper: bool) -> Result { + unsafe { + let loader = wef_load_library(helper); + if loader.is_null() { + return Err(Error::LoadLibrary); + } + Ok(Self(loader)) + } + } + + /// Load the CEF framework in the main process from the expected app + /// bundle location relative to the executable. + pub fn load_in_main() -> Result { + Self::load(false) + } + + /// Load the CEF framework in the helper process from the expected app + /// bundle location relative to the executable. + pub fn load_in_helper() -> Result { + Self::load(true) + } +} diff --git a/wef/src/func_registry/async_function_type.rs b/wef/src/func_registry/async_function_type.rs new file mode 100644 index 0000000..4f234cb --- /dev/null +++ b/wef/src/func_registry/async_function_type.rs @@ -0,0 +1,120 @@ +use serde::de::DeserializeOwned; +use serde_json::Value; + +use crate::{ + Frame, + func_registry::{CallFunctionError, into_result::IntoFunctionResult}, +}; + +/// Represents a async function type that can be called from JavaScript. +pub trait AsyncFunctionType { + /// Number of arguments. + const NUM_ARGUMENTS: usize; + + /// Calls the function with the given arguments. + fn call( + &self, + frame: Frame, + args: Vec, + ) -> impl Future> + Send + 'static; +} + +macro_rules! impl_async_function_types { + ($($name:ident),*) => { + impl AsyncFunctionType<($($name,)*), R> for F + where + F: Fn($($name),*) -> Fut + Clone + Send + Sync + 'static, + Fut: Future + Send + 'static, + Ret: IntoFunctionResult + Send, + $($name: DeserializeOwned + Send,)* + { + const NUM_ARGUMENTS: usize = tuple_len::tuple_len!(($($name,)*)); + + fn call(&self, _frame: Frame, args: Vec) -> impl Future> + Send + 'static { + let f = self.clone(); + async move { + let expected_args = tuple_len::tuple_len!(($($name,)*)); + + if args.len() != expected_args { + return Err(CallFunctionError::InvalidNumberOfArguments { + expected: expected_args, + actual: args.len(), + }); + } + + let mut args = args; + args.reverse(); + + $( + #[allow(non_snake_case)] + let $name: $name = serde_json::from_value(args.pop().unwrap()).map_err(|e| { + CallFunctionError::InvalidArgument { + arg_name: "A1".to_string(), + error: e.to_string(), + } + })?; + )* + + + match (f($($name),*).await).into_function_result() { + Ok(value) => Ok(serde_json::to_value(value).unwrap()), + Err(e) => Err(CallFunctionError::Other(e.to_string())), + } + } + } + } + + impl AsyncFunctionType<(Frame, $($name,)*), R> for F + where + F: Fn(Frame, $($name),*) -> Fut + Clone + Send + Sync + 'static, + Fut: Future + Send + 'static, + Ret: IntoFunctionResult + Send, + $($name: DeserializeOwned + Send,)* + { + const NUM_ARGUMENTS: usize = tuple_len::tuple_len!(($($name,)*)); + + fn call(&self, frame: Frame, args: Vec) -> impl Future> + Send + 'static { + let f = self.clone(); + async move { + let expected_args = tuple_len::tuple_len!(($($name,)*)); + + if args.len() != expected_args { + return Err(CallFunctionError::InvalidNumberOfArguments { + expected: expected_args, + actual: args.len(), + }); + } + + let mut args = args; + args.reverse(); + + $( + #[allow(non_snake_case)] + let $name: $name = serde_json::from_value(args.pop().unwrap()).map_err(|e| { + CallFunctionError::InvalidArgument { + arg_name: "A1".to_string(), + error: e.to_string(), + } + })?; + )* + + + match (f(frame, $($name),*).await).into_function_result() { + Ok(value) => Ok(serde_json::to_value(value).unwrap()), + Err(e) => Err(CallFunctionError::Other(e.to_string())), + } + } + } + } + }; +} + +impl_async_function_types!(); +impl_async_function_types!(A1); +impl_async_function_types!(A1, A2); +impl_async_function_types!(A1, A2, A3); +impl_async_function_types!(A1, A2, A3, A4); +impl_async_function_types!(A1, A2, A3, A4, A5); +impl_async_function_types!(A1, A2, A3, A4, A5, A6); +impl_async_function_types!(A1, A2, A3, A4, A5, A6, A7); +impl_async_function_types!(A1, A2, A3, A4, A5, A6, A7, A8); diff --git a/wef/src/func_registry/builder.rs b/wef/src/func_registry/builder.rs new file mode 100644 index 0000000..755b4c0 --- /dev/null +++ b/wef/src/func_registry/builder.rs @@ -0,0 +1,91 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures_util::future::BoxFuture; + +use crate::{ + AsyncFunctionType, FuncRegistry, FunctionType, + func_registry::dyn_wrapper::{DynAsyncFunctionWrapper, DynFunctionType, DynFunctionWrapper}, +}; + +/// A builder for creating a function registry. +#[derive(Default)] +pub struct FuncRegistryBuilder { + functions: HashMap>, +} + +impl FuncRegistryBuilder { + /// Registers a function with the given name. + pub fn register(mut self, name: &str, func: F) -> Self + where + F: FunctionType + Send + Sync + 'static, + S: Send + Sync + 'static, + R: Send + Sync + 'static, + { + self.functions + .insert(name.to_string(), Box::new(DynFunctionWrapper::new(func))); + self + } + + /// Consumes the builder and returns a new [`AsyncFuncRegistryBuilder`]. + pub fn with_spawner(self, spawner: S) -> AsyncFuncRegistryBuilder + where + S: Fn(BoxFuture<'static, ()>) -> R + Send + Sync + 'static, + { + AsyncFuncRegistryBuilder { + functions: self.functions, + spawner: Arc::new(move |fut| { + spawner(fut); + }), + } + } + + /// Builds the [`FuncRegistry`]. + pub fn build(self) -> FuncRegistry { + FuncRegistry { + functions: Arc::new(self.functions), + spawner: None, + } + } +} + +/// A builder for creating an function registry with async functions. +pub struct AsyncFuncRegistryBuilder { + functions: HashMap>, + spawner: Arc) + Send + Sync>, +} + +impl AsyncFuncRegistryBuilder { + /// Registers a function with the given name. + pub fn register(mut self, name: &str, func: F) -> Self + where + F: FunctionType + Send + Sync + 'static, + S: Send + Sync + 'static, + R: Send + Sync + 'static, + { + self.functions + .insert(name.to_string(), Box::new(DynFunctionWrapper::new(func))); + self + } + + /// Registers a async function with the given name. + pub fn register_async(mut self, name: &str, func: F) -> Self + where + F: AsyncFunctionType + Send + Sync + 'static, + S: Send + Sync + 'static, + R: Send + Sync + 'static, + { + self.functions.insert( + name.to_string(), + Box::new(DynAsyncFunctionWrapper::new(func)), + ); + self + } + + /// Builds the [`FuncRegistry`]. + pub fn build(self) -> FuncRegistry { + FuncRegistry { + functions: Arc::new(self.functions), + spawner: Some(self.spawner), + } + } +} diff --git a/wef/src/func_registry/dyn_wrapper.rs b/wef/src/func_registry/dyn_wrapper.rs new file mode 100644 index 0000000..36d1c12 --- /dev/null +++ b/wef/src/func_registry/dyn_wrapper.rs @@ -0,0 +1,96 @@ +use std::marker::PhantomData; + +use futures_util::future::BoxFuture; +use serde_json::Value; + +use crate::{AsyncFunctionType, Frame, FunctionType, query::QueryCallback}; + +pub(crate) trait DynFunctionType: Send + Sync { + fn num_arguments(&self) -> usize; + + fn call( + &self, + spawner: Option<&(dyn Fn(BoxFuture<'static, ()>) + Send + Sync)>, + frame: Frame, + args: Vec, + callback: QueryCallback, + ); +} + +pub(crate) struct DynFunctionWrapper { + func: F, + _mark: PhantomData<(S, R)>, +} + +impl DynFunctionWrapper { + #[inline] + pub(crate) fn new(func: F) -> Self { + Self { + func, + _mark: PhantomData, + } + } +} + +impl DynFunctionType for DynFunctionWrapper +where + F: FunctionType + Send + Sync, + S: Send + Sync, + R: Send + Sync, +{ + #[inline] + fn num_arguments(&self) -> usize { + F::NUM_ARGUMENTS + } + + #[inline] + fn call( + &self, + _spawner: Option<&(dyn Fn(BoxFuture<'static, ()>) + Send + Sync)>, + frame: Frame, + args: Vec, + callback: QueryCallback, + ) { + callback.result(self.func.call(frame, args)) + } +} + +pub(crate) struct DynAsyncFunctionWrapper { + func: F, + _mark: PhantomData<(S, R)>, +} + +impl DynAsyncFunctionWrapper { + #[inline] + pub(crate) fn new(func: F) -> Self { + Self { + func, + _mark: PhantomData, + } + } +} + +impl DynFunctionType for DynAsyncFunctionWrapper +where + F: AsyncFunctionType + Send + Sync, + S: Send + Sync, + R: Send + Sync, +{ + #[inline] + fn num_arguments(&self) -> usize { + F::NUM_ARGUMENTS + } + + #[inline] + fn call( + &self, + spawner: Option<&(dyn Fn(BoxFuture<'static, ()>) + Send + Sync)>, + frame: Frame, + args: Vec, + callback: QueryCallback, + ) { + let spawner = spawner.expect("BUG: spawner is None"); + let fut = self.func.call(frame, args); + spawner(Box::pin(async move { callback.result(fut.await) })); + } +} diff --git a/wef/src/func_registry/error.rs b/wef/src/func_registry/error.rs new file mode 100644 index 0000000..b63f35d --- /dev/null +++ b/wef/src/func_registry/error.rs @@ -0,0 +1,26 @@ +/// Error type for function calls. +#[derive(Debug, thiserror::Error)] +pub enum CallFunctionError { + /// Invalid number of arguments. + #[error("Invalid number of arguments, expected {expected}, got {actual}")] + InvalidNumberOfArguments { + /// Expected number of arguments. + expected: usize, + /// Actual number of arguments. + actual: usize, + }, + /// Invalid argument type. + #[error("Invalid argument {arg_name}: {error}")] + InvalidArgument { + /// Argument name. + arg_name: String, + /// Error message. + error: String, + }, + /// Function not found. + #[error("Function not found: {0}")] + NotFound(String), + /// Other errors. + #[error("{0}")] + Other(String), +} diff --git a/wef/src/func_registry/function_type.rs b/wef/src/func_registry/function_type.rs new file mode 100644 index 0000000..ab9b1b4 --- /dev/null +++ b/wef/src/func_registry/function_type.rs @@ -0,0 +1,110 @@ +use serde::de::DeserializeOwned; +use serde_json::Value; + +use crate::{ + Frame, + func_registry::{CallFunctionError, into_result::IntoFunctionResult}, +}; + +/// Represents a function type that can be called from JavaScript. +pub trait FunctionType { + /// Number of arguments. + const NUM_ARGUMENTS: usize; + + /// Calls the function with the given arguments. + fn call(&self, frame: Frame, args: Vec) -> Result; +} + +macro_rules! impl_function_types { + ($($name:ident),*) => { + impl FunctionType<($($name,)*), R> for F + where + F: Fn($($name),*) -> Ret, + Ret: IntoFunctionResult, + $($name: DeserializeOwned,)* + { + const NUM_ARGUMENTS: usize = tuple_len::tuple_len!(($($name,)*)); + + fn call(&self, _frame: Frame, args: Vec) -> Result { + let expected_args = tuple_len::tuple_len!(($($name,)*)); + + if args.len() != expected_args { + return Err(CallFunctionError::InvalidNumberOfArguments { + expected: expected_args, + actual: args.len(), + }); + } + + let mut args = args; + args.reverse(); + + $( + #[allow(non_snake_case)] + let $name: $name = serde_json::from_value(args.pop().unwrap()).map_err(|e| { + CallFunctionError::InvalidArgument { + arg_name: "A1".to_string(), + error: e.to_string(), + } + })?; + )* + + + let result = IntoFunctionResult::into_function_result(self($($name),*)); + match result { + Ok(value) => Ok(serde_json::to_value(value).unwrap()), + Err(e) => Err(CallFunctionError::Other(e.to_string())), + } + } + } + + impl FunctionType<(Frame, $($name,)*), R> for F + where + F: Fn(Frame, $($name),*) -> Ret, + Ret: IntoFunctionResult, + $($name: DeserializeOwned,)* + { + const NUM_ARGUMENTS: usize = tuple_len::tuple_len!(($($name,)*)); + + fn call(&self, frame: Frame, args: Vec) -> Result { + let expected_args = tuple_len::tuple_len!(($($name,)*)); + + if args.len() != expected_args { + return Err(CallFunctionError::InvalidNumberOfArguments { + expected: expected_args, + actual: args.len(), + }); + } + + let mut args = args; + args.reverse(); + + $( + #[allow(non_snake_case)] + let $name: $name = serde_json::from_value(args.pop().unwrap()).map_err(|e| { + CallFunctionError::InvalidArgument { + arg_name: "A1".to_string(), + error: e.to_string(), + } + })?; + )* + + + let result = IntoFunctionResult::into_function_result(self(frame, $($name),*)); + match result { + Ok(value) => Ok(serde_json::to_value(value).unwrap()), + Err(e) => Err(CallFunctionError::Other(e.to_string())), + } + } + } + }; +} + +impl_function_types!(); +impl_function_types!(A1); +impl_function_types!(A1, A2); +impl_function_types!(A1, A2, A3); +impl_function_types!(A1, A2, A3, A4); +impl_function_types!(A1, A2, A3, A4, A5); +impl_function_types!(A1, A2, A3, A4, A5, A6); +impl_function_types!(A1, A2, A3, A4, A5, A6, A7); +impl_function_types!(A1, A2, A3, A4, A5, A6, A7, A8); diff --git a/wef/src/func_registry/inject.js b/wef/src/func_registry/inject.js new file mode 100644 index 0000000..bd201f5 --- /dev/null +++ b/wef/src/func_registry/inject.js @@ -0,0 +1,35 @@ +window.jsBridge = { + __internal: { + call(method, args) { + var request = { + method: method, + args, + }; + return new Promise((resolve, reject) => { + window.cefQuery({ + request: JSON.stringify(request), + persistent: false, + onSuccess: function (response) { + resolve(JSON.parse(response)); + }, + onFailure: (error_code, error_message) => reject(error_message), + }); + }); + }, + nextEventListenerId: 0, + eventListeners: {}, + emit(message) { + for (const id in this.eventListeners) { + this.eventListeners[id](message); + } + }, + }, + addEventListener(callback) { + const id = this.__internal.nextEventListenerId++; + this.__internal.eventListeners[id] = callback; + return id; + }, + removeEventListener(id) { + delete this.__internal.eventListeners[id]; + }, +}; diff --git a/wef/src/func_registry/into_result.rs b/wef/src/func_registry/into_result.rs new file mode 100644 index 0000000..905c432 --- /dev/null +++ b/wef/src/func_registry/into_result.rs @@ -0,0 +1,30 @@ +use std::error::Error; + +use serde::Serialize; +use serde_json::Value; + +use crate::func_registry::CallFunctionError; + +pub(crate) trait IntoFunctionResult { + fn into_function_result(self) -> Result; +} + +impl IntoFunctionResult for Result +where + T: Serialize, + E: Error, +{ + fn into_function_result(self) -> Result { + self.map(|value| serde_json::to_value(value).unwrap()) + .map_err(|err| CallFunctionError::Other(err.to_string())) + } +} + +impl IntoFunctionResult for T +where + T: Serialize, +{ + fn into_function_result(self) -> Result { + Ok(serde_json::to_value(self).unwrap()) + } +} diff --git a/wef/src/func_registry/mod.rs b/wef/src/func_registry/mod.rs new file mode 100644 index 0000000..b226c03 --- /dev/null +++ b/wef/src/func_registry/mod.rs @@ -0,0 +1,13 @@ +mod async_function_type; +mod builder; +mod dyn_wrapper; +mod error; +mod function_type; +mod into_result; +mod registry; + +pub use async_function_type::AsyncFunctionType; +pub use builder::{AsyncFuncRegistryBuilder, FuncRegistryBuilder}; +pub use error::CallFunctionError; +pub use function_type::FunctionType; +pub use registry::FuncRegistry; diff --git a/wef/src/func_registry/registry.rs b/wef/src/func_registry/registry.rs new file mode 100644 index 0000000..791da1c --- /dev/null +++ b/wef/src/func_registry/registry.rs @@ -0,0 +1,105 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures_util::future::BoxFuture; +use serde_json::Value; + +use crate::{ + Frame, FuncRegistryBuilder, + func_registry::{CallFunctionError, dyn_wrapper::DynFunctionType}, + query::QueryCallback, +}; + +/// A registry for functions that can be called from JavaScript. +/// +/// To create a new `FuncRegistry`, use the [`FuncRegistry::builder`] method to +/// create a `FuncRegistryBuilder`, register your functions, and then call +/// [`FuncRegistryBuilder::build`] to create the `FuncRegistry`. +/// +/// ```rust, no_run +/// use wef::{Browser, FuncRegistry}; +/// +/// let registry = FuncRegistry::builder() +/// .register("add", |a: i32, b: i32| a + b) +/// .register("sub", |a: i32, b: i32| a - b) +/// .build(); +/// +/// let browser = Browser::builder() +/// .func_registry(registry) // Register the functions with the browser +/// .build(); +/// ``` +/// +/// The functions can be synchronous or asynchronous, and they can accept any +/// number of arguments. The arguments must implement the `serde::Serialize` and +/// `serde::Deserialize` traits. +/// +/// Call the functions from JavaScript: +/// +/// ```javascript +/// jsBridge.add(1, 2); // Returns 3 +/// jsBridge.sub(5, 3); // Returns 2 +/// ``` +/// +/// # Asynchronous Functions +/// +/// You can also register asynchronous functions. Call +/// `FuncRegistryBuilder::with_spawner` to create an `AsyncFuncRegistryBuilder` +/// that allows you to register async functions. +/// +/// ```rust, ignore +/// use wef::{Browser, FuncRegistry}; +/// +/// let registry = FuncRegistry::builder() +/// .with_spawner(tokio::spawn) +/// .register_async("sleep", |millis: u64| async move { +/// tokio::time::sleep(std::time::Duration::from_millis(millis)).await; +/// "done" +/// }) +/// .build(); +/// +/// let browser = Browser::builder() +/// .func_registry(registry) // Register the functions with the browser +/// .build(); +/// ``` +/// +/// _You can clone the `FuncRegistry` and use it in multiple browsers._ +#[derive(Default, Clone)] +pub struct FuncRegistry { + pub(crate) functions: Arc>>, + pub(crate) spawner: Option) + Send + Sync>>, +} + +impl FuncRegistry { + /// Creates a new `FuncRegistryBuilder`. + #[inline] + pub fn builder() -> FuncRegistryBuilder { + FuncRegistryBuilder::default() + } + + pub(crate) fn call(&self, frame: Frame, name: &str, args: Vec, callback: QueryCallback) { + let Some(func) = self.functions.get(name) else { + callback.result(Err(CallFunctionError::NotFound(name.to_string()))); + return; + }; + func.call(self.spawner.as_deref(), frame, args, callback) + } + + pub(crate) fn javascript(&self) -> String { + let mut code = include_str!("inject.js").to_string(); + + for (name, func) in &*self.functions { + let args = (0..func.num_arguments()) + .map(|i| format!("arg{}", i)) + .collect::>() + .join(","); + code += &format!( + r#"window.jsBridge.{name} = function({args}) {{ + return window.jsBridge.__internal.call("{name}", [{args}]); + }};"#, + name = name, + args = args + ); + } + + code + } +} diff --git a/wef/src/geom.rs b/wef/src/geom.rs new file mode 100644 index 0000000..189cef6 --- /dev/null +++ b/wef/src/geom.rs @@ -0,0 +1,117 @@ +/// A rectangle structure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(C)] +pub struct Rect { + /// X coordinate of the rectangle. + pub x: T, + /// Y coordinate of the rectangle. + pub y: T, + /// Width of the rectangle. + pub width: T, + /// Height of the rectangle. + pub height: T, +} + +impl Rect { + /// Creates a new rectangle with the specified coordinates and size. + #[inline] + pub fn new(x: T, y: T, width: T, height: T) -> Self { + Rect { + x, + y, + width, + height, + } + } + + /// Returns the origin point of the rectangle. + #[inline] + pub fn origin(&self) -> Point + where + T: Copy, + { + Point::new(self.x, self.y) + } + + /// Returns the size of the rectangle. + pub fn size(&self) -> Size + where + T: Copy, + { + Size::new(self.width, self.height) + } + + /// Maps the rectangle to a new type using the provided function. + pub fn map(&self, f: F) -> Rect + where + T: Copy, + F: Fn(T) -> U, + { + Rect { + x: f(self.x), + y: f(self.y), + width: f(self.width), + height: f(self.height), + } + } +} + +/// A point structure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(C)] +pub struct Point { + /// X coordinate of the point. + pub x: T, + /// Y coordinate of the point. + pub y: T, +} + +impl Point { + /// Creates a new point with the specified coordinates. + #[inline] + pub fn new(x: T, y: T) -> Self { + Point { x, y } + } + + /// Maps the point to a new type using the provided function. + pub fn map(&self, f: F) -> Point + where + T: Copy, + F: Fn(T) -> U, + { + Point { + x: f(self.x), + y: f(self.y), + } + } +} + +/// A size structure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(C)] +pub struct Size { + /// Width of the size. + pub width: T, + /// Height of the size. + pub height: T, +} + +impl Size { + /// Creates a new size with the specified width and height. + #[inline] + pub fn new(width: T, height: T) -> Self { + Size { width, height } + } + + /// Maps the size to a new type using the provided function. + pub fn map(&self, f: F) -> Size + where + T: Copy, + F: Fn(T) -> U, + { + Size { + width: f(self.width), + height: f(self.height), + } + } +} diff --git a/wef/src/input.rs b/wef/src/input.rs new file mode 100644 index 0000000..499ab2c --- /dev/null +++ b/wef/src/input.rs @@ -0,0 +1,66 @@ +/// Mouse button +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub enum MouseButton { + /// Left + Left = 0, + /// Middle + Middle = 1, + /// Right + Right = 2, +} + +/// Key codes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(i32)] +pub enum KeyCode { + /// Backspace + Backspace = 0x08, + /// Delete + Delete = 0x2E, + /// Tab + Tab = 0x09, + /// Enter + Enter = 0x0D, + /// PageUp + PageUp = 0x21, + /// PageDown + PageDown = 0x22, + /// End + End = 0x23, + /// Home + Home = 0x24, + /// Arrow left + ArrowLeft = 0x25, + /// Arrow up + ArrowUp = 0x26, + /// Arrow right + ArrowRight = 0x27, + /// Arrow down + ArrowDown = 0x28, +} + +impl KeyCode { + pub(crate) fn as_char(&self) -> Option { + use KeyCode::*; + + match self { + Enter => Some(0x0D), + _ => None, + } + } +} + +bitflags::bitflags! { + /// Key modifiers for keyboard events. + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] + pub struct KeyModifier: i32 { + /// Shift key + const SHIFT = 0x1; + /// Control key + const CONTROL = 0x2; + /// Alt key + const ALT = 0x4; + } +} diff --git a/wef/src/js_dialog.rs b/wef/src/js_dialog.rs new file mode 100644 index 0000000..c12e8a1 --- /dev/null +++ b/wef/src/js_dialog.rs @@ -0,0 +1,43 @@ +use std::ffi::CString; + +use crate::ffi::*; + +/// Supported JS dialog types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum JsDialogType<'a> { + Alert, + Confirm, + Prompt { default_prompt_text: &'a str }, +} + +/// Js dialog callback. +pub struct JsDialogCallback(*mut wef_js_dialog_callback_t); + +unsafe impl Send for JsDialogCallback {} +unsafe impl Sync for JsDialogCallback {} + +impl Drop for JsDialogCallback { + fn drop(&mut self) { + unsafe { wef_js_dialog_callback_destroy(self.0) } + } +} + +impl JsDialogCallback { + pub(crate) fn new(callback: *mut wef_js_dialog_callback_t) -> Self { + JsDialogCallback(callback) + } + + /// Continue the JS dialog request. + /// + /// Set `success` to true if the OK button was pressed. + /// The `user_input` value should be specified for prompt dialogs. + pub fn continue_(&self, success: bool, user_input: Option<&str>) { + unsafe { + let user_input_cstr = user_input + .and_then(|user_input| CString::new(user_input).ok()) + .unwrap_or_default(); + wef_js_dialog_callback_continue(self.0, success, user_input_cstr.as_ptr()); + } + } +} diff --git a/wef/src/lib.rs b/wef/src/lib.rs new file mode 100644 index 0000000..0cfdff9 --- /dev/null +++ b/wef/src/lib.rs @@ -0,0 +1,54 @@ +#![doc = include_str!("../README.md")] + +mod app_handler; +mod browser; +mod browser_handler; +mod builder; +mod context_menu; +mod cursor; +mod dirty_rects; +mod dpi; +mod error; +mod ffi; +mod file_dialog; +mod frame; +#[cfg(target_os = "macos")] +mod framework_loader; +mod func_registry; +mod geom; +mod input; +mod js_dialog; +mod query; +#[cfg(target_os = "macos")] +mod sandbox_context; +mod settings; +mod wef; + +pub use app_handler::ApplicationHandler; +pub use browser::Browser; +pub use browser_handler::{BrowserHandler, ImageBuffer, LogSeverity, PaintElementType}; +pub use builder::BrowserBuilder; +pub use context_menu::{ + ContextMenuEditStateFlags, ContextMenuMediaStateFlags, ContextMenuMediaType, ContextMenuParams, + ContextMenuTypeFlags, +}; +pub use cursor::{CursorInfo, CursorType}; +pub use dirty_rects::{DirtyRects, DirtyRectsIter}; +pub use dpi::{LogicalUnit, PhysicalUnit}; +pub use error::Error; +pub use file_dialog::{Accept, FileDialogCallback, FileDialogMode}; +pub use frame::Frame; +#[cfg(target_os = "macos")] +pub use framework_loader::FrameworkLoader; +pub use func_registry::{ + AsyncFuncRegistryBuilder, AsyncFunctionType, CallFunctionError, FuncRegistry, + FuncRegistryBuilder, FunctionType, +}; +pub use geom::{Point, Rect, Size}; +pub use input::{KeyCode, KeyModifier, MouseButton}; +pub use js_dialog::{JsDialogCallback, JsDialogType}; +#[cfg(target_os = "macos")] +pub use sandbox_context::SandboxContext; +pub use serde_json::Value; +pub use settings::Settings; +pub use wef::*; diff --git a/wef/src/query.rs b/wef/src/query.rs new file mode 100644 index 0000000..5ee90ba --- /dev/null +++ b/wef/src/query.rs @@ -0,0 +1,39 @@ +use std::ffi::CString; + +use serde_json::Value; + +use crate::{ffi::*, func_registry::CallFunctionError}; + +pub(crate) struct QueryCallback(*mut wef_query_callback_t); + +unsafe impl Send for QueryCallback {} +unsafe impl Sync for QueryCallback {} + +impl Drop for QueryCallback { + fn drop(&mut self) { + unsafe { wef_query_callback_destroy(self.0) }; + } +} + +impl QueryCallback { + pub(crate) fn new(callback: *mut wef_query_callback_t) -> Self { + Self(callback) + } + + pub(crate) fn result(self, res: Result) { + match res { + Ok(resp) => self.success(resp), + Err(err) => self.failure(err), + } + } + + fn success(self, resp: Value) { + let resp = CString::new(serde_json::to_string(&resp).unwrap_or_default()).unwrap(); + unsafe { wef_query_callback_success(self.0, resp.as_ptr()) } + } + + fn failure(self, err: CallFunctionError) { + let err = CString::new(err.to_string()).unwrap(); + unsafe { wef_query_callback_failure(self.0, err.as_ptr()) } + } +} diff --git a/wef/src/sandbox_context.rs b/wef/src/sandbox_context.rs new file mode 100644 index 0000000..419fe21 --- /dev/null +++ b/wef/src/sandbox_context.rs @@ -0,0 +1,37 @@ +use std::{ + ffi::{CString, c_char}, + os::raw::c_void, +}; + +use crate::{Error, ffi::*}; + +/// The sandbox is used to restrict sub-processes (renderer, GPU, etc) from +/// directly accessing system resources. +/// +/// This helps to protect the user from untrusted and potentially malicious Web +/// content. +#[derive(Debug)] +pub struct SandboxContext(*mut c_void); + +impl SandboxContext { + pub fn new() -> Result { + unsafe { + let args: Vec = std::env::args() + .filter_map(|arg| CString::new(arg).ok()) + .collect(); + let c_args: Vec<*const c_char> = args.iter().map(|arg| arg.as_ptr()).collect(); + + let context = wef_sandbox_context_create(c_args.as_ptr(), args.len() as i32); + if context.is_null() { + return Err(Error::SandboxContextCreate); + } + Ok(SandboxContext(context)) + } + } +} + +impl Drop for SandboxContext { + fn drop(&mut self) { + unsafe { wef_sandbox_context_destroy(self.0) }; + } +} diff --git a/wef/src/settings.rs b/wef/src/settings.rs new file mode 100644 index 0000000..afc5bd3 --- /dev/null +++ b/wef/src/settings.rs @@ -0,0 +1,117 @@ +use std::ffi::CString; + +use crate::ApplicationHandler; + +/// Application settings. +#[derive(Debug)] +pub struct Settings { + pub(crate) locale: Option, + pub(crate) cache_path: Option, + pub(crate) root_cache_path: Option, + pub(crate) browser_subprocess_path: Option, + pub(crate) external_message_pump: bool, + pub(crate) handler: T, +} + +impl Settings<()> { + /// Creates a new [`Settings`] instance with default values. + #[inline] + pub fn new() -> Self { + Self { + locale: None, + cache_path: None, + root_cache_path: None, + browser_subprocess_path: None, + external_message_pump: false, + handler: (), + } + } +} + +impl Settings { + /// The locale string that will be passed to CEF. + /// + /// If `None` the default locale of "en-US" will be used. + pub fn locale(mut self, locale: impl Into>) -> Self { + self.locale = Some(CString::new(locale).expect("invalid locale string")); + self + } + + /// The directory where data for the global browser cache will be stored on + /// disk. + /// + /// If this value is non-empty then it must be an absolute path that is + /// either equal to or a child directory of `root_cache_path`. If + /// this value is empty then browsers will be created in "incognito mode" + /// where in-memory caches are used for storage and no profile-specific data + /// is persisted to disk (installation-specific data will still be persisted + /// in root_cache_path). HTML5 databases such as localStorage will only + /// persist across sessions if a cache path is specified. + pub fn cache_path(mut self, path: impl Into>) -> Self { + self.cache_path = Some(CString::new(path).expect("invalid cache path")); + self + } + + /// The root directory for installation-specific data and the parent + /// directory for profile-specific data. + /// + /// If this value is `None` then the default platform-specific directory + /// will be used ("~/.config/cef_user_data" directory on Linux, + /// "~/Library/Application Support/CEF/User Data" directory on MacOS, + /// "AppData\Local\CEF\User Data" directory under the user profile + /// directory on Windows). Use of the default directory is not + /// recommended in production applications (see below). + /// + /// NOTE: Multiple application instances writing to the same root_cache_path + /// directory could result in data corruption. + pub fn root_cache_path(mut self, path: impl Into>) -> Self { + self.root_cache_path = Some(CString::new(path).expect("invalid root cache path")); + self + } + + /// The path to a separate executable that will be launched for + /// sub-processes. + /// + /// If this value is not set on Windows or Linux then the + /// main process executable will be used. If this value is not set on + /// macOS then a helper executable must exist at + /// `Contents/Frameworks/ Helper.app/Contents/MacOS/ Helper` + /// in the top-level app bundle. See the comments on CefExecuteProcess() + /// for details. + /// + /// If this value is set then it must be an absolute path. + pub fn browser_subprocess_path(mut self, path: impl Into>) -> Self { + self.browser_subprocess_path = + Some(CString::new(path).expect("invalid browser subprocess path")); + self + } + + /// Enable to control browser process main (UI) thread message pump + /// scheduling via the + /// [`crate::ApplicationHandler::on_schedule_message_pump_work`] + /// callback. + /// + /// This option is recommended for use in combination with the + /// [`crate::do_message_work`] function in cases where the CEF message loop + /// must be integrated into an existing application message. + pub fn external_message_pump(mut self, enable: bool) -> Self { + self.external_message_pump = enable; + self + } + + /// Sets the event handler. + #[inline] + pub fn handler(self, handler: Q) -> Settings + where + Q: ApplicationHandler, + { + Settings { + locale: self.locale, + cache_path: self.cache_path, + root_cache_path: self.root_cache_path, + browser_subprocess_path: self.browser_subprocess_path, + external_message_pump: self.external_message_pump, + handler, + } + } +} diff --git a/wef/src/wef.rs b/wef/src/wef.rs new file mode 100644 index 0000000..a6d29db --- /dev/null +++ b/wef/src/wef.rs @@ -0,0 +1,146 @@ +use std::ffi::{CString, c_char, c_void}; + +use crate::{ApplicationHandler, Error, app_handler::ApplicationState, ffi::*, settings::Settings}; + +/// Initialize the CEF browser process. +/// +/// This function should be called on the main application thread to +/// initialize the CEF browser process. +pub fn init(settings: Settings) -> Result<(), Error> +where + T: ApplicationHandler, +{ + unsafe { + let callbacks = CAppCallbacks { + on_schedule_message_pump_work: crate::app_handler::on_schedule_message_pump_work::, + }; + + let handler = Box::into_raw(Box::new(ApplicationState { + handler: settings.handler, + })); + + extern "C" fn destroy_handler(user_data: *mut c_void) { + unsafe { _ = Box::from_raw(user_data as *mut T) } + } + + let c_settings = CSettings { + locale: to_cstr_ptr_opt(settings.locale.as_deref()), + cache_path: to_cstr_ptr_opt(settings.cache_path.as_deref()), + root_cache_path: to_cstr_ptr_opt(settings.root_cache_path.as_deref()), + browser_subprocess_path: to_cstr_ptr_opt(settings.browser_subprocess_path.as_deref()), + callbacks, + userdata: handler as *mut c_void, + destroy_userdata: destroy_handler::, + }; + + if !wef_init(&c_settings) { + return Err(Error::InitializeBrowserProcess); + } + } + + Ok(()) +} + +/// Executes the CEF subprocess. +/// +/// This function should be called from the application entry point function +/// to execute a secondary process. It can be used to run secondary +/// processes from the browser client executable. +/// +/// If called for the browser process (identified by no "type" command-line +/// value) it will return immediately with a value of `false`. +/// +/// If called for a recognized secondary process it will block until the +/// process should exit and then return `true`. +/// +/// # Examples +/// +/// ```rust, no_run +/// use wef::Settings; +/// +/// fn main() -> Result<(), Box> { +/// if wef::exec_process()? { +/// return Ok(()); +/// } +/// +/// wef::init(Settings::new()); +/// // ... event loop +/// wef::shutdown(); +/// Ok(()) +/// } +/// ``` +pub fn exec_process() -> Result { + let args: Vec = std::env::args() + .filter_map(|arg| CString::new(arg).ok()) + .collect(); + let c_args: Vec<*const c_char> = args.iter().map(|arg| arg.as_ptr()).collect(); + Ok(unsafe { wef_exec_process(c_args.as_ptr(), args.len() as i32) }) +} + +/// Perform a single iteration of CEF message loop processing. +/// +/// This function is provided for cases where the CEF message loop must be +/// integrated into an existing application message loop. +pub fn do_message_work() { + unsafe { wef_do_message_work() }; +} + +/// Shuts down the CEF library. +/// +/// # Panics +/// +/// This function **MUST NOT** be called while any `CefBrowser` instances are +/// still alive. If there are any `CefBrowser` objects that have not been +/// dropped properly at the time of calling this function, it will likely lead +/// to a crash or undefined behavior. +pub fn shutdown() { + unsafe { wef_shutdown() }; +} + +/// Launch the Wef application. +/// +/// This function initializes the CEF library and runs the main process. +/// It is a convenience function that combines the [`init`] and [`shutdown`] +/// functions. +/// +/// On macOS, it also loads the CEF framework using the +/// `crate::FrameworkLoader`. +/// +/// # Panics +/// +/// This function panics if the CEF library fails to initialize or if the +/// CEF framework fails to load on macOS. +/// +/// # Examples +/// +/// ```rust, no_run +/// use wef::Settings; +/// +/// fn main() { +/// let settings = Settings::new(); +/// wef::launch(settings, || { +/// // do something in the main process +/// }); +/// } +/// ``` +pub fn launch(settings: Settings, f: F) -> R +where + T: ApplicationHandler, + F: FnOnce() -> R, +{ + if cfg!(not(target_os = "macos")) { + if exec_process().expect("failed to execute process") { + // Is helper process, exit immediately + std::process::exit(0); + } + } + + #[cfg(target_os = "macos")] + let _ = crate::FrameworkLoader::load_in_main().expect("failed to load CEF framework"); + + // Run the main process + init(settings).expect("failed to initialize CEF"); + let res = f(); + shutdown(); + res +} From 65e0ecd1a33a93525dcec52ebed1a2a9c1df4bb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:49:46 +0000 Subject: [PATCH 03/16] Complete wef extraction with CI config, documentation, and final polish Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- .github/dependabot.yml | 7 ++ .github/workflows/ci.yml | 87 ++++++++++++++++++ CONTRIBUTING.md | 188 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +- README.md | 104 +++++++++++++++++++++- 5 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fcf104b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e423ecb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI +on: + pull_request: + push: + branches: + - "*" + tags: + - "*" +env: + CEF_VERSION: "136.1.6+g1ac1b14+chromium-136.0.7103.114" + +jobs: + test: + name: Test + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + run_on: macos-latest + - target: x86_64-linux-gnu + run_on: ubuntu-latest + - target: x86_64-windows-msvc + run_on: windows-latest + runs-on: ${{ matrix.run_on }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy + - name: Install system dependencies + if: ${{ matrix.run_on != 'windows-latest' }} + run: | + if [ "${{ matrix.run_on }}" = "ubuntu-latest" ]; then + sudo apt-get update + sudo apt-get install -y libglib2.0-dev pkg-config + elif [ "${{ matrix.run_on }}" = "macos-latest" ]; then + # macOS dependencies if needed + echo "macOS setup complete" + fi + - name: Machete + if: ${{ matrix.run_on == 'macos-latest' }} + uses: bnjbvr/cargo-machete@v0.9.1 + - name: Setup | Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: test-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} + - name: Cache CEF + id: cache-cef + uses: actions/cache@v4 + with: + path: $HOME/.cef + key: cef-${{ env.CEF_VERSION }}-${{ matrix.target }} + - name: Init CEF + if: ${{ !steps.cache-cef.outputs.cache-hit }} + run: | + cargo run -p cargo-wef -- wef init + - name: Typo check + if: ${{ matrix.run_on == 'macos-latest' }} + run: | + cargo install typos-cli || echo "typos-cli already installed" + typos + - name: Lint + if: ${{ matrix.run_on == 'macos-latest' }} + run: | + cargo clippy -- --deny warnings + - name: Test Linux + if: ${{ matrix.run_on == 'ubuntu-latest' }} + run: | + cargo run -p cargo-wef -- wef add-framework target/debug + cargo test --all + - name: Test Windows + if: ${{ matrix.run_on == 'windows-latest' }} + run: | + cargo run -p cargo-wef -- wef add-framework target/debug + cargo test --all + - name: Test macOS + if: ${{ matrix.run_on == 'macos-latest' }} + run: | + cargo test --all \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..84a3bf1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,188 @@ +# Contributing to Wef + +Thank you for your interest in contributing to Wef! This document provides guidelines and information for contributors. + +## Prerequisites + +Before you can build and test Wef, you'll need: + +### System Dependencies + +#### Linux (Ubuntu/Debian) +```bash +sudo apt-get update +sudo apt-get install -y libglib2.0-dev pkg-config build-essential +``` + +#### Linux (CentOS/RHEL/Fedora) +```bash +sudo yum install -y glib2-devel pkgconfig gcc-c++ +# or for newer versions: +sudo dnf install -y glib2-devel pkgconfig gcc-c++ +``` + +#### macOS +```bash +# Install Xcode Command Line Tools +xcode-select --install +``` + +#### Windows +- Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + +### Rust Toolchain + +Install Rust using [rustup](https://rustup.rs/): +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +## Development Setup + +1. **Clone the repository** + ```bash + git clone https://github.com/longbridge/wef.git + cd wef + ``` + +2. **Install cargo-wef** + ```bash + cargo install --path cargo-wef + ``` + +3. **Initialize CEF** + ```bash + cargo wef init + ``` + This downloads the Chromium Embedded Framework binaries to `~/.cef` + +4. **Build the project** + ```bash + cargo wef build + ``` + +5. **Run tests** + ```bash + cargo test --all + ``` + +## Project Structure + +- **`wef/`** - Core library implementing CEF3 bindings +- **`cargo-wef/`** - Command-line tool for building wef applications +- **`examples/`** - Example applications demonstrating usage + +## Building and Testing + +### Building +```bash +# Build everything +cargo wef build + +# Build specific components +cargo build -p wef # Core library +cargo build -p cargo-wef # CLI tool +``` + +### Testing +```bash +# Run all tests +cargo test --all + +# Test specific components +cargo test -p wef +cargo test -p cargo-wef +``` + +### Running Examples +```bash +# Run the winit example +cargo wef run --example wef-winit +``` + +## Code Style + +We use the standard Rust formatting tools: + +```bash +# Format code +cargo fmt --all + +# Check formatting +cargo fmt --all -- --check + +# Run clippy +cargo clippy --all -- -D warnings +``` + +## Submitting Changes + +1. **Fork the repository** on GitHub +2. **Create a feature branch** from `main` +3. **Make your changes** following the coding standards +4. **Add tests** for new functionality +5. **Ensure all tests pass** with `cargo test --all` +6. **Format your code** with `cargo fmt --all` +7. **Run clippy** with `cargo clippy --all -- -D warnings` +8. **Commit your changes** with clear, descriptive commit messages +9. **Push to your fork** and create a Pull Request + +## Commit Messages + +Use clear, descriptive commit messages following conventional commits: + +- `feat: add new functionality` +- `fix: resolve bug in component` +- `docs: update README` +- `refactor: improve code structure` +- `test: add test coverage` + +## Pull Request Guidelines + +- **Describe your changes** clearly in the PR description +- **Reference related issues** using keywords like "Fixes #123" +- **Keep changes focused** - one feature or fix per PR +- **Update documentation** if you're changing public APIs +- **Add tests** for new functionality +- **Ensure CI passes** before requesting review + +## Issues and Feature Requests + +- Check existing issues before creating new ones +- Use clear, descriptive titles +- Provide reproduction steps for bugs +- Include system information (OS, Rust version, etc.) + +## Development Tips + +### CEF Troubleshooting + +If you encounter CEF-related build issues: + +1. **Clear CEF cache** + ```bash + rm -rf ~/.cef + cargo wef init + ``` + +2. **Check CEF version** + The CEF version is defined in `.github/workflows/ci.yml` + +3. **Verify system dependencies** + Ensure all required system packages are installed + +### Debugging + +- Use `cargo wef run --example wef-winit` to test changes quickly +- Enable debug logging with `RUST_LOG=debug` +- Use the debugger-friendly debug profile for development + +## Getting Help + +- **Documentation**: Check the [README](README.md) and [library docs](wef/README.md) +- **Issues**: Search existing [GitHub issues](https://github.com/longbridge/wef/issues) +- **Discussions**: Use [GitHub Discussions](https://github.com/longbridge/wef/discussions) for questions + +## License + +By contributing to Wef, you agree that your contributions will be licensed under the Apache License 2.0. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9d95054..2a177c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,10 +52,10 @@ debug = "limited" split-debuginfo = "unpacked" [profile.dev.package] -resvg = { opt-level = 3 } -rustybuzz = { opt-level = 3 } -taffy = { opt-level = 3 } -ttf-parser = { opt-level = 3 } +# Optimize image processing libraries in debug builds for better performance +image = { opt-level = 3 } +png = { opt-level = 3 } +reqwest = { opt-level = 3 } [workspace.metadata.typos] files.extend-exclude = ["**/fixtures/*"] \ No newline at end of file diff --git a/README.md b/README.md index 9bfd3e2..43ff638 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# wef \ No newline at end of file +# Wef - Web Embedding Framework + +![CI](https://github.com/longbridge/wef/workflows/CI/badge.svg) +[![Crates.io](https://img.shields.io/crates/v/wef.svg)](https://crates.io/crates/wef) +[![Documentation](https://docs.rs/wef/badge.svg)](https://docs.rs/wef) + +**Wef** (Web Embedding Framework) is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support. + +> The `Wef` name is an abbreviation of "Web Embedding Framework", and it's also inspired by Wry. + +![Wef Example](https://github.com/user-attachments/assets/f677ecb4-dbff-4e0d-86b9-203f6e1004a4) + +## Features + +- **Cross-Platform**: Support for Windows, macOS, and Linux +- **CEF3 Integration**: Built on top of Chromium Embedded Framework for reliable web rendering +- **Offscreen Rendering**: Advanced rendering capabilities with offscreen support +- **JavaScript Bridge**: Seamless communication between Rust and JavaScript +- **Multi-Process Architecture**: Leverages CEF's multi-process design for stability +- **Cargo Integration**: Complete toolchain with `cargo-wef` for easy development + +## Quick Start + +### Installation + +Add `wef` to your `Cargo.toml`: + +```toml +[dependencies] +wef = "0.7.0" +``` + +### Install cargo-wef + +```bash +cargo install cargo-wef +``` + +### Initialize CEF + +```bash +cargo wef init +``` + +### Basic Usage + +```rust +use wef::Settings; + +fn main() { + let settings = Settings::new(); + wef::launch(settings, || { + // Your application logic here + }); +} +``` + +## Documentation + +For comprehensive documentation, examples, and API reference, see: + +- **[Library Documentation](wef/README.md)** - Complete guide to using the wef library +- **[API Reference](https://docs.rs/wef)** - Detailed API documentation +- **[Examples](examples/)** - Working examples including winit integration + +## Project Structure + +This repository contains: + +- **[`wef/`](wef/)** - The core wef library +- **[`cargo-wef/`](cargo-wef/)** - Command-line tool for building and managing wef applications +- **[`examples/`](examples/)** - Example applications demonstrating wef usage + +## Development + +### Building + +```bash +# Build the library +cargo wef build + +# Run tests +cargo test --all + +# Run an example +cargo wef run --example wef-winit +``` + +### Requirements + +- CEF binary distribution (automatically downloaded by `cargo-wef`) +- Platform-specific dependencies: + - **Linux**: `libglib2.0-dev`, `pkg-config` + - **Windows**: Visual Studio Build Tools + - **macOS**: Xcode Command Line Tools + +## Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +## License + +Licensed under the Apache License, Version 2.0. See [LICENSE-APACHE](wef/LICENSE-APACHE) for details. \ No newline at end of file From b344d4e95308735279ac6b95ee34d054cab784f6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 10:54:54 +0800 Subject: [PATCH 04/16] Simplify test job condition in CI workflow Remove condition for running tests on pull requests. --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e423ecb..db3e53d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ env: jobs: test: name: Test - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: fail-fast: false matrix: @@ -84,4 +83,4 @@ jobs: - name: Test macOS if: ${{ matrix.run_on == 'macos-latest' }} run: | - cargo test --all \ No newline at end of file + cargo test --all From 7a505925f4b295199ad481c9d6611208db0704af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:01:06 +0000 Subject: [PATCH 05/16] Restructure repository: move wef library to root, cargo-wef to tool/ Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +- wef/.rustfmt.toml => .rustfmt.toml | 0 Cargo.toml | 37 +- wef/LICENSE-APACHE => LICENSE-APACHE | 0 README.md | 12 +- wef/build.rs => build.rs | 0 {wef/cpp => cpp}/app.h | 0 {wef/cpp => cpp}/app_callbacks.h | 0 {wef/cpp => cpp}/app_render_process.h | 0 {wef/cpp => cpp}/browser_callbacks.h | 0 {wef/cpp => cpp}/client.cpp | 0 {wef/cpp => cpp}/client.h | 0 {wef/cpp => cpp}/cursor.cpp | 0 {wef/cpp => cpp}/dirty_rect.cpp | 0 {wef/cpp => cpp}/external_pump.cpp | 0 {wef/cpp => cpp}/external_pump.h | 0 {wef/cpp => cpp}/external_pump_linux.cpp | 0 {wef/cpp => cpp}/external_pump_mac.mm | 0 {wef/cpp => cpp}/external_pump_win.cpp | 0 {wef/cpp => cpp}/file_dialog.cpp | 0 {wef/cpp => cpp}/frame.cpp | 0 {wef/cpp => cpp}/frame.h | 0 {wef/cpp => cpp}/js_dialog.cpp | 0 {wef/cpp => cpp}/load_library.cpp | 0 {wef/cpp => cpp}/query.cpp | 0 {wef/cpp => cpp}/sandbox_context.cpp | 0 {wef/cpp => cpp}/utils.h | 0 {wef/cpp => cpp}/wef.cpp | 0 examples/wef-winit/Cargo.toml | 2 +- {wef/src => src}/app_handler.rs | 0 {wef/src => src}/browser.rs | 0 {wef/src => src}/browser_handler.rs | 0 {wef/src => src}/builder.rs | 0 {wef/src => src}/context_menu.rs | 0 {wef/src => src}/cursor.rs | 0 {wef/src => src}/dirty_rects.rs | 0 {wef/src => src}/dpi.rs | 0 {wef/src => src}/error.rs | 0 {wef/src => src}/ffi.rs | 0 {wef/src => src}/file_dialog.rs | 0 {wef/src => src}/frame.rs | 0 {wef/src => src}/framework_loader.rs | 0 .../func_registry/async_function_type.rs | 0 {wef/src => src}/func_registry/builder.rs | 0 {wef/src => src}/func_registry/dyn_wrapper.rs | 0 {wef/src => src}/func_registry/error.rs | 0 .../func_registry/function_type.rs | 0 {wef/src => src}/func_registry/inject.js | 0 {wef/src => src}/func_registry/into_result.rs | 0 {wef/src => src}/func_registry/mod.rs | 0 {wef/src => src}/func_registry/registry.rs | 0 {wef/src => src}/geom.rs | 0 {wef/src => src}/input.rs | 0 {wef/src => src}/js_dialog.rs | 0 {wef/src => src}/lib.rs | 0 {wef/src => src}/query.rs | 0 {wef/src => src}/sandbox_context.rs | 0 {wef/src => src}/settings.rs | 0 {wef/src => src}/wef.rs | 0 {cargo-wef => tool}/Cargo.toml | 2 +- .../src/commands/add_framework.rs | 0 {cargo-wef => tool}/src/commands/build.rs | 0 {cargo-wef => tool}/src/commands/init.rs | 0 {cargo-wef => tool}/src/commands/mod.rs | 0 {cargo-wef => tool}/src/commands/run.rs | 0 .../src/internal/add_cef_framework.rs | 0 .../src/internal/add_helper.rs | 0 .../src/internal/cef_platform.rs | 0 .../src/internal/download_cef.rs | 0 .../src/internal/find_cef_root.rs | 0 {cargo-wef => tool}/src/internal/mod.rs | 0 {cargo-wef => tool}/src/internal/plist.rs | 0 {cargo-wef => tool}/src/main.rs | 0 wef/Cargo.toml | 29 -- wef/README.md | 359 ------------------ 75 files changed, 42 insertions(+), 405 deletions(-) rename wef/.rustfmt.toml => .rustfmt.toml (100%) rename wef/LICENSE-APACHE => LICENSE-APACHE (100%) rename wef/build.rs => build.rs (100%) rename {wef/cpp => cpp}/app.h (100%) rename {wef/cpp => cpp}/app_callbacks.h (100%) rename {wef/cpp => cpp}/app_render_process.h (100%) rename {wef/cpp => cpp}/browser_callbacks.h (100%) rename {wef/cpp => cpp}/client.cpp (100%) rename {wef/cpp => cpp}/client.h (100%) rename {wef/cpp => cpp}/cursor.cpp (100%) rename {wef/cpp => cpp}/dirty_rect.cpp (100%) rename {wef/cpp => cpp}/external_pump.cpp (100%) rename {wef/cpp => cpp}/external_pump.h (100%) rename {wef/cpp => cpp}/external_pump_linux.cpp (100%) rename {wef/cpp => cpp}/external_pump_mac.mm (100%) rename {wef/cpp => cpp}/external_pump_win.cpp (100%) rename {wef/cpp => cpp}/file_dialog.cpp (100%) rename {wef/cpp => cpp}/frame.cpp (100%) rename {wef/cpp => cpp}/frame.h (100%) rename {wef/cpp => cpp}/js_dialog.cpp (100%) rename {wef/cpp => cpp}/load_library.cpp (100%) rename {wef/cpp => cpp}/query.cpp (100%) rename {wef/cpp => cpp}/sandbox_context.cpp (100%) rename {wef/cpp => cpp}/utils.h (100%) rename {wef/cpp => cpp}/wef.cpp (100%) rename {wef/src => src}/app_handler.rs (100%) rename {wef/src => src}/browser.rs (100%) rename {wef/src => src}/browser_handler.rs (100%) rename {wef/src => src}/builder.rs (100%) rename {wef/src => src}/context_menu.rs (100%) rename {wef/src => src}/cursor.rs (100%) rename {wef/src => src}/dirty_rects.rs (100%) rename {wef/src => src}/dpi.rs (100%) rename {wef/src => src}/error.rs (100%) rename {wef/src => src}/ffi.rs (100%) rename {wef/src => src}/file_dialog.rs (100%) rename {wef/src => src}/frame.rs (100%) rename {wef/src => src}/framework_loader.rs (100%) rename {wef/src => src}/func_registry/async_function_type.rs (100%) rename {wef/src => src}/func_registry/builder.rs (100%) rename {wef/src => src}/func_registry/dyn_wrapper.rs (100%) rename {wef/src => src}/func_registry/error.rs (100%) rename {wef/src => src}/func_registry/function_type.rs (100%) rename {wef/src => src}/func_registry/inject.js (100%) rename {wef/src => src}/func_registry/into_result.rs (100%) rename {wef/src => src}/func_registry/mod.rs (100%) rename {wef/src => src}/func_registry/registry.rs (100%) rename {wef/src => src}/geom.rs (100%) rename {wef/src => src}/input.rs (100%) rename {wef/src => src}/js_dialog.rs (100%) rename {wef/src => src}/lib.rs (100%) rename {wef/src => src}/query.rs (100%) rename {wef/src => src}/sandbox_context.rs (100%) rename {wef/src => src}/settings.rs (100%) rename {wef/src => src}/wef.rs (100%) rename {cargo-wef => tool}/Cargo.toml (96%) rename {cargo-wef => tool}/src/commands/add_framework.rs (100%) rename {cargo-wef => tool}/src/commands/build.rs (100%) rename {cargo-wef => tool}/src/commands/init.rs (100%) rename {cargo-wef => tool}/src/commands/mod.rs (100%) rename {cargo-wef => tool}/src/commands/run.rs (100%) rename {cargo-wef => tool}/src/internal/add_cef_framework.rs (100%) rename {cargo-wef => tool}/src/internal/add_helper.rs (100%) rename {cargo-wef => tool}/src/internal/cef_platform.rs (100%) rename {cargo-wef => tool}/src/internal/download_cef.rs (100%) rename {cargo-wef => tool}/src/internal/find_cef_root.rs (100%) rename {cargo-wef => tool}/src/internal/mod.rs (100%) rename {cargo-wef => tool}/src/internal/plist.rs (100%) rename {cargo-wef => tool}/src/main.rs (100%) delete mode 100644 wef/Cargo.toml delete mode 100644 wef/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db3e53d..2951556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: - name: Init CEF if: ${{ !steps.cache-cef.outputs.cache-hit }} run: | - cargo run -p cargo-wef -- wef init + cargo run --bin cargo-wef -- wef init - name: Typo check if: ${{ matrix.run_on == 'macos-latest' }} run: | @@ -73,12 +73,12 @@ jobs: - name: Test Linux if: ${{ matrix.run_on == 'ubuntu-latest' }} run: | - cargo run -p cargo-wef -- wef add-framework target/debug + cargo run --bin cargo-wef -- wef add-framework target/debug cargo test --all - name: Test Windows if: ${{ matrix.run_on == 'windows-latest' }} run: | - cargo run -p cargo-wef -- wef add-framework target/debug + cargo run --bin cargo-wef -- wef add-framework target/debug cargo test --all - name: Test macOS if: ${{ matrix.run_on == 'macos-latest' }} diff --git a/wef/.rustfmt.toml b/.rustfmt.toml similarity index 100% rename from wef/.rustfmt.toml rename to .rustfmt.toml diff --git a/Cargo.toml b/Cargo.toml index 2a177c3..72d6e45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,41 @@ [workspace] members = [ - "wef", - "cargo-wef", + "tool", "examples/wef-winit", ] -default-members = ["wef"] +default-members = ["."] resolver = "2" +[package] +name = "wef" +version = "0.7.0" +edition = "2024" +authors = ["sunli "] +license = "Apache-2.0" +homepage = "https://github.com/longbridge/wef" +repository = "https://github.com/longbridge/wef" +description = "Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support." + +[build-dependencies] +cc = { version = "1.2.18", features = ["parallel"] } +pkg-config = "0.3.32" +dirs = "6.0.0" + +[dependencies] +image = { version = "0.25.6", default-features = false } +bitflags = "2.9.0" +thiserror = "2.0.12" +num_enum = "0.7.3" +mime = "0.3.17" +raw-window-handle = "0.6.2" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tuple_len = "3.0.0" +futures-util = "0.3.31" + [workspace.dependencies] -wef = { path = "wef" } +wef = { path = "." } ropey = { version = "=2.0.0-beta.1", features = ["metric_utf16", "metric_lines_lf"] } anyhow = "1" @@ -46,6 +72,9 @@ todo = "deny" type_complexity = "allow" manual_is_multiple_of = "allow" +[lints] +workspace = true + [profile.dev] codegen-units = 16 debug = "limited" diff --git a/wef/LICENSE-APACHE b/LICENSE-APACHE similarity index 100% rename from wef/LICENSE-APACHE rename to LICENSE-APACHE diff --git a/README.md b/README.md index 43ff638..b6abbfe 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,14 @@ fn main() { ## Documentation -For comprehensive documentation, examples, and API reference, see: - -- **[Library Documentation](wef/README.md)** - Complete guide to using the wef library -- **[API Reference](https://docs.rs/wef)** - Detailed API documentation -- **[Examples](examples/)** - Working examples including winit integration +For comprehensive documentation, examples, and API reference, see the library documentation within this repository. ## Project Structure This repository contains: -- **[`wef/`](wef/)** - The core wef library -- **[`cargo-wef/`](cargo-wef/)** - Command-line tool for building and managing wef applications +- **Core Library** - The main wef library (in the root directory) +- **[`tool/`](tool/)** - Command-line tool (`cargo-wef`) for building and managing wef applications - **[`examples/`](examples/)** - Example applications demonstrating wef usage ## Development @@ -100,4 +96,4 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING. ## License -Licensed under the Apache License, Version 2.0. See [LICENSE-APACHE](wef/LICENSE-APACHE) for details. \ No newline at end of file +Licensed under the Apache License, Version 2.0. See [LICENSE-APACHE](LICENSE-APACHE) for details. \ No newline at end of file diff --git a/wef/build.rs b/build.rs similarity index 100% rename from wef/build.rs rename to build.rs diff --git a/wef/cpp/app.h b/cpp/app.h similarity index 100% rename from wef/cpp/app.h rename to cpp/app.h diff --git a/wef/cpp/app_callbacks.h b/cpp/app_callbacks.h similarity index 100% rename from wef/cpp/app_callbacks.h rename to cpp/app_callbacks.h diff --git a/wef/cpp/app_render_process.h b/cpp/app_render_process.h similarity index 100% rename from wef/cpp/app_render_process.h rename to cpp/app_render_process.h diff --git a/wef/cpp/browser_callbacks.h b/cpp/browser_callbacks.h similarity index 100% rename from wef/cpp/browser_callbacks.h rename to cpp/browser_callbacks.h diff --git a/wef/cpp/client.cpp b/cpp/client.cpp similarity index 100% rename from wef/cpp/client.cpp rename to cpp/client.cpp diff --git a/wef/cpp/client.h b/cpp/client.h similarity index 100% rename from wef/cpp/client.h rename to cpp/client.h diff --git a/wef/cpp/cursor.cpp b/cpp/cursor.cpp similarity index 100% rename from wef/cpp/cursor.cpp rename to cpp/cursor.cpp diff --git a/wef/cpp/dirty_rect.cpp b/cpp/dirty_rect.cpp similarity index 100% rename from wef/cpp/dirty_rect.cpp rename to cpp/dirty_rect.cpp diff --git a/wef/cpp/external_pump.cpp b/cpp/external_pump.cpp similarity index 100% rename from wef/cpp/external_pump.cpp rename to cpp/external_pump.cpp diff --git a/wef/cpp/external_pump.h b/cpp/external_pump.h similarity index 100% rename from wef/cpp/external_pump.h rename to cpp/external_pump.h diff --git a/wef/cpp/external_pump_linux.cpp b/cpp/external_pump_linux.cpp similarity index 100% rename from wef/cpp/external_pump_linux.cpp rename to cpp/external_pump_linux.cpp diff --git a/wef/cpp/external_pump_mac.mm b/cpp/external_pump_mac.mm similarity index 100% rename from wef/cpp/external_pump_mac.mm rename to cpp/external_pump_mac.mm diff --git a/wef/cpp/external_pump_win.cpp b/cpp/external_pump_win.cpp similarity index 100% rename from wef/cpp/external_pump_win.cpp rename to cpp/external_pump_win.cpp diff --git a/wef/cpp/file_dialog.cpp b/cpp/file_dialog.cpp similarity index 100% rename from wef/cpp/file_dialog.cpp rename to cpp/file_dialog.cpp diff --git a/wef/cpp/frame.cpp b/cpp/frame.cpp similarity index 100% rename from wef/cpp/frame.cpp rename to cpp/frame.cpp diff --git a/wef/cpp/frame.h b/cpp/frame.h similarity index 100% rename from wef/cpp/frame.h rename to cpp/frame.h diff --git a/wef/cpp/js_dialog.cpp b/cpp/js_dialog.cpp similarity index 100% rename from wef/cpp/js_dialog.cpp rename to cpp/js_dialog.cpp diff --git a/wef/cpp/load_library.cpp b/cpp/load_library.cpp similarity index 100% rename from wef/cpp/load_library.cpp rename to cpp/load_library.cpp diff --git a/wef/cpp/query.cpp b/cpp/query.cpp similarity index 100% rename from wef/cpp/query.cpp rename to cpp/query.cpp diff --git a/wef/cpp/sandbox_context.cpp b/cpp/sandbox_context.cpp similarity index 100% rename from wef/cpp/sandbox_context.cpp rename to cpp/sandbox_context.cpp diff --git a/wef/cpp/utils.h b/cpp/utils.h similarity index 100% rename from wef/cpp/utils.h rename to cpp/utils.h diff --git a/wef/cpp/wef.cpp b/cpp/wef.cpp similarity index 100% rename from wef/cpp/wef.cpp rename to cpp/wef.cpp diff --git a/examples/wef-winit/Cargo.toml b/examples/wef-winit/Cargo.toml index 3452ff0..5f0a53e 100644 --- a/examples/wef-winit/Cargo.toml +++ b/examples/wef-winit/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" embed-manifest = "1.4.0" [dependencies] -wef = { path = "../../wef" } +wef = { path = "../.." } image = "0.25.6" winit = "0.30.9" diff --git a/wef/src/app_handler.rs b/src/app_handler.rs similarity index 100% rename from wef/src/app_handler.rs rename to src/app_handler.rs diff --git a/wef/src/browser.rs b/src/browser.rs similarity index 100% rename from wef/src/browser.rs rename to src/browser.rs diff --git a/wef/src/browser_handler.rs b/src/browser_handler.rs similarity index 100% rename from wef/src/browser_handler.rs rename to src/browser_handler.rs diff --git a/wef/src/builder.rs b/src/builder.rs similarity index 100% rename from wef/src/builder.rs rename to src/builder.rs diff --git a/wef/src/context_menu.rs b/src/context_menu.rs similarity index 100% rename from wef/src/context_menu.rs rename to src/context_menu.rs diff --git a/wef/src/cursor.rs b/src/cursor.rs similarity index 100% rename from wef/src/cursor.rs rename to src/cursor.rs diff --git a/wef/src/dirty_rects.rs b/src/dirty_rects.rs similarity index 100% rename from wef/src/dirty_rects.rs rename to src/dirty_rects.rs diff --git a/wef/src/dpi.rs b/src/dpi.rs similarity index 100% rename from wef/src/dpi.rs rename to src/dpi.rs diff --git a/wef/src/error.rs b/src/error.rs similarity index 100% rename from wef/src/error.rs rename to src/error.rs diff --git a/wef/src/ffi.rs b/src/ffi.rs similarity index 100% rename from wef/src/ffi.rs rename to src/ffi.rs diff --git a/wef/src/file_dialog.rs b/src/file_dialog.rs similarity index 100% rename from wef/src/file_dialog.rs rename to src/file_dialog.rs diff --git a/wef/src/frame.rs b/src/frame.rs similarity index 100% rename from wef/src/frame.rs rename to src/frame.rs diff --git a/wef/src/framework_loader.rs b/src/framework_loader.rs similarity index 100% rename from wef/src/framework_loader.rs rename to src/framework_loader.rs diff --git a/wef/src/func_registry/async_function_type.rs b/src/func_registry/async_function_type.rs similarity index 100% rename from wef/src/func_registry/async_function_type.rs rename to src/func_registry/async_function_type.rs diff --git a/wef/src/func_registry/builder.rs b/src/func_registry/builder.rs similarity index 100% rename from wef/src/func_registry/builder.rs rename to src/func_registry/builder.rs diff --git a/wef/src/func_registry/dyn_wrapper.rs b/src/func_registry/dyn_wrapper.rs similarity index 100% rename from wef/src/func_registry/dyn_wrapper.rs rename to src/func_registry/dyn_wrapper.rs diff --git a/wef/src/func_registry/error.rs b/src/func_registry/error.rs similarity index 100% rename from wef/src/func_registry/error.rs rename to src/func_registry/error.rs diff --git a/wef/src/func_registry/function_type.rs b/src/func_registry/function_type.rs similarity index 100% rename from wef/src/func_registry/function_type.rs rename to src/func_registry/function_type.rs diff --git a/wef/src/func_registry/inject.js b/src/func_registry/inject.js similarity index 100% rename from wef/src/func_registry/inject.js rename to src/func_registry/inject.js diff --git a/wef/src/func_registry/into_result.rs b/src/func_registry/into_result.rs similarity index 100% rename from wef/src/func_registry/into_result.rs rename to src/func_registry/into_result.rs diff --git a/wef/src/func_registry/mod.rs b/src/func_registry/mod.rs similarity index 100% rename from wef/src/func_registry/mod.rs rename to src/func_registry/mod.rs diff --git a/wef/src/func_registry/registry.rs b/src/func_registry/registry.rs similarity index 100% rename from wef/src/func_registry/registry.rs rename to src/func_registry/registry.rs diff --git a/wef/src/geom.rs b/src/geom.rs similarity index 100% rename from wef/src/geom.rs rename to src/geom.rs diff --git a/wef/src/input.rs b/src/input.rs similarity index 100% rename from wef/src/input.rs rename to src/input.rs diff --git a/wef/src/js_dialog.rs b/src/js_dialog.rs similarity index 100% rename from wef/src/js_dialog.rs rename to src/js_dialog.rs diff --git a/wef/src/lib.rs b/src/lib.rs similarity index 100% rename from wef/src/lib.rs rename to src/lib.rs diff --git a/wef/src/query.rs b/src/query.rs similarity index 100% rename from wef/src/query.rs rename to src/query.rs diff --git a/wef/src/sandbox_context.rs b/src/sandbox_context.rs similarity index 100% rename from wef/src/sandbox_context.rs rename to src/sandbox_context.rs diff --git a/wef/src/settings.rs b/src/settings.rs similarity index 100% rename from wef/src/settings.rs rename to src/settings.rs diff --git a/wef/src/wef.rs b/src/wef.rs similarity index 100% rename from wef/src/wef.rs rename to src/wef.rs diff --git a/cargo-wef/Cargo.toml b/tool/Cargo.toml similarity index 96% rename from cargo-wef/Cargo.toml rename to tool/Cargo.toml index 7d1452b..73b0a17 100644 --- a/cargo-wef/Cargo.toml +++ b/tool/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" homepage = "https://github.com/longbridge/wef" repository = "https://github.com/longbridge/wef" description = "Cargo-wef is a command line tool for wef" -readme = "../wef/README.md" +readme = "../README.md" [dependencies] clap = { version = "4.5.38", features = ["derive", "env"] } diff --git a/cargo-wef/src/commands/add_framework.rs b/tool/src/commands/add_framework.rs similarity index 100% rename from cargo-wef/src/commands/add_framework.rs rename to tool/src/commands/add_framework.rs diff --git a/cargo-wef/src/commands/build.rs b/tool/src/commands/build.rs similarity index 100% rename from cargo-wef/src/commands/build.rs rename to tool/src/commands/build.rs diff --git a/cargo-wef/src/commands/init.rs b/tool/src/commands/init.rs similarity index 100% rename from cargo-wef/src/commands/init.rs rename to tool/src/commands/init.rs diff --git a/cargo-wef/src/commands/mod.rs b/tool/src/commands/mod.rs similarity index 100% rename from cargo-wef/src/commands/mod.rs rename to tool/src/commands/mod.rs diff --git a/cargo-wef/src/commands/run.rs b/tool/src/commands/run.rs similarity index 100% rename from cargo-wef/src/commands/run.rs rename to tool/src/commands/run.rs diff --git a/cargo-wef/src/internal/add_cef_framework.rs b/tool/src/internal/add_cef_framework.rs similarity index 100% rename from cargo-wef/src/internal/add_cef_framework.rs rename to tool/src/internal/add_cef_framework.rs diff --git a/cargo-wef/src/internal/add_helper.rs b/tool/src/internal/add_helper.rs similarity index 100% rename from cargo-wef/src/internal/add_helper.rs rename to tool/src/internal/add_helper.rs diff --git a/cargo-wef/src/internal/cef_platform.rs b/tool/src/internal/cef_platform.rs similarity index 100% rename from cargo-wef/src/internal/cef_platform.rs rename to tool/src/internal/cef_platform.rs diff --git a/cargo-wef/src/internal/download_cef.rs b/tool/src/internal/download_cef.rs similarity index 100% rename from cargo-wef/src/internal/download_cef.rs rename to tool/src/internal/download_cef.rs diff --git a/cargo-wef/src/internal/find_cef_root.rs b/tool/src/internal/find_cef_root.rs similarity index 100% rename from cargo-wef/src/internal/find_cef_root.rs rename to tool/src/internal/find_cef_root.rs diff --git a/cargo-wef/src/internal/mod.rs b/tool/src/internal/mod.rs similarity index 100% rename from cargo-wef/src/internal/mod.rs rename to tool/src/internal/mod.rs diff --git a/cargo-wef/src/internal/plist.rs b/tool/src/internal/plist.rs similarity index 100% rename from cargo-wef/src/internal/plist.rs rename to tool/src/internal/plist.rs diff --git a/cargo-wef/src/main.rs b/tool/src/main.rs similarity index 100% rename from cargo-wef/src/main.rs rename to tool/src/main.rs diff --git a/wef/Cargo.toml b/wef/Cargo.toml deleted file mode 100644 index c9c7120..0000000 --- a/wef/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "wef" -version = "0.7.0" -edition = "2024" -authors = ["sunli "] -license = "Apache-2.0" -homepage = "https://github.com/longbridge/wef" -repository = "https://github.com/longbridge/wef" -description = "Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support." - -[build-dependencies] -cc = { version = "1.2.18", features = ["parallel"] } -pkg-config = "0.3.32" -dirs = "6.0.0" - -[dependencies] -image = { version = "0.25.6", default-features = false } -bitflags = "2.9.0" -thiserror = "2.0.12" -num_enum = "0.7.3" -mime = "0.3.17" -raw-window-handle = "0.6.2" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -tuple_len = "3.0.0" -futures-util = "0.3.31" - -[lints] -workspace = true \ No newline at end of file diff --git a/wef/README.md b/wef/README.md deleted file mode 100644 index db3b3e7..0000000 --- a/wef/README.md +++ /dev/null @@ -1,359 +0,0 @@ -# Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support. - -> The `Wef` name is abbreviation of "Web Embedding Framework", it also inspires by Wry. - -![Wef Example](https://github.com/user-attachments/assets/f677ecb4-dbff-4e0d-86b9-203f6e1004a4) - -## Contents - -- [Introduction](#introduction) -- [Getting Started](#getting-started) -- [Important Concepts](#important-concepts) -- [Application Layout](#application-layout) - - [Windows](#windows) - - [Linux](#linux) - - [macOS](#macos) -- [Application Structure](#application-structure) - - [Entry-Point Function](#entry-point-function) - - [Single Executable](#single-executable) - - [Separate Sub-Process Executable](#separate-sub-process-executable) -- [Examples](#examples) -- [JS Bridge](#js-bridge) - - [Call Rust functions from JavaScript](#call-rust-functions-from-javascript) - - [Post Message from Rust to JavaScript](#post-message-from-rust-to-javascript) -- [Cargo-wef](#cargo-wef) - - [Installation Cargo-wef](#installation-cargo-wef) - - [Build Wef application](#build-wef-application) - - [macOS Bundle Settings](#macos-bundle-settings) - - [Run Wef application](#run-wef-application) - - [Add CEF3 Framework to the application](#add-cef3-framework-to-the-application) - -## Introduction - -`Wef` is a Rust library that provides a simple and efficient way to embed WebView functionality in your applications. It uses the `Chromium Embedded Framework (CEF3)` for rendering web content and supports offscreen rendering, allowing you to create rich web-based user interfaces. - -`CEF3` is the next generation of CEF based on the multi-process Chromium Content API. - -## Getting Started - -To use Wef, you need to download the CEF binary distribution. You can find the latest version of CEF on the [CEF Download page](https://cef-builds.spotifycdn.com/index.html). Make sure to download the appropriate version for your platform (Windows, macOS or Linux). - -After downloading the CEF binary distribution, extract it to a directory of your choice. - -Set the `CEF_ROOT` environment variable to point to the directory where you extracted the CEF binary distribution. This is necessary for Wef to locate the CEF libraries and resources. - -## Important Concepts - -CEF3 runs using multiple processes. The main process which handles window creation, UI and network access is called the `browser` process. This is generally the same process as the host application and the majority of the application logic will run in the browser process. Blink rendering and JavaScript execution occur in a separate `render` process. Some application logic, such as JavaScript bindings and DOM access, will also run in the render process. The default process model will spawn a new render process for each unique origin (scheme + domain). Other processes will be spawned as needed, such as the `gpu` process to handle accelerated compositing. - -By default the main application executable will be spawned multiple times to represent separate processes. This is handled via command-line flags that are passed into the `wef::execute_process` function. If the main application executable is large, takes a long time to load, or is otherwise unsuitable for non-browser processes the host can use a separate executable for those other processes. This can be configured via the `Settings.browser_subprocess_path` variable. - -## Application Layout - -### Windows - -On Windows the default layout places the libcef library and related resources next to the application executable. - -```plain -Application/ - cefclient.exe <= cefclient application executable - libcef.dll <= main CEF library - icudtl.dat <= unicode support data - libEGL.dll, libGLESv2.dll, ... <= accelerated compositing support libraries - chrome_100_percent.pak, chrome_200_percent.pak, resources.pak <= non-localized resources and strings - snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot - locales/ - en-US.pak, ... <= locale-specific resources and strings -``` - -### Linux - -On Linux the default layout places the libcef library and related resources next to the application executable. Note however that there's a discrepancy between where libcef.so is located in the client distribution and where it's located in the binary distribution that you build yourself. The location depends on how the linker rpath value is set when building the application executable. For example, a value of "-Wl,-rpath,." ("." meaning the current directory) will allow you to place libcef.so next to the application executable. The path to libcef.so can also be specified using the LD_LIBRARY_PATH environment variable. - -```plain -Application/ - cefclient <= cefclient application executable - libcef.so <= main CEF library - icudtl.dat <= unicode support data - chrome_100_percent.pak, chrome_200_percent.pak, resources.pak <= non-localized resources and strings - snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot - locales/ - en-US.pak, ... <= locale-specific resources and strings -``` - -### macOS - -The application (app bundle) layout on macOS is mandated by the Chromium implementation and consequently is not very flexible. - -```plain -cefclient.app/ - Contents/ - Frameworks/ - Chromium Embedded Framework.framework/ - Chromium Embedded Framework <= main application library - Resources/ - chrome_100_percent.pak, chrome_200_percent.pak, resources.pak, ... <= non-localized resources and strings - icudtl.dat <= unicode support data - snapshot_blob.bin, v8_context_snapshot.bin <= V8 initial snapshot - en.lproj/, ... <= locale-specific resources and strings - cefclient Helper.app/ - Contents/ - Info.plist - MacOS/ - cefclient Helper <= helper executable - cefclient Helper (Alerts).app/ - Contents/ - Info.plist - MacOS/ - cefclient Helper (Alerts) - cefclient Helper (GPU).app/ - Contents/ - Info.plist - MacOS/ - cefclient Helper (GPU) - cefclient Helper (Plugin).app/ - Contents/ - Info.plist - MacOS/ - cefclient Helper (Plugin) - cefclient Helper (Renderer).app/ - Contents/ - Info.plist - MacOS/ - cefclient Helper (Renderer) - Info.plist - MacOS/ - cefclient <= cefclient application executable -``` - -### Application Structure - -Every CEF3 application has the same general structure. - -Provide an entry-point function that initializes CEF and runs either sub-process executable logic or the CEF message loop. - -Provide an implementation of `wef::BrowserHandler` to handle browser-instance-specific callbacks. -Call `BrowserBuilder:build` to create a browser instance. - -#### Entry-Point Function - -As described in the `Important Concepts` section a CEF3 application will run multiple processes. The processes can all use the same executable or a separate executable can be specified for the sub-processes. Execution of the process begins in the entry-point function. - -#### Single Executable - -When running as a single executable the entry-point function is required to differentiate between the different process types. The single executable structure is supported on Windows and Linux but not on macOS. - -```rust, no_run -use wef::Settings; - -fn main(){ - let settings = Settings::new(); - wef::launch(settings, || { - // message loop - }); -} -``` - -#### Separate Sub-Process Executable - -When using a separate sub-process executable you need two separate executable projects and two separate entry-point functions. - -**Main application entry-point function:** - -```rust, no_run -use wef::Settings; - -fn main() { - let settings = Settings::new(); - wef::launch(settings, || { - // message loop - }); -} -``` - -**Sub-process application entry-point function:** - -```rust, no_run -fn main() -> Result<(), Box> { - // Load the CEF framework library at runtime instead of linking directly as required by the macOS implementation. - #[cfg(target_os = "macos")] - let _ = wef::FrameworkLoader::load_in_helper()?; - - wef::exec_process()?; - Ok(()) -} -``` - -## Examples - -- [winit](../examples/wef-winit/) - -## JS Bridge - -The JS Bridge allows you to call Rust functions from JavaScript and post messages from Rust to JavaScript. This is useful for integrating Rust logic into your web-based user interface. - -### Call Rust functions from JavaScript - -**Register Rust functions in the browser instance:** - -```rust, no_run -use wef::{FuncRegistry, Browser}; - -// Create functions registry -let func_registry = FuncRegistry::builder() - .register("addInt", |a: i32, b: i32| a + b) - .register("subInt", |a: f32, b: f32| a - b) - .build(); - -// Build browser instance with the functions registry -let browser = Browser::builder() - .func_registry(func_registry) - .build(); -``` - -**Call Rust functions from JavaScript:** - -The Rust results are returned as promises in JavaScript. You can use the `Promise.then` method to handle the result, and the `Promise.catch` method to handle errors. - -```javascript -jsBridge.addInt(1, 2).then((result) => { - console.log("Result of addInt:", result); - // Result of addInt: 3 -}); -``` - -**Asynchronous functions:** - -Use `FuncRegistry::with_spawn` to create a `AsyncFuncRegistryBuilder` and register asynchronous functions with `AsyncFuncRegistryBuilder::register_async` method. - -```rust, ignore -use std::time::Duration; - -use wef::FuncRegistry; - -let func_registry = FuncRegistry::builder() - .with_spawn(tokio::spawn) // Convert the builder to AsyncFuncRegistryBuilder - .register_async("sleep", |millis: u64| async move { - tokio::sleep(Duration::from_millis(millis)).await; - }) - .build(); -``` - -### Post Message from Rust to JavaScript - -```rust, ignore -let browser: Browser = ...; // browser instance -let Some(frame) = browser.main_frame() { - frame.emit("ok"); // Emit a message to the javascript side -} -``` - -**Subscribe to messages in JavaScript:** - -```javascript -jsBridge.addEventListener((message) => { - console.log("Message from Rust:", message); - // Message from Rust: ok -}); -``` - -## Cargo Wef - -The `cargo-wef` is a command-line tool that helps you set up the necessary directory structure for your CEF3 application. It creates the required directories and copies the necessary files from the CEF binary distribution to the appropriate locations. - -We strongly recommend using `cargo-wef` to build/run your CEF3 application, as it simplifies the process of setting up and building your application. - -### Installation Cargo Wef - -To install the `cargo-wef`, you can use the following command: - -```bash -cargo install cargo-wef -``` - -### Init Wef - -The `init` command used to init and download CEF into your system, default download path is `~/.cef`, you can change it by passing the path to the command. - -```bash -cargo wef init -``` - -### Build Wef application - -Like cargo build, but it will also copy the CEF3 framework to the target directory. - -```bash -cargo wef build -``` - -On macOS, this command will also create an application bundle with the CEF3 framework inside. -On Windows and Linux, it will copy the CEF3 framework to the target directory. - -````bash -If on macOS, this command also create application bundle with the CEF3 framework inside. - -### Run Wef application - -Like cargo run, but it will also copy the CEF3 framework to the target directory. - -```bash -cargo wef run -```` - -#### macOS Bundle Settings - -You can specify the application bundle settings in your `Cargo.toml` file under the `package.metadata.bundle` section, otherwise it will use the default settings. - -```toml -[package.metadata.bundle] -name = "my-wef-app" -identifier = "my.wef.app" -``` - -Settings for a specific binary: - -```toml -[package.metadata.bundle.bin.example1] -name = "my-wef-app" -identifier = "my.wef.app" -``` - -Settings for a specific example: - -```toml -[package.metadata.bundle.example.app1] -name = "my-wef-app" -identifier = "my.wef.app" -``` - -| name | type | optional | description | -| ---------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | -| name | String | No | Bundle name | -| identifier | String | No | Bundle identifier | -| display_name | String | Yes | Display name, If is `None` then use `name` | -| executable_name | String | Yes | Executable name, If is `None` then use `name` | -| region | String | Yes | Region, If is `None` then use `en` | -| bundle_version | String | Yes | Bundle version, If is `None` then use empty string | -| bundle_short_version | String | Yes | Bundle short version, If is `None` then use crate version | -| category | String | Yes | Category | -| minimum_system_version | String | Yes | Minimum system version | -| icons | [String] | Yes | Array of icon paths, base path is the package directory(same as `Cargo.toml`) | -| url_schemes | [String] | Yes | Array of URL schemes | -| agent_app | bool | Yes | If is `true` then indicating whether the app is an agent app that runs in the background and doesn't appear in the Dock. | - -### Add CEF3 Framework to the application - -For macOS - -```bash -cargo wef add-framework /path/to/your/app.bundle -``` - -For Windows or Linux - -```bash -cargo wef add-framework /path/to/app -``` - -Or you can use the `--release` flag to add the framework to a release build of your application. \ No newline at end of file From c362aaaed1b03fbd5990016252965932f4775635 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:16:13 +0000 Subject: [PATCH 06/16] Add missing wef-example and fix CI workflow binary references Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- .github/workflows/ci.yml | 6 ++-- Cargo.toml | 1 + examples/wef-example/Cargo.toml | 12 +++++++ examples/wef-example/build.rs | 3 ++ examples/wef-example/src/main.rs | 54 ++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 examples/wef-example/Cargo.toml create mode 100644 examples/wef-example/build.rs create mode 100644 examples/wef-example/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2951556..db3e53d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: - name: Init CEF if: ${{ !steps.cache-cef.outputs.cache-hit }} run: | - cargo run --bin cargo-wef -- wef init + cargo run -p cargo-wef -- wef init - name: Typo check if: ${{ matrix.run_on == 'macos-latest' }} run: | @@ -73,12 +73,12 @@ jobs: - name: Test Linux if: ${{ matrix.run_on == 'ubuntu-latest' }} run: | - cargo run --bin cargo-wef -- wef add-framework target/debug + cargo run -p cargo-wef -- wef add-framework target/debug cargo test --all - name: Test Windows if: ${{ matrix.run_on == 'windows-latest' }} run: | - cargo run --bin cargo-wef -- wef add-framework target/debug + cargo run -p cargo-wef -- wef add-framework target/debug cargo test --all - name: Test macOS if: ${{ matrix.run_on == 'macos-latest' }} diff --git a/Cargo.toml b/Cargo.toml index 72d6e45..a84fc10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "tool", "examples/wef-winit", + "examples/wef-example", ] default-members = ["."] diff --git a/examples/wef-example/Cargo.toml b/examples/wef-example/Cargo.toml new file mode 100644 index 0000000..60b4102 --- /dev/null +++ b/examples/wef-example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wef-example" +version = "0.1.0" +edition = "2024" + +[build-dependencies] +embed-manifest = "1.4.0" + +[dependencies] +wef = { path = "../.." } +serde = { version = "1.0.219", features = ["derive"] } +futures-util = "0.3.31" diff --git a/examples/wef-example/build.rs b/examples/wef-example/build.rs new file mode 100644 index 0000000..c2bce4f --- /dev/null +++ b/examples/wef-example/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/examples/wef-example/src/main.rs b/examples/wef-example/src/main.rs new file mode 100644 index 0000000..a88555d --- /dev/null +++ b/examples/wef-example/src/main.rs @@ -0,0 +1,54 @@ +use std::sync::mpsc; +use wef::{Browser, BrowserHandler, Settings, FuncRegistry}; + +struct SimpleHandler; + +impl BrowserHandler for SimpleHandler { + fn on_context_created(&mut self, _frame: &wef::Frame) {} + + fn on_before_browse(&mut self, _frame: &wef::Frame, _request: &wef::ffi::CefRequest, _user_gesture: bool, _is_redirect: bool) -> bool { + false + } +} + +fn main() -> Result<(), Box> { + // Simple wef example showing basic browser creation + println!("Starting wef example..."); + + let settings = Settings::new() + .with_log_severity(wef::settings::LogSeverity::Info) + .with_cache_path(std::env::temp_dir().join("wef-example-cache")); + + // Create a function registry for JavaScript bridge + let func_registry = FuncRegistry::builder() + .register("hello", || "Hello from Rust!".to_string()) + .register("add", |a: i32, b: i32| a + b) + .build(); + + wef::launch(settings, || { + println!("CEF initialized successfully"); + + // Create a browser instance + let handler = SimpleHandler; + let browser = Browser::builder() + .with_url("data:text/html,

Hello from Wef!

") + .with_size(800, 600) + .with_handler(handler) + .with_func_registry(func_registry) + .build(); + + if let Ok(_browser) = browser { + println!("Browser created successfully!"); + + // In a real application, you would handle events and keep the browser running + // For this example, we'll just demonstrate that the browser can be created + std::thread::sleep(std::time::Duration::from_secs(2)); + } else { + eprintln!("Failed to create browser"); + } + + println!("Wef example completed"); + }); + + Ok(()) +} From 9bb61aae7dd49214a2ece0a92281278409179d48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:08:54 +0000 Subject: [PATCH 07/16] Fix CI workflow by restoring job condition for pull request handling Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db3e53d..13b014c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: jobs: test: name: Test + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: fail-fast: false matrix: From b9fffd1ccf8c6e3373595ffcce03f2bf0164726c Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 13:31:20 +0800 Subject: [PATCH 08/16] Simplify test job conditions in CI workflow Remove condition for running tests on pull requests. --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13b014c..db3e53d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ env: jobs: test: name: Test - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: fail-fast: false matrix: From cf28c8719f28ac9c75b6c824780719ad2ef50203 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:36:27 +0000 Subject: [PATCH 09/16] Remove unused dependencies from wef-example to fix cargo-machete CI check Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- examples/wef-example/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/wef-example/Cargo.toml b/examples/wef-example/Cargo.toml index 60b4102..8c80578 100644 --- a/examples/wef-example/Cargo.toml +++ b/examples/wef-example/Cargo.toml @@ -8,5 +8,3 @@ embed-manifest = "1.4.0" [dependencies] wef = { path = "../.." } -serde = { version = "1.0.219", features = ["derive"] } -futures-util = "0.3.31" From 5aaa99c2730d603e0d44ca0d1c72db15748ccb2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 06:01:39 +0000 Subject: [PATCH 10/16] Restructure repository with crates/ subdirectories and add missing webview crate Co-authored-by: huacnlee <5518+huacnlee@users.noreply.github.com> --- Cargo.toml | 39 +- {tool => crates/cargo-wef}/Cargo.toml | 7 +- .../cargo-wef}/src/commands/add_framework.rs | 0 .../cargo-wef}/src/commands/build.rs | 0 .../cargo-wef}/src/commands/init.rs | 0 .../cargo-wef}/src/commands/mod.rs | 0 .../cargo-wef}/src/commands/run.rs | 0 .../src/internal/add_cef_framework.rs | 0 .../cargo-wef}/src/internal/add_helper.rs | 0 .../cargo-wef}/src/internal/cef_platform.rs | 0 .../cargo-wef}/src/internal/download_cef.rs | 0 .../cargo-wef}/src/internal/find_cef_root.rs | 0 .../cargo-wef}/src/internal/mod.rs | 0 .../cargo-wef}/src/internal/plist.rs | 0 {tool => crates/cargo-wef}/src/main.rs | 0 crates/webview/.rustfmt.toml | 12 + crates/webview/Cargo.toml | 19 + crates/webview/LICENSE-APACHE | 191 ++++++++++ crates/webview/README.md | 16 + crates/webview/src/browser_handler.rs | 339 +++++++++++++++++ crates/webview/src/context_menu.rs | 117 ++++++ crates/webview/src/element.rs | 246 ++++++++++++ crates/webview/src/events.rs | 100 +++++ crates/webview/src/frame_view.rs | 37 ++ crates/webview/src/lib.rs | 15 + crates/webview/src/utils.rs | 77 ++++ crates/webview/src/webview.rs | 350 ++++++++++++++++++ crates/webview/tests/index.html | 48 +++ crates/webview/tests/input.html | 83 +++++ crates/webview/tests/jsbridge.html | 103 ++++++ crates/webview/tests/jsdialog.html | 33 ++ crates/webview/tests/upload.html | 69 ++++ .rustfmt.toml => crates/wef/.rustfmt.toml | 0 crates/wef/Cargo.toml | 29 ++ LICENSE-APACHE => crates/wef/LICENSE-APACHE | 0 build.rs => crates/wef/build.rs | 0 {cpp => crates/wef/cpp}/app.h | 0 {cpp => crates/wef/cpp}/app_callbacks.h | 0 {cpp => crates/wef/cpp}/app_render_process.h | 0 {cpp => crates/wef/cpp}/browser_callbacks.h | 0 {cpp => crates/wef/cpp}/client.cpp | 0 {cpp => crates/wef/cpp}/client.h | 0 {cpp => crates/wef/cpp}/cursor.cpp | 0 {cpp => crates/wef/cpp}/dirty_rect.cpp | 0 {cpp => crates/wef/cpp}/external_pump.cpp | 0 {cpp => crates/wef/cpp}/external_pump.h | 0 .../wef/cpp}/external_pump_linux.cpp | 0 {cpp => crates/wef/cpp}/external_pump_mac.mm | 0 {cpp => crates/wef/cpp}/external_pump_win.cpp | 0 {cpp => crates/wef/cpp}/file_dialog.cpp | 0 {cpp => crates/wef/cpp}/frame.cpp | 0 {cpp => crates/wef/cpp}/frame.h | 0 {cpp => crates/wef/cpp}/js_dialog.cpp | 0 {cpp => crates/wef/cpp}/load_library.cpp | 0 {cpp => crates/wef/cpp}/query.cpp | 0 {cpp => crates/wef/cpp}/sandbox_context.cpp | 0 {cpp => crates/wef/cpp}/utils.h | 0 {cpp => crates/wef/cpp}/wef.cpp | 0 {src => crates/wef/src}/app_handler.rs | 0 {src => crates/wef/src}/browser.rs | 0 {src => crates/wef/src}/browser_handler.rs | 0 {src => crates/wef/src}/builder.rs | 0 {src => crates/wef/src}/context_menu.rs | 0 {src => crates/wef/src}/cursor.rs | 0 {src => crates/wef/src}/dirty_rects.rs | 0 {src => crates/wef/src}/dpi.rs | 0 {src => crates/wef/src}/error.rs | 0 {src => crates/wef/src}/ffi.rs | 0 {src => crates/wef/src}/file_dialog.rs | 0 {src => crates/wef/src}/frame.rs | 0 {src => crates/wef/src}/framework_loader.rs | 0 .../src}/func_registry/async_function_type.rs | 0 .../wef/src}/func_registry/builder.rs | 0 .../wef/src}/func_registry/dyn_wrapper.rs | 0 .../wef/src}/func_registry/error.rs | 0 .../wef/src}/func_registry/function_type.rs | 0 .../wef/src}/func_registry/inject.js | 0 .../wef/src}/func_registry/into_result.rs | 0 {src => crates/wef/src}/func_registry/mod.rs | 0 .../wef/src}/func_registry/registry.rs | 0 {src => crates/wef/src}/geom.rs | 0 {src => crates/wef/src}/input.rs | 0 {src => crates/wef/src}/js_dialog.rs | 0 {src => crates/wef/src}/lib.rs | 0 {src => crates/wef/src}/query.rs | 0 {src => crates/wef/src}/sandbox_context.rs | 0 {src => crates/wef/src}/settings.rs | 0 {src => crates/wef/src}/wef.rs | 0 examples/wef-example/Cargo.toml | 2 +- examples/wef-winit/Cargo.toml | 2 +- 90 files changed, 1897 insertions(+), 37 deletions(-) rename {tool => crates/cargo-wef}/Cargo.toml (91%) rename {tool => crates/cargo-wef}/src/commands/add_framework.rs (100%) rename {tool => crates/cargo-wef}/src/commands/build.rs (100%) rename {tool => crates/cargo-wef}/src/commands/init.rs (100%) rename {tool => crates/cargo-wef}/src/commands/mod.rs (100%) rename {tool => crates/cargo-wef}/src/commands/run.rs (100%) rename {tool => crates/cargo-wef}/src/internal/add_cef_framework.rs (100%) rename {tool => crates/cargo-wef}/src/internal/add_helper.rs (100%) rename {tool => crates/cargo-wef}/src/internal/cef_platform.rs (100%) rename {tool => crates/cargo-wef}/src/internal/download_cef.rs (100%) rename {tool => crates/cargo-wef}/src/internal/find_cef_root.rs (100%) rename {tool => crates/cargo-wef}/src/internal/mod.rs (100%) rename {tool => crates/cargo-wef}/src/internal/plist.rs (100%) rename {tool => crates/cargo-wef}/src/main.rs (100%) create mode 100644 crates/webview/.rustfmt.toml create mode 100644 crates/webview/Cargo.toml create mode 100644 crates/webview/LICENSE-APACHE create mode 100644 crates/webview/README.md create mode 100644 crates/webview/src/browser_handler.rs create mode 100644 crates/webview/src/context_menu.rs create mode 100644 crates/webview/src/element.rs create mode 100644 crates/webview/src/events.rs create mode 100644 crates/webview/src/frame_view.rs create mode 100644 crates/webview/src/lib.rs create mode 100644 crates/webview/src/utils.rs create mode 100644 crates/webview/src/webview.rs create mode 100644 crates/webview/tests/index.html create mode 100644 crates/webview/tests/input.html create mode 100644 crates/webview/tests/jsbridge.html create mode 100644 crates/webview/tests/jsdialog.html create mode 100644 crates/webview/tests/upload.html rename .rustfmt.toml => crates/wef/.rustfmt.toml (100%) create mode 100644 crates/wef/Cargo.toml rename LICENSE-APACHE => crates/wef/LICENSE-APACHE (100%) rename build.rs => crates/wef/build.rs (100%) rename {cpp => crates/wef/cpp}/app.h (100%) rename {cpp => crates/wef/cpp}/app_callbacks.h (100%) rename {cpp => crates/wef/cpp}/app_render_process.h (100%) rename {cpp => crates/wef/cpp}/browser_callbacks.h (100%) rename {cpp => crates/wef/cpp}/client.cpp (100%) rename {cpp => crates/wef/cpp}/client.h (100%) rename {cpp => crates/wef/cpp}/cursor.cpp (100%) rename {cpp => crates/wef/cpp}/dirty_rect.cpp (100%) rename {cpp => crates/wef/cpp}/external_pump.cpp (100%) rename {cpp => crates/wef/cpp}/external_pump.h (100%) rename {cpp => crates/wef/cpp}/external_pump_linux.cpp (100%) rename {cpp => crates/wef/cpp}/external_pump_mac.mm (100%) rename {cpp => crates/wef/cpp}/external_pump_win.cpp (100%) rename {cpp => crates/wef/cpp}/file_dialog.cpp (100%) rename {cpp => crates/wef/cpp}/frame.cpp (100%) rename {cpp => crates/wef/cpp}/frame.h (100%) rename {cpp => crates/wef/cpp}/js_dialog.cpp (100%) rename {cpp => crates/wef/cpp}/load_library.cpp (100%) rename {cpp => crates/wef/cpp}/query.cpp (100%) rename {cpp => crates/wef/cpp}/sandbox_context.cpp (100%) rename {cpp => crates/wef/cpp}/utils.h (100%) rename {cpp => crates/wef/cpp}/wef.cpp (100%) rename {src => crates/wef/src}/app_handler.rs (100%) rename {src => crates/wef/src}/browser.rs (100%) rename {src => crates/wef/src}/browser_handler.rs (100%) rename {src => crates/wef/src}/builder.rs (100%) rename {src => crates/wef/src}/context_menu.rs (100%) rename {src => crates/wef/src}/cursor.rs (100%) rename {src => crates/wef/src}/dirty_rects.rs (100%) rename {src => crates/wef/src}/dpi.rs (100%) rename {src => crates/wef/src}/error.rs (100%) rename {src => crates/wef/src}/ffi.rs (100%) rename {src => crates/wef/src}/file_dialog.rs (100%) rename {src => crates/wef/src}/frame.rs (100%) rename {src => crates/wef/src}/framework_loader.rs (100%) rename {src => crates/wef/src}/func_registry/async_function_type.rs (100%) rename {src => crates/wef/src}/func_registry/builder.rs (100%) rename {src => crates/wef/src}/func_registry/dyn_wrapper.rs (100%) rename {src => crates/wef/src}/func_registry/error.rs (100%) rename {src => crates/wef/src}/func_registry/function_type.rs (100%) rename {src => crates/wef/src}/func_registry/inject.js (100%) rename {src => crates/wef/src}/func_registry/into_result.rs (100%) rename {src => crates/wef/src}/func_registry/mod.rs (100%) rename {src => crates/wef/src}/func_registry/registry.rs (100%) rename {src => crates/wef/src}/geom.rs (100%) rename {src => crates/wef/src}/input.rs (100%) rename {src => crates/wef/src}/js_dialog.rs (100%) rename {src => crates/wef/src}/lib.rs (100%) rename {src => crates/wef/src}/query.rs (100%) rename {src => crates/wef/src}/sandbox_context.rs (100%) rename {src => crates/wef/src}/settings.rs (100%) rename {src => crates/wef/src}/wef.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index a84fc10..ba3b6c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,42 +1,18 @@ [workspace] members = [ - "tool", + "crates/wef", + "crates/cargo-wef", + "crates/webview", "examples/wef-winit", "examples/wef-example", ] -default-members = ["."] +default-members = ["crates/wef"] resolver = "2" -[package] -name = "wef" -version = "0.7.0" -edition = "2024" -authors = ["sunli "] -license = "Apache-2.0" -homepage = "https://github.com/longbridge/wef" -repository = "https://github.com/longbridge/wef" -description = "Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support." - -[build-dependencies] -cc = { version = "1.2.18", features = ["parallel"] } -pkg-config = "0.3.32" -dirs = "6.0.0" - -[dependencies] -image = { version = "0.25.6", default-features = false } -bitflags = "2.9.0" -thiserror = "2.0.12" -num_enum = "0.7.3" -mime = "0.3.17" -raw-window-handle = "0.6.2" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -tuple_len = "3.0.0" -futures-util = "0.3.31" - [workspace.dependencies] -wef = { path = "." } +wef = { path = "crates/wef" } +gpui-webview = { path = "crates/webview" } ropey = { version = "=2.0.0-beta.1", features = ["metric_utf16", "metric_lines_lf"] } anyhow = "1" @@ -73,9 +49,6 @@ todo = "deny" type_complexity = "allow" manual_is_multiple_of = "allow" -[lints] -workspace = true - [profile.dev] codegen-units = 16 debug = "limited" diff --git a/tool/Cargo.toml b/crates/cargo-wef/Cargo.toml similarity index 91% rename from tool/Cargo.toml rename to crates/cargo-wef/Cargo.toml index 73b0a17..b66c432 100644 --- a/tool/Cargo.toml +++ b/crates/cargo-wef/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" homepage = "https://github.com/longbridge/wef" repository = "https://github.com/longbridge/wef" description = "Cargo-wef is a command line tool for wef" -readme = "../README.md" +readme = "../../README.md" [dependencies] clap = { version = "4.5.38", features = ["derive", "env"] } @@ -25,4 +25,7 @@ dirs = "6.0.0" cargo_metadata = "0.20.0" serde_json = "1.0.140" image = "0.25.6" -icns = "0.3.1" \ No newline at end of file +icns = "0.3.1" + +[lints] +workspace = true \ No newline at end of file diff --git a/tool/src/commands/add_framework.rs b/crates/cargo-wef/src/commands/add_framework.rs similarity index 100% rename from tool/src/commands/add_framework.rs rename to crates/cargo-wef/src/commands/add_framework.rs diff --git a/tool/src/commands/build.rs b/crates/cargo-wef/src/commands/build.rs similarity index 100% rename from tool/src/commands/build.rs rename to crates/cargo-wef/src/commands/build.rs diff --git a/tool/src/commands/init.rs b/crates/cargo-wef/src/commands/init.rs similarity index 100% rename from tool/src/commands/init.rs rename to crates/cargo-wef/src/commands/init.rs diff --git a/tool/src/commands/mod.rs b/crates/cargo-wef/src/commands/mod.rs similarity index 100% rename from tool/src/commands/mod.rs rename to crates/cargo-wef/src/commands/mod.rs diff --git a/tool/src/commands/run.rs b/crates/cargo-wef/src/commands/run.rs similarity index 100% rename from tool/src/commands/run.rs rename to crates/cargo-wef/src/commands/run.rs diff --git a/tool/src/internal/add_cef_framework.rs b/crates/cargo-wef/src/internal/add_cef_framework.rs similarity index 100% rename from tool/src/internal/add_cef_framework.rs rename to crates/cargo-wef/src/internal/add_cef_framework.rs diff --git a/tool/src/internal/add_helper.rs b/crates/cargo-wef/src/internal/add_helper.rs similarity index 100% rename from tool/src/internal/add_helper.rs rename to crates/cargo-wef/src/internal/add_helper.rs diff --git a/tool/src/internal/cef_platform.rs b/crates/cargo-wef/src/internal/cef_platform.rs similarity index 100% rename from tool/src/internal/cef_platform.rs rename to crates/cargo-wef/src/internal/cef_platform.rs diff --git a/tool/src/internal/download_cef.rs b/crates/cargo-wef/src/internal/download_cef.rs similarity index 100% rename from tool/src/internal/download_cef.rs rename to crates/cargo-wef/src/internal/download_cef.rs diff --git a/tool/src/internal/find_cef_root.rs b/crates/cargo-wef/src/internal/find_cef_root.rs similarity index 100% rename from tool/src/internal/find_cef_root.rs rename to crates/cargo-wef/src/internal/find_cef_root.rs diff --git a/tool/src/internal/mod.rs b/crates/cargo-wef/src/internal/mod.rs similarity index 100% rename from tool/src/internal/mod.rs rename to crates/cargo-wef/src/internal/mod.rs diff --git a/tool/src/internal/plist.rs b/crates/cargo-wef/src/internal/plist.rs similarity index 100% rename from tool/src/internal/plist.rs rename to crates/cargo-wef/src/internal/plist.rs diff --git a/tool/src/main.rs b/crates/cargo-wef/src/main.rs similarity index 100% rename from tool/src/main.rs rename to crates/cargo-wef/src/main.rs diff --git a/crates/webview/.rustfmt.toml b/crates/webview/.rustfmt.toml new file mode 100644 index 0000000..f87905e --- /dev/null +++ b/crates/webview/.rustfmt.toml @@ -0,0 +1,12 @@ +edition = "2021" +newline_style = "unix" +# comments +normalize_comments = true +wrap_comments = true +format_code_in_doc_comments = true +# imports +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +# report +#report_fixme="Unnumbered" +#report_todo="Unnumbered" diff --git a/crates/webview/Cargo.toml b/crates/webview/Cargo.toml new file mode 100644 index 0000000..be7cac6 --- /dev/null +++ b/crates/webview/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "gpui-webview" +version = "0.1.0" +edition = "2024" +authors = ["sunli "] +license = "Apache-2.0" +homepage = "https://github.com/longbridge/wef" +repository = "https://github.com/longbridge/wef" + +[dependencies] +wef = { path = "../wef" } +serde = { version = "1.0.219", features = ["derive"] } +raw-window-handle = "0.6.2" +rust-i18n = "3" + +image = { version = "0.25.6", default-features = false } + +[lints] +workspace = true diff --git a/crates/webview/LICENSE-APACHE b/crates/webview/LICENSE-APACHE new file mode 100644 index 0000000..3cc771d --- /dev/null +++ b/crates/webview/LICENSE-APACHE @@ -0,0 +1,191 @@ +Copyright 2024 - 2025 Longbridge + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/webview/README.md b/crates/webview/README.md new file mode 100644 index 0000000..8fda0a0 --- /dev/null +++ b/crates/webview/README.md @@ -0,0 +1,16 @@ +# WebView support for GPUI + +## Bootstrap + +You should init the Wef first. + +```bash +cargo install cargo-wef +cargo wef init +``` + +## Run example + +```bash +cargo wef run -p wef-example +``` diff --git a/crates/webview/src/browser_handler.rs b/crates/webview/src/browser_handler.rs new file mode 100644 index 0000000..be69234 --- /dev/null +++ b/crates/webview/src/browser_handler.rs @@ -0,0 +1,339 @@ +use std::{rc::Rc, sync::Arc}; + +use gpui::{AnyWindowHandle, AppContext, AsyncApp, ParentElement, RenderImage, Styled, WeakEntity}; +use gpui_component::{ + ContextModal, + input::{InputState, TextInput}, + v_flex, +}; +use wef::{ + BrowserHandler, ContextMenuParams, CursorInfo, CursorType, DirtyRects, Frame, ImageBuffer, + JsDialogCallback, JsDialogType, LogSeverity, LogicalUnit, PaintElementType, Point, Rect, +}; + +use crate::{ + WebView, + context_menu::{ContextMenuInfo, build_context_menu}, + events::*, + frame_view::FrameView, + utils::from_wef_cursor_type, +}; + +/// A Handler implementation for the WebView. +pub(crate) struct WebViewHandler { + window_handle: AnyWindowHandle, + entity: WeakEntity, + cx: AsyncApp, +} + +impl WebViewHandler { + pub(crate) fn new( + window_handle: AnyWindowHandle, + entity: WeakEntity, + cx: AsyncApp, + ) -> Self { + Self { + window_handle, + entity, + cx, + } + } +} + +impl BrowserHandler for WebViewHandler { + fn on_popup_show(&mut self, show: bool) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |webview, _cx| { + webview.popup = show.then(FrameView::default); + }); + } + } + + fn on_popup_position(&mut self, rect: Rect>) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |webview, _cx| { + webview.popup_rect = Some(rect); + }); + } + } + + fn on_created(&mut self) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_webview, cx| { + cx.emit(CreatedEvent); + }); + } + } + + fn on_paint( + &mut self, + type_: PaintElementType, + _dirty_rects: &DirtyRects, + image_buffer: ImageBuffer, + ) { + let image = Arc::new(RenderImage::new([image::Frame::new( + image::ImageBuffer::from_vec( + image_buffer.width(), + image_buffer.height(), + image_buffer.to_vec(), + ) + .unwrap(), + )])); + + _ = self.entity.update(&mut self.cx, |webview, cx| { + match type_ { + PaintElementType::View => webview.main.update(image), + PaintElementType::Popup => { + if let Some(popup_frame) = &mut webview.popup { + popup_frame.update(image); + } + } + } + cx.notify(); + }); + } + + fn on_address_changed(&mut self, frame: Frame, url: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(AddressChangedEvent { + frame, + url: url.to_string(), + }); + }); + } + } + + fn on_title_changed(&mut self, title: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(TitleChangedEvent { + title: title.to_string(), + }); + }); + } + } + + fn on_tooltip(&mut self, text: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(TooltipEvent { + text: text.to_string(), + }); + }); + } + } + + fn on_status_message(&mut self, text: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(StatusMessageEvent { + text: text.to_string(), + }); + }); + } + } + + fn on_console_message( + &mut self, + message: &str, + level: LogSeverity, + source: &str, + line_number: i32, + ) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(ConsoleMessageEvent { + message: message.to_string(), + level, + source: source.to_string(), + line_number, + }); + }); + } + } + + fn on_before_popup(&mut self, url: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(BeforePopupEvent { + url: url.to_string(), + }); + }); + } + } + + fn on_loading_progress_changed(&mut self, progress: f32) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(LoadingProgressChangedEvent { progress }); + }); + } + } + + fn on_loading_state_changed( + &mut self, + is_loading: bool, + can_go_back: bool, + can_go_forward: bool, + ) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(LoadingStateChangedEvent { + is_loading, + can_go_back, + can_go_forward, + }); + }); + } + } + + fn on_load_start(&mut self, frame: Frame) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(LoadStartEvent { frame }); + }); + } + } + + fn on_load_end(&mut self, frame: Frame) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(LoadEndEvent { frame }); + }); + } + } + + fn on_load_error(&mut self, frame: Frame, error_text: &str, failed_url: &str) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |_, cx| { + cx.emit(LoadErrorEvent { + frame, + error_text: error_text.to_string(), + failed_url: failed_url.to_string(), + }); + }); + } + } + + fn on_cursor_changed( + &mut self, + cursor_type: CursorType, + _cursor_info: Option, + ) -> bool { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_entity(&entity, |webview, cx| { + webview.cursor = from_wef_cursor_type(cursor_type); + cx.notify(); + }); + } + true + } + + fn on_context_menu(&mut self, frame: Frame, params: ContextMenuParams) { + if let Some(entity) = self.entity.upgrade() { + _ = self.cx.update_window(self.window_handle, |_, window, cx| { + cx.update_entity(&entity, |webview, cx| { + webview.context_menu = Some(ContextMenuInfo { + crood: Point::new( + LogicalUnit(params.crood.x.0 + webview.bounds.origin.x.0 as i32), + LogicalUnit(params.crood.y.0 + webview.bounds.origin.y.0 as i32), + ), + frame, + menu: build_context_menu(webview, ¶ms, window, cx), + link_url: params.link_url.map(ToString::to_string), + }); + cx.notify(); + }) + }); + } + } + + fn on_js_dialog( + &mut self, + type_: JsDialogType, + message_text: &str, + callback: JsDialogCallback, + ) -> bool { + _ = self.cx.update_window(self.window_handle, |_, window, cx| { + let message_text = message_text.to_string(); + let callback = Rc::new(callback); + + match type_ { + JsDialogType::Alert => { + window.open_modal(cx, move |modal, _, _| { + modal + .footer(|ok, _, window, cx| vec![ok(window, cx)]) + .child(message_text.clone()) + .on_ok({ + let callback = callback.clone(); + move |_, _, _| { + callback.continue_(true, None); + true + } + }) + }); + } + JsDialogType::Confirm => { + window.open_modal(cx, move |modal, _, _| { + modal + .footer(|ok, cancel, window, cx| { + vec![ok(window, cx), cancel(window, cx)] + }) + .child(message_text.clone()) + .on_ok({ + let callback = callback.clone(); + move |_, _, _| { + callback.continue_(true, None); + true + } + }) + .on_cancel({ + let callback = callback.clone(); + move |_, _, _| { + callback.continue_(false, None); + true + } + }) + }); + } + JsDialogType::Prompt { + default_prompt_text, + } => { + let default_prompt_text = default_prompt_text.to_string(); + let input_state = + cx.new(|cx| InputState::new(window, cx).default_value(default_prompt_text)); + window.open_modal(cx, move |modal, _, _| { + modal + .footer(move |ok, cancel, window, cx| { + vec![ok(window, cx), cancel(window, cx)] + }) + .child( + v_flex() + .gap_3() + .child(message_text.clone()) + .child(TextInput::new(&input_state)), + ) + .on_ok({ + let callback = callback.clone(); + let input_state = input_state.clone(); + move |_, _, cx| { + callback.continue_(true, Some(&input_state.read(cx).value())); + true + } + }) + .on_cancel({ + let callback = callback.clone(); + move |_, _, _| { + callback.continue_(false, None); + true + } + }) + }); + } + } + }); + + true + } +} diff --git a/crates/webview/src/context_menu.rs b/crates/webview/src/context_menu.rs new file mode 100644 index 0000000..cbac8d6 --- /dev/null +++ b/crates/webview/src/context_menu.rs @@ -0,0 +1,117 @@ +use gpui::{Action, App, Entity, Window}; +use gpui_component::popup_menu::PopupMenu; +use rust_i18n::t; +use schemars::JsonSchema; +use serde::Deserialize; +use wef::{ContextMenuParams, Frame, LogicalUnit, Point}; + +use crate::WebView; + +#[derive(Action, Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)] +#[action(namespace = webview)] +pub(crate) enum ContextMenuAction { + CopyLinkAddress, + Undo, + Redo, + Cut, + Copy, + Paste, + ParseAsPlainText, + SelectAll, + GoBack, + GoForward, + Reload, +} + +pub(crate) struct ContextMenuInfo { + pub(crate) crood: Point>, + pub(crate) frame: Frame, + pub(crate) menu: Entity, + pub(crate) link_url: Option, +} + +pub(crate) fn build_context_menu( + webview: &WebView, + params: &ContextMenuParams, + window: &mut Window, + cx: &mut App, +) -> Entity { + use wef::{ContextMenuEditStateFlags as EditStateFlags, ContextMenuTypeFlags as TypeFlags}; + + PopupMenu::build(window, cx, |mut popmenu, _window, cx| { + if params.type_.contains(TypeFlags::SELECTION) { + popmenu = popmenu.menu( + t!("WebView.ContextMenu.Copy"), + Box::new(ContextMenuAction::Copy), + ); + } + + if params.type_.contains(TypeFlags::LINK) { + popmenu = popmenu.menu( + t!("WebView.ContextMenu.CopyLinkAddress"), + Box::new(ContextMenuAction::CopyLinkAddress), + ); + } else if params.type_.contains(TypeFlags::EDITABLE) { + popmenu = popmenu + .menu_with_disabled( + t!("WebView.ContextMenu.Undo"), + Box::new(ContextMenuAction::Undo), + !params.edit_state_flags.contains(EditStateFlags::CAN_UNDO), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.Redo"), + Box::new(ContextMenuAction::Redo), + !params.edit_state_flags.contains(EditStateFlags::CAN_REDO), + ) + .separator() + .menu_with_disabled( + t!("WebView.ContextMenu.Cut"), + Box::new(ContextMenuAction::Cut), + !params.edit_state_flags.contains(EditStateFlags::CAN_CUT), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.Copy"), + Box::new(ContextMenuAction::Copy), + !params.edit_state_flags.contains(EditStateFlags::CAN_COPY), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.Paste"), + Box::new(ContextMenuAction::Paste), + !params.edit_state_flags.contains(EditStateFlags::CAN_PASTE), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.ParseAsPlainText"), + Box::new(ContextMenuAction::ParseAsPlainText), + !params + .edit_state_flags + .contains(EditStateFlags::CAN_EDIT_RICHLY), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.SelectAll"), + Box::new(ContextMenuAction::SelectAll), + !params + .edit_state_flags + .contains(EditStateFlags::CAN_SELECT_ALL), + ); + } else if params.type_.contains(TypeFlags::PAGE) { + popmenu = popmenu + .menu_with_disabled( + t!("WebView.ContextMenu.Back"), + Box::new(ContextMenuAction::GoBack), + !webview.browser().can_back(), + ) + .menu_with_disabled( + t!("WebView.ContextMenu.Forward"), + Box::new(ContextMenuAction::GoForward), + !webview.browser().can_forward(), + ) + .menu( + t!("WebView.ContextMenu.Reload"), + Box::new(ContextMenuAction::Reload), + ) + } + + cx.notify(); + popmenu + }) +} diff --git a/crates/webview/src/element.rs b/crates/webview/src/element.rs new file mode 100644 index 0000000..6ff4ad7 --- /dev/null +++ b/crates/webview/src/element.rs @@ -0,0 +1,246 @@ +use std::{panic::Location, rc::Rc, sync::Arc}; + +use gpui::{ + App, Bounds, BoxShadow, Corners, DispatchPhase, Element, ElementInputHandler, Entity, + FocusHandle, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, + IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, RenderImage, Size, + StyleRefinement, Styled, Window, hsla, point, px, size, +}; +use wef::{Browser, LogicalUnit, Rect}; + +use crate::{WebView, utils::*}; + +pub(crate) struct WebViewElement { + webview: Entity, + focus_handle: FocusHandle, + browser: Rc, + interactivity: Interactivity, + view_image: Arc, + popup_image: Option<(Rect>, Arc)>, +} + +impl WebViewElement { + pub(crate) fn new( + webview: Entity, + focus_handle: FocusHandle, + browser: Rc, + view_image: Arc, + popup_image: Option<(Rect>, Arc)>, + ) -> Self { + Self { + webview, + focus_handle, + browser, + interactivity: Interactivity::default(), + view_image, + popup_image, + } + } +} + +impl IntoElement for WebViewElement { + type Element = WebViewElement; + + #[inline] + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for WebViewElement { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for WebViewElement { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl Element for WebViewElement { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| window.request_layout(style, None, cx), + ); + (layout_id, ()) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + self.webview.update(cx, |webview, _| { + webview.bounds = bounds; + }); + + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_, _, hit_box, _, _| hit_box, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.handle_input( + &self.focus_handle, + ElementInputHandler::new(bounds, self.webview.clone()), + cx, + ); + + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |_, window, _cx| { + let scale_factor = window.scale_factor(); + self.browser.resize(wef::Size::new( + wef::PhysicalUnit((bounds.size.width.0 * scale_factor) as i32), + wef::PhysicalUnit((bounds.size.height.0 * scale_factor) as i32), + )); + + let image_size = self.view_image.size(0); + _ = window.paint_image( + Bounds::new( + bounds.origin, + Size::new( + px(image_size.width.0 as f32 / scale_factor), + px(image_size.height.0 as f32 / scale_factor), + ), + ), + Corners::all(px(0.0)), + self.view_image.clone(), + 0, + false, + ); + + if let Some((rect, image)) = &self.popup_image { + let bounds = Bounds::new( + point(px(rect.x.0 as f32), px(rect.y.0 as f32)), + size(px(rect.width.0 as f32), px(rect.height.0 as f32)), + ) + bounds.origin; + + let shadows = &[ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(10.)), + blur_radius: px(15.), + spread_radius: px(-3.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(4.)), + blur_radius: px(6.), + spread_radius: px(-4.), + }, + ]; + window.paint_shadows(bounds, Corners::all(px(0.0)), shadows); + + _ = window.paint_image(bounds, Corners::all(px(0.0)), image.clone(), 0, false); + } + }, + ); + + let cursor_style = self.webview.read(cx).cursor; + window.set_cursor_style(cursor_style, hitbox.as_ref().unwrap()); + + window.on_mouse_event({ + let entity = self.webview.clone(); + move |event: &MouseMoveEvent, phase, _, cx| { + let webview = entity.read(cx); + if phase == DispatchPhase::Bubble + && (event.dragging() || bounds.contains(&event.position)) + { + let position = event.position - bounds.origin; + webview.browser().send_mouse_move_event( + wef::Point::new( + wef::LogicalUnit(position.x.0 as i32), + wef::LogicalUnit(position.y.0 as i32), + ), + to_wef_key_modifiers(&event.modifiers), + ); + } + } + }); + + window.on_mouse_event({ + let entity = self.webview.clone(); + move |event: &MouseDownEvent, phase, _, cx| { + let webview = entity.read(cx); + if phase == DispatchPhase::Bubble && bounds.contains(&event.position) { + if let Some(mouse_button) = to_wef_mouse_button(event.button) { + let modifiers = to_wef_key_modifiers(&event.modifiers); + webview.browser().send_mouse_click_event( + mouse_button, + false, + event.click_count, + modifiers, + ); + webview.browser().set_focus(true); + } + } + } + }); + + window.on_mouse_event({ + let entity = self.webview.clone(); + move |event: &MouseUpEvent, phase, _, cx| { + let webview = entity.read(cx); + if phase == DispatchPhase::Bubble && bounds.contains(&event.position) { + if let Some(mouse_button) = to_wef_mouse_button(event.button) { + let modifiers = to_wef_key_modifiers(&event.modifiers); + webview.browser().send_mouse_click_event( + mouse_button, + true, + event.click_count, + modifiers, + ); + } + } + } + }); + } +} diff --git a/crates/webview/src/events.rs b/crates/webview/src/events.rs new file mode 100644 index 0000000..de0a979 --- /dev/null +++ b/crates/webview/src/events.rs @@ -0,0 +1,100 @@ +//! Events for the WebView. + +use wef::{Frame, LogSeverity}; + +/// Emitted when the browser is created. +#[derive(Debug)] +pub struct CreatedEvent; + +/// Emitted when the address of the frame changes. +#[derive(Debug)] +pub struct AddressChangedEvent { + /// The frame object. + pub frame: Frame, + /// The new URL. + pub url: String, +} + +/// Emitted when the title changes. +#[derive(Debug)] +pub struct TitleChangedEvent { + /// The new title. + pub title: String, +} + +/// Emitted when the browser is about to display a tooltip. +#[derive(Debug)] +pub struct TooltipEvent { + /// The tooltip text. + pub text: String, +} + +/// Emitted when the browser receives a status message. +#[derive(Debug)] +pub struct StatusMessageEvent { + /// The status message text. + pub text: String, +} + +/// Emitted when the browser receives a console message. +#[derive(Debug)] +pub struct ConsoleMessageEvent { + /// The console message text. + pub message: String, + /// The log level. + pub level: LogSeverity, + /// The source code file where the message is sent. + pub source: String, + /// The line number in the source code file. + pub line_number: i32, +} + +/// Emitted when preparing to open a popup browser window. +#[derive(Debug)] +pub struct BeforePopupEvent { + /// The URL of the popup window. + pub url: String, +} + +/// Emitted when the overall page loading progress changes. +#[derive(Debug)] +pub struct LoadingProgressChangedEvent { + /// Ranges from 0.0 to 1.0. + pub progress: f32, +} + +/// Emitted when the loading state changes. +#[derive(Debug)] +pub struct LoadingStateChangedEvent { + /// Whether the browser is loading a page. + pub is_loading: bool, + /// Whether the browser can go back in history. + pub can_go_back: bool, + /// Whether the browser can go forward in history. + pub can_go_forward: bool, +} + +/// Emitted when the browser starts loading a page. +#[derive(Debug)] +pub struct LoadStartEvent { + /// The frame object. + pub frame: Frame, +} + +/// Emitted when the browser finishes loading a page. +#[derive(Debug)] +pub struct LoadEndEvent { + /// The frame object. + pub frame: Frame, +} + +/// Emitted when the browser fails to load a page. +#[derive(Debug)] +pub struct LoadErrorEvent { + /// The frame object. + pub frame: Frame, + /// The error text. + pub error_text: String, + /// The uRL that failed to load. + pub failed_url: String, +} diff --git a/crates/webview/src/frame_view.rs b/crates/webview/src/frame_view.rs new file mode 100644 index 0000000..3a5f170 --- /dev/null +++ b/crates/webview/src/frame_view.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use gpui::{RenderImage, Window}; + +#[derive(Debug, Default)] +pub(crate) struct FrameView { + id: usize, + rendered_frame: Option<(usize, Arc)>, + frame: Option<(usize, Arc)>, +} + +impl FrameView { + pub(crate) fn render(&mut self, window: &mut Window) -> Option> { + let (current_frame_id, image) = self.frame.clone()?; + + if let Some((rendered_frame_id, rendered_image)) = self.rendered_frame.take() { + if rendered_frame_id != current_frame_id { + _ = window.drop_image(rendered_image); + } + self.rendered_frame = None; + } + + self.rendered_frame = self.frame.clone(); + Some(image) + } + + pub(crate) fn update(&mut self, image: Arc) { + self.frame = Some((self.id, image)); + self.id += 1; + } + + pub(crate) fn clear(&mut self, window: &mut Window) { + if let Some((_, image)) = self.rendered_frame.take() { + _ = window.drop_image(image); + } + } +} diff --git a/crates/webview/src/lib.rs b/crates/webview/src/lib.rs new file mode 100644 index 0000000..62f16d6 --- /dev/null +++ b/crates/webview/src/lib.rs @@ -0,0 +1,15 @@ +#![doc = include_str!("../README.md")] + +mod browser_handler; +mod context_menu; +mod element; +mod frame_view; +mod utils; +mod webview; + +pub mod events; + +pub use webview::WebView; +pub use wef; + +rust_i18n::i18n!("locales", fallback = "en"); diff --git a/crates/webview/src/utils.rs b/crates/webview/src/utils.rs new file mode 100644 index 0000000..e1eebf5 --- /dev/null +++ b/crates/webview/src/utils.rs @@ -0,0 +1,77 @@ +use gpui::{CursorStyle, Modifiers, MouseButton}; + +pub(crate) fn to_wef_mouse_button(button: MouseButton) -> Option { + Some(match button { + MouseButton::Left => wef::MouseButton::Left, + MouseButton::Middle => wef::MouseButton::Middle, + MouseButton::Right => wef::MouseButton::Right, + _ => return None, + }) +} + +pub(crate) fn to_wef_key_modifiers(modifiers: &Modifiers) -> wef::KeyModifier { + let mut wef_modifiers = wef::KeyModifier::empty(); + if modifiers.shift { + wef_modifiers |= wef::KeyModifier::SHIFT; + } + if modifiers.control { + wef_modifiers |= wef::KeyModifier::CONTROL; + } + if modifiers.alt { + wef_modifiers |= wef::KeyModifier::ALT; + } + wef_modifiers +} + +pub(crate) fn to_wef_key_code(key_code: &str) -> Option { + Some(match key_code { + "backspace" => wef::KeyCode::Backspace, + "delete" => wef::KeyCode::Delete, + "tab" => wef::KeyCode::Tab, + "enter" => wef::KeyCode::Enter, + "pageup" => wef::KeyCode::PageUp, + "pagedown" => wef::KeyCode::PageDown, + "end" => wef::KeyCode::End, + "home" => wef::KeyCode::Home, + "left" => wef::KeyCode::ArrowLeft, + "up" => wef::KeyCode::ArrowUp, + "right" => wef::KeyCode::ArrowRight, + "down" => wef::KeyCode::ArrowDown, + _ => return None, + }) +} + +pub(crate) fn from_wef_cursor_type(cursor: wef::CursorType) -> CursorStyle { + use wef::CursorType::*; + + match cursor { + Cross => CursorStyle::Crosshair, + Hand => CursorStyle::PointingHand, + IBeam => CursorStyle::IBeam, + EastResize => CursorStyle::ResizeRight, + NorthResize => CursorStyle::ResizeUp, + NorthEastResize => CursorStyle::ResizeUpRightDownLeft, + NorthWestResize => CursorStyle::ResizeUpLeftDownRight, + SouthResize => CursorStyle::ResizeDown, + SouthEastResize => CursorStyle::ResizeUpRightDownLeft, + SouthWestResize => CursorStyle::ResizeUpLeftDownRight, + WestResize => CursorStyle::ResizeLeft, + NorthSouthResize => CursorStyle::ResizeUpDown, + EastWestResize => CursorStyle::ResizeLeftRight, + NorthEastSouthWestResize => CursorStyle::ResizeUpRightDownLeft, + NorthWestSouthEastResize => CursorStyle::ResizeUpLeftDownRight, + ColumnResize => CursorStyle::ResizeColumn, + RowResize => CursorStyle::ResizeRow, + ContextMenu => CursorStyle::ContextualMenu, + NoDrop => CursorStyle::OperationNotAllowed, + Copy => CursorStyle::DragCopy, + None => CursorStyle::None, + NotAllowed => CursorStyle::OperationNotAllowed, + Grab => CursorStyle::OpenHand, + Grabbing => CursorStyle::ClosedHand, + DndNone => CursorStyle::None, + DndCopy => CursorStyle::DragCopy, + DndLink => CursorStyle::DragLink, + _ => CursorStyle::Arrow, + } +} diff --git a/crates/webview/src/webview.rs b/crates/webview/src/webview.rs new file mode 100644 index 0000000..95401a6 --- /dev/null +++ b/crates/webview/src/webview.rs @@ -0,0 +1,350 @@ +use std::{ops::Range, rc::Rc}; + +use gpui::{ + App, Bounds, ClipboardItem, CursorStyle, Empty, Entity, EntityInputHandler, EventEmitter, + FocusHandle, Focusable, KeyDownEvent, KeyUpEvent, MouseDownEvent, Pixels, ScrollWheelEvent, + Subscription, UTF16Selection, WeakEntity, Window, anchored, deferred, div, point, prelude::*, + px, +}; +use wef::{Browser, FuncRegistry, LogicalUnit, Point, Rect}; + +use crate::{ + browser_handler::WebViewHandler, + context_menu::{ContextMenuAction, ContextMenuInfo}, + element::WebViewElement, + events::*, + frame_view::FrameView, + utils::*, +}; + +/// A web view based on the Chromium Embedded Framework (CEF). +pub struct WebView { + pub(crate) main: FrameView, + pub(crate) popup: Option, + pub(crate) popup_rect: Option>>, + pub(crate) cursor: CursorStyle, + pub(crate) context_menu: Option, + pub(crate) bounds: Bounds, + focus_handle: FocusHandle, + browser: Rc, + _subscriptions: Vec, +} + +impl WebView { + /// Creates a new `WebView` instance with the given URL. + pub fn new(url: &str, window: &mut Window, cx: &mut App) -> Entity { + Self::with_func_registry(url, FuncRegistry::default(), window, cx) + } + + /// Creates a new `WebView` instance with the given URL and function + /// registry. + pub fn with_func_registry( + url: &str, + function_registry: FuncRegistry, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let window_handle = window.window_handle(); + let entity = cx.new(|cx| { + let entity = cx.entity(); + + let browser = Rc::new( + Browser::builder() + .parent( + raw_window_handle::HasWindowHandle::window_handle(window) + .ok() + .map(|handle| handle.as_raw()), + ) + .device_scale_factor(window.scale_factor()) + .url(url) + .handler(WebViewHandler::new( + window_handle, + entity.downgrade(), + cx.to_async(), + )) + .func_registry(function_registry) + .build(), + ); + + let focus_handle = cx.focus_handle(); + + let _subscriptions = vec![ + cx.on_focus(&focus_handle, window, Self::on_focus), + cx.on_blur(&focus_handle, window, Self::on_blur), + ]; + + Self { + focus_handle, + main: FrameView::default(), + popup: None, + popup_rect: None, + browser, + cursor: CursorStyle::Arrow, + context_menu: None, + bounds: Bounds::default(), + _subscriptions, + } + }); + + cx.observe_release(&entity, |webview, cx| { + for window in cx.windows() { + _ = cx.update_window(window, |_, window, _cx| { + webview.main.clear(window); + if let Some(popup_frame) = &mut webview.popup { + popup_frame.clear(window); + } + }); + } + }) + .detach(); + + entity + } + + /// Returns the browser instance. + #[inline] + pub fn browser(&self) -> &Rc { + &self.browser + } + + fn scroll_wheel_handler( + &mut self, + event: &ScrollWheelEvent, + _window: &mut Window, + _cx: &mut Context, + ) { + let (delta_x, delta_y) = match event.delta { + gpui::ScrollDelta::Pixels(point) => (point.x.0, point.y.0), + gpui::ScrollDelta::Lines(point) => (point.x * 20.0, point.y * 20.0), + }; + self.browser().send_mouse_wheel_event(Point::new( + LogicalUnit(delta_x as i32), + LogicalUnit(delta_y as i32), + )); + } + + fn keydown_handler( + &mut self, + event: &KeyDownEvent, + _window: &mut Window, + _cx: &mut Context, + ) { + let modifiers = to_wef_key_modifiers(&event.keystroke.modifiers); + if let Some(key_code) = to_wef_key_code(&event.keystroke.key) { + self.browser().send_key_event(true, key_code, modifiers); + }; + } + + fn keyup_handler(&mut self, event: &KeyUpEvent, _window: &mut Window, _cx: &mut Context) { + let modifiers = to_wef_key_modifiers(&event.keystroke.modifiers); + let Some(key_code) = to_wef_key_code(&event.keystroke.key) else { + return; + }; + self.browser().send_key_event(false, key_code, modifiers); + } + + fn on_focus(&mut self, _window: &mut Window, _cx: &mut Context) { + self.browser().set_focus(true); + } + + fn on_blur(&mut self, _window: &mut Window, _cx: &mut Context) { + self.browser().set_focus(false); + } + + fn on_context_menu_action( + &mut self, + action: &ContextMenuAction, + _window: &mut Window, + cx: &mut Context, + ) { + use ContextMenuAction::*; + + if let Some(info) = self.context_menu.take() { + let action = *action; + + cx.spawn(async move |webview: WeakEntity, cx| { + let Ok(browser) = webview.read_with(cx, |webview, _cx| webview.browser().clone()) + else { + return; + }; + match action { + CopyLinkAddress => { + if let Some(link_url) = &info.link_url { + _ = cx.update(|cx| { + cx.write_to_clipboard(ClipboardItem::new_string(link_url.clone())) + }); + } + } + Undo => info.frame.undo(), + Redo => info.frame.redo(), + Cut => info.frame.cut(), + Copy => info.frame.copy(), + Paste => info.frame.paste(), + ParseAsPlainText => info.frame.paste_and_match_style(), + SelectAll => info.frame.select_all(), + GoBack => browser.back(), + GoForward => browser.forward(), + Reload => browser.reload(), + } + }) + .detach(); + } + } + + fn context_menu_mousedown_out_handler( + &mut self, + _event: &MouseDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.context_menu = None; + cx.notify(); + } +} + +impl Focusable for WebView { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for WebView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(image) = self.main.render(window) else { + return Empty.into_any_element(); + }; + + let mut root = div().size_full().child( + WebViewElement::new( + cx.entity(), + self.focus_handle.clone(), + self.browser.clone(), + image.clone(), + self.popup_rect + .zip(self.popup.as_mut().and_then(|f| f.render(window))), + ) + .size_full() + .track_focus(&self.focus_handle) + .on_scroll_wheel(cx.listener(Self::scroll_wheel_handler)) + .on_key_down(cx.listener(Self::keydown_handler)) + .on_key_up(cx.listener(Self::keyup_handler)), + ); + + if let Some(info) = &self.context_menu { + root = root.child(deferred( + anchored() + .position(point(px(info.crood.x.0 as f32), px(info.crood.y.0 as f32))) + .child( + div() + .child(info.menu.clone()) + .shadow_2xl() + .on_mouse_down_out( + cx.listener(Self::context_menu_mousedown_out_handler), + ), + ), + )); + } + + root.on_action(cx.listener(Self::on_context_menu_action)) + .into_any_element() + } +} + +impl EntityInputHandler for WebView { + fn text_for_range( + &mut self, + _range: Range, + _adjusted_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn replace_text_in_range( + &mut self, + _range: Option>, + text: &str, + _window: &mut Window, + _cx: &mut Context, + ) { + self.browser().ime_commit(text); + } + + fn replace_and_mark_text_in_range( + &mut self, + _range: Option>, + new_text: &str, + new_selected_range: Option>, + _window: &mut Window, + _cx: &mut Context, + ) { + let new_selected_range = new_selected_range.unwrap_or_default(); + self.browser().ime_set_composition( + new_text, + new_selected_range.start, + new_selected_range.end, + ); + } + + fn bounds_for_range( + &mut self, + _range_utf16: Range, + _element_bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _point: gpui::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } +} + +macro_rules! impl_emiter { + ($($ty:ty),*) => { + $( + impl EventEmitter<$ty> for WebView {} + )* + }; +} + +impl_emiter!( + LoadingProgressChangedEvent, + CreatedEvent, + AddressChangedEvent, + TitleChangedEvent, + TooltipEvent, + StatusMessageEvent, + ConsoleMessageEvent, + BeforePopupEvent, + LoadingStateChangedEvent, + LoadStartEvent, + LoadEndEvent, + LoadErrorEvent +); diff --git a/crates/webview/tests/index.html b/crates/webview/tests/index.html new file mode 100644 index 0000000..6bd541c --- /dev/null +++ b/crates/webview/tests/index.html @@ -0,0 +1,48 @@ + + + + + + Test Cases + + + +

Test Cases

+
+ + diff --git a/crates/webview/tests/input.html b/crates/webview/tests/input.html new file mode 100644 index 0000000..8419be7 --- /dev/null +++ b/crates/webview/tests/input.html @@ -0,0 +1,83 @@ + + + + + + Text Input + + + +

Text Input

+
+ + + + + + + +
+

+ + + + diff --git a/crates/webview/tests/jsbridge.html b/crates/webview/tests/jsbridge.html new file mode 100644 index 0000000..642916a --- /dev/null +++ b/crates/webview/tests/jsbridge.html @@ -0,0 +1,103 @@ + + + + + + JSBridge + + + +

JSBridge

+
+

Call Native Functions

+ + + + + + +
+

+ + + + diff --git a/crates/webview/tests/jsdialog.html b/crates/webview/tests/jsdialog.html new file mode 100644 index 0000000..ce931cf --- /dev/null +++ b/crates/webview/tests/jsdialog.html @@ -0,0 +1,33 @@ + + + + + + JS Dialog + + + +

JavaScript Dialog

+ + + + + diff --git a/crates/webview/tests/upload.html b/crates/webview/tests/upload.html new file mode 100644 index 0000000..503b032 --- /dev/null +++ b/crates/webview/tests/upload.html @@ -0,0 +1,69 @@ + + + + + + File Upload + + + +

File Upload

+
+
+
+ +
+

+ + + + diff --git a/.rustfmt.toml b/crates/wef/.rustfmt.toml similarity index 100% rename from .rustfmt.toml rename to crates/wef/.rustfmt.toml diff --git a/crates/wef/Cargo.toml b/crates/wef/Cargo.toml new file mode 100644 index 0000000..c9c7120 --- /dev/null +++ b/crates/wef/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "wef" +version = "0.7.0" +edition = "2024" +authors = ["sunli "] +license = "Apache-2.0" +homepage = "https://github.com/longbridge/wef" +repository = "https://github.com/longbridge/wef" +description = "Wef is a Rust library for embedding WebView functionality using Chromium Embedded Framework (CEF3) with offscreen rendering support." + +[build-dependencies] +cc = { version = "1.2.18", features = ["parallel"] } +pkg-config = "0.3.32" +dirs = "6.0.0" + +[dependencies] +image = { version = "0.25.6", default-features = false } +bitflags = "2.9.0" +thiserror = "2.0.12" +num_enum = "0.7.3" +mime = "0.3.17" +raw-window-handle = "0.6.2" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tuple_len = "3.0.0" +futures-util = "0.3.31" + +[lints] +workspace = true \ No newline at end of file diff --git a/LICENSE-APACHE b/crates/wef/LICENSE-APACHE similarity index 100% rename from LICENSE-APACHE rename to crates/wef/LICENSE-APACHE diff --git a/build.rs b/crates/wef/build.rs similarity index 100% rename from build.rs rename to crates/wef/build.rs diff --git a/cpp/app.h b/crates/wef/cpp/app.h similarity index 100% rename from cpp/app.h rename to crates/wef/cpp/app.h diff --git a/cpp/app_callbacks.h b/crates/wef/cpp/app_callbacks.h similarity index 100% rename from cpp/app_callbacks.h rename to crates/wef/cpp/app_callbacks.h diff --git a/cpp/app_render_process.h b/crates/wef/cpp/app_render_process.h similarity index 100% rename from cpp/app_render_process.h rename to crates/wef/cpp/app_render_process.h diff --git a/cpp/browser_callbacks.h b/crates/wef/cpp/browser_callbacks.h similarity index 100% rename from cpp/browser_callbacks.h rename to crates/wef/cpp/browser_callbacks.h diff --git a/cpp/client.cpp b/crates/wef/cpp/client.cpp similarity index 100% rename from cpp/client.cpp rename to crates/wef/cpp/client.cpp diff --git a/cpp/client.h b/crates/wef/cpp/client.h similarity index 100% rename from cpp/client.h rename to crates/wef/cpp/client.h diff --git a/cpp/cursor.cpp b/crates/wef/cpp/cursor.cpp similarity index 100% rename from cpp/cursor.cpp rename to crates/wef/cpp/cursor.cpp diff --git a/cpp/dirty_rect.cpp b/crates/wef/cpp/dirty_rect.cpp similarity index 100% rename from cpp/dirty_rect.cpp rename to crates/wef/cpp/dirty_rect.cpp diff --git a/cpp/external_pump.cpp b/crates/wef/cpp/external_pump.cpp similarity index 100% rename from cpp/external_pump.cpp rename to crates/wef/cpp/external_pump.cpp diff --git a/cpp/external_pump.h b/crates/wef/cpp/external_pump.h similarity index 100% rename from cpp/external_pump.h rename to crates/wef/cpp/external_pump.h diff --git a/cpp/external_pump_linux.cpp b/crates/wef/cpp/external_pump_linux.cpp similarity index 100% rename from cpp/external_pump_linux.cpp rename to crates/wef/cpp/external_pump_linux.cpp diff --git a/cpp/external_pump_mac.mm b/crates/wef/cpp/external_pump_mac.mm similarity index 100% rename from cpp/external_pump_mac.mm rename to crates/wef/cpp/external_pump_mac.mm diff --git a/cpp/external_pump_win.cpp b/crates/wef/cpp/external_pump_win.cpp similarity index 100% rename from cpp/external_pump_win.cpp rename to crates/wef/cpp/external_pump_win.cpp diff --git a/cpp/file_dialog.cpp b/crates/wef/cpp/file_dialog.cpp similarity index 100% rename from cpp/file_dialog.cpp rename to crates/wef/cpp/file_dialog.cpp diff --git a/cpp/frame.cpp b/crates/wef/cpp/frame.cpp similarity index 100% rename from cpp/frame.cpp rename to crates/wef/cpp/frame.cpp diff --git a/cpp/frame.h b/crates/wef/cpp/frame.h similarity index 100% rename from cpp/frame.h rename to crates/wef/cpp/frame.h diff --git a/cpp/js_dialog.cpp b/crates/wef/cpp/js_dialog.cpp similarity index 100% rename from cpp/js_dialog.cpp rename to crates/wef/cpp/js_dialog.cpp diff --git a/cpp/load_library.cpp b/crates/wef/cpp/load_library.cpp similarity index 100% rename from cpp/load_library.cpp rename to crates/wef/cpp/load_library.cpp diff --git a/cpp/query.cpp b/crates/wef/cpp/query.cpp similarity index 100% rename from cpp/query.cpp rename to crates/wef/cpp/query.cpp diff --git a/cpp/sandbox_context.cpp b/crates/wef/cpp/sandbox_context.cpp similarity index 100% rename from cpp/sandbox_context.cpp rename to crates/wef/cpp/sandbox_context.cpp diff --git a/cpp/utils.h b/crates/wef/cpp/utils.h similarity index 100% rename from cpp/utils.h rename to crates/wef/cpp/utils.h diff --git a/cpp/wef.cpp b/crates/wef/cpp/wef.cpp similarity index 100% rename from cpp/wef.cpp rename to crates/wef/cpp/wef.cpp diff --git a/src/app_handler.rs b/crates/wef/src/app_handler.rs similarity index 100% rename from src/app_handler.rs rename to crates/wef/src/app_handler.rs diff --git a/src/browser.rs b/crates/wef/src/browser.rs similarity index 100% rename from src/browser.rs rename to crates/wef/src/browser.rs diff --git a/src/browser_handler.rs b/crates/wef/src/browser_handler.rs similarity index 100% rename from src/browser_handler.rs rename to crates/wef/src/browser_handler.rs diff --git a/src/builder.rs b/crates/wef/src/builder.rs similarity index 100% rename from src/builder.rs rename to crates/wef/src/builder.rs diff --git a/src/context_menu.rs b/crates/wef/src/context_menu.rs similarity index 100% rename from src/context_menu.rs rename to crates/wef/src/context_menu.rs diff --git a/src/cursor.rs b/crates/wef/src/cursor.rs similarity index 100% rename from src/cursor.rs rename to crates/wef/src/cursor.rs diff --git a/src/dirty_rects.rs b/crates/wef/src/dirty_rects.rs similarity index 100% rename from src/dirty_rects.rs rename to crates/wef/src/dirty_rects.rs diff --git a/src/dpi.rs b/crates/wef/src/dpi.rs similarity index 100% rename from src/dpi.rs rename to crates/wef/src/dpi.rs diff --git a/src/error.rs b/crates/wef/src/error.rs similarity index 100% rename from src/error.rs rename to crates/wef/src/error.rs diff --git a/src/ffi.rs b/crates/wef/src/ffi.rs similarity index 100% rename from src/ffi.rs rename to crates/wef/src/ffi.rs diff --git a/src/file_dialog.rs b/crates/wef/src/file_dialog.rs similarity index 100% rename from src/file_dialog.rs rename to crates/wef/src/file_dialog.rs diff --git a/src/frame.rs b/crates/wef/src/frame.rs similarity index 100% rename from src/frame.rs rename to crates/wef/src/frame.rs diff --git a/src/framework_loader.rs b/crates/wef/src/framework_loader.rs similarity index 100% rename from src/framework_loader.rs rename to crates/wef/src/framework_loader.rs diff --git a/src/func_registry/async_function_type.rs b/crates/wef/src/func_registry/async_function_type.rs similarity index 100% rename from src/func_registry/async_function_type.rs rename to crates/wef/src/func_registry/async_function_type.rs diff --git a/src/func_registry/builder.rs b/crates/wef/src/func_registry/builder.rs similarity index 100% rename from src/func_registry/builder.rs rename to crates/wef/src/func_registry/builder.rs diff --git a/src/func_registry/dyn_wrapper.rs b/crates/wef/src/func_registry/dyn_wrapper.rs similarity index 100% rename from src/func_registry/dyn_wrapper.rs rename to crates/wef/src/func_registry/dyn_wrapper.rs diff --git a/src/func_registry/error.rs b/crates/wef/src/func_registry/error.rs similarity index 100% rename from src/func_registry/error.rs rename to crates/wef/src/func_registry/error.rs diff --git a/src/func_registry/function_type.rs b/crates/wef/src/func_registry/function_type.rs similarity index 100% rename from src/func_registry/function_type.rs rename to crates/wef/src/func_registry/function_type.rs diff --git a/src/func_registry/inject.js b/crates/wef/src/func_registry/inject.js similarity index 100% rename from src/func_registry/inject.js rename to crates/wef/src/func_registry/inject.js diff --git a/src/func_registry/into_result.rs b/crates/wef/src/func_registry/into_result.rs similarity index 100% rename from src/func_registry/into_result.rs rename to crates/wef/src/func_registry/into_result.rs diff --git a/src/func_registry/mod.rs b/crates/wef/src/func_registry/mod.rs similarity index 100% rename from src/func_registry/mod.rs rename to crates/wef/src/func_registry/mod.rs diff --git a/src/func_registry/registry.rs b/crates/wef/src/func_registry/registry.rs similarity index 100% rename from src/func_registry/registry.rs rename to crates/wef/src/func_registry/registry.rs diff --git a/src/geom.rs b/crates/wef/src/geom.rs similarity index 100% rename from src/geom.rs rename to crates/wef/src/geom.rs diff --git a/src/input.rs b/crates/wef/src/input.rs similarity index 100% rename from src/input.rs rename to crates/wef/src/input.rs diff --git a/src/js_dialog.rs b/crates/wef/src/js_dialog.rs similarity index 100% rename from src/js_dialog.rs rename to crates/wef/src/js_dialog.rs diff --git a/src/lib.rs b/crates/wef/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/wef/src/lib.rs diff --git a/src/query.rs b/crates/wef/src/query.rs similarity index 100% rename from src/query.rs rename to crates/wef/src/query.rs diff --git a/src/sandbox_context.rs b/crates/wef/src/sandbox_context.rs similarity index 100% rename from src/sandbox_context.rs rename to crates/wef/src/sandbox_context.rs diff --git a/src/settings.rs b/crates/wef/src/settings.rs similarity index 100% rename from src/settings.rs rename to crates/wef/src/settings.rs diff --git a/src/wef.rs b/crates/wef/src/wef.rs similarity index 100% rename from src/wef.rs rename to crates/wef/src/wef.rs diff --git a/examples/wef-example/Cargo.toml b/examples/wef-example/Cargo.toml index 8c80578..d595224 100644 --- a/examples/wef-example/Cargo.toml +++ b/examples/wef-example/Cargo.toml @@ -7,4 +7,4 @@ edition = "2024" embed-manifest = "1.4.0" [dependencies] -wef = { path = "../.." } +wef = { path = "../../crates/wef" } diff --git a/examples/wef-winit/Cargo.toml b/examples/wef-winit/Cargo.toml index 5f0a53e..e01de2f 100644 --- a/examples/wef-winit/Cargo.toml +++ b/examples/wef-winit/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" embed-manifest = "1.4.0" [dependencies] -wef = { path = "../.." } +wef = { path = "../../crates/wef" } image = "0.25.6" winit = "0.30.9" From 0e57072250a5c8ad9f649b38fbd74a1dc9969a9c Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:07:44 +0800 Subject: [PATCH 11/16] . --- LICENSE-APACHE | 191 +++++++++++++++++++++++++++++++++ README.md | 10 +- crates/webview/LICENSE-APACHE | 192 +--------------------------------- crates/wef/LICENSE-APACHE | 192 +--------------------------------- crates/wef/README.md | 1 + 5 files changed, 200 insertions(+), 386 deletions(-) create mode 100644 LICENSE-APACHE create mode 100644 crates/wef/README.md diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..b6ef865 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,191 @@ +Copyright 2024 - 2025 Longbridge + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md index b6abbfe..1c2497a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Wef - Web Embedding Framework +# Wef + +> Web Embedding Framework ![CI](https://github.com/longbridge/wef/workflows/CI/badge.svg) [![Crates.io](https://img.shields.io/crates/v/wef.svg)](https://crates.io/crates/wef) @@ -27,7 +29,7 @@ Add `wef` to your `Cargo.toml`: ```toml [dependencies] -wef = "0.7.0" +wef = "0.6.0" ``` ### Install cargo-wef @@ -64,7 +66,7 @@ For comprehensive documentation, examples, and API reference, see the library do This repository contains: - **Core Library** - The main wef library (in the root directory) -- **[`tool/`](tool/)** - Command-line tool (`cargo-wef`) for building and managing wef applications +- **[`tool/`](tool/)** - Command-line tool (`cargo-wef`) for building and managing wef applications - **[`examples/`](examples/)** - Example applications demonstrating wef usage ## Development @@ -96,4 +98,4 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING. ## License -Licensed under the Apache License, Version 2.0. See [LICENSE-APACHE](LICENSE-APACHE) for details. \ No newline at end of file +Licensed under the Apache License, Version 2.0. See [LICENSE-APACHE](LICENSE-APACHE) for details. diff --git a/crates/webview/LICENSE-APACHE b/crates/webview/LICENSE-APACHE index 3cc771d..e5ee4a2 100644 --- a/crates/webview/LICENSE-APACHE +++ b/crates/webview/LICENSE-APACHE @@ -1,191 +1 @@ -Copyright 2024 - 2025 Longbridge - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS +../../LICENSE-APACHE diff --git a/crates/wef/LICENSE-APACHE b/crates/wef/LICENSE-APACHE index b6ef865..e5ee4a2 100644 --- a/crates/wef/LICENSE-APACHE +++ b/crates/wef/LICENSE-APACHE @@ -1,191 +1 @@ -Copyright 2024 - 2025 Longbridge - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file +../../LICENSE-APACHE diff --git a/crates/wef/README.md b/crates/wef/README.md new file mode 100644 index 0000000..ae42a26 --- /dev/null +++ b/crates/wef/README.md @@ -0,0 +1 @@ +../../README.md From c79c3aad6179dd6b8db1155277e8ef0a246c923f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:11:55 +0800 Subject: [PATCH 12/16] Fix example deps. --- examples/wef-example/Cargo.toml | 3 + examples/wef-example/src/main.rs | 176 +++++++++++++++++++++++-------- 2 files changed, 134 insertions(+), 45 deletions(-) diff --git a/examples/wef-example/Cargo.toml b/examples/wef-example/Cargo.toml index d595224..cb93e4f 100644 --- a/examples/wef-example/Cargo.toml +++ b/examples/wef-example/Cargo.toml @@ -8,3 +8,6 @@ embed-manifest = "1.4.0" [dependencies] wef = { path = "../../crates/wef" } +gpui-webview = { path = "../../crates/webview" } +gpui-component = { git = "https://github.com/longbridge/gpui-component.git" } +gpui = { git = "https://github.com/zed-industries/zed.git" } diff --git a/examples/wef-example/src/main.rs b/examples/wef-example/src/main.rs index a88555d..4f0ce04 100644 --- a/examples/wef-example/src/main.rs +++ b/examples/wef-example/src/main.rs @@ -1,54 +1,140 @@ -use std::sync::mpsc; -use wef::{Browser, BrowserHandler, Settings, FuncRegistry}; +use std::time::Duration; -struct SimpleHandler; +use futures_util::StreamExt; +use gpui::{ + App, AppContext, Application, Bounds, Context, Entity, IntoElement, ParentElement, Render, + Styled, Timer, Window, WindowBounds, WindowOptions, div, px, size, +}; +use gpui_component::{ + Root, + input::{InputEvent, InputState, TextInput}, +}; +use gpui_webview::{ + WebView, + events::TitleChangedEvent, + wef::{self, Frame, FuncRegistry, Settings}, +}; +use serde::Serialize; -impl BrowserHandler for SimpleHandler { - fn on_context_created(&mut self, _frame: &wef::Frame) {} - - fn on_before_browse(&mut self, _frame: &wef::Frame, _request: &wef::ffi::CefRequest, _user_gesture: bool, _is_redirect: bool) -> bool { - false - } +struct Main { + address_state: Entity, + webview: Entity, } -fn main() -> Result<(), Box> { - // Simple wef example showing basic browser creation - println!("Starting wef example..."); - - let settings = Settings::new() - .with_log_severity(wef::settings::LogSeverity::Info) - .with_cache_path(std::env::temp_dir().join("wef-example-cache")); - - // Create a function registry for JavaScript bridge - let func_registry = FuncRegistry::builder() - .register("hello", || "Hello from Rust!".to_string()) - .register("add", |a: i32, b: i32| a + b) - .build(); - - wef::launch(settings, || { - println!("CEF initialized successfully"); - - // Create a browser instance - let handler = SimpleHandler; - let browser = Browser::builder() - .with_url("data:text/html,

Hello from Wef!

") - .with_size(800, 600) - .with_handler(handler) - .with_func_registry(func_registry) +impl Main { + fn new(window: &mut Window, cx: &mut App) -> Entity { + let background_executor = cx.background_executor().clone(); + + let func_registry = FuncRegistry::builder() + .with_spawner(move |fut| { + background_executor.spawn(fut).detach(); + }) + .register("toUppercase", |value: String| value.to_uppercase()) + .register("addInt", |a: i32, b: i32| a + b) + .register("parseInt", |value: String| value.parse::()) + .register_async("sleep", |millis: u64| async move { + Timer::after(Duration::from_millis(millis)).await; + "ok" + }) + .register("emit", |frame: Frame| { + #[derive(Debug, Serialize)] + struct Message { + event: String, + data: String, + } + + frame.emit(Message { + event: "custom".to_string(), + data: "ok".to_string(), + }); + }) .build(); - - if let Ok(_browser) = browser { - println!("Browser created successfully!"); - - // In a real application, you would handle events and keep the browser running - // For this example, we'll just demonstrate that the browser can be created - std::thread::sleep(std::time::Duration::from_secs(2)); - } else { - eprintln!("Failed to create browser"); + + cx.new(|cx| { + let url = "https://www.google.com"; + + // create webview + let webview = WebView::with_func_registry(url, func_registry.clone(), window, cx); + + window + .subscribe(&webview, cx, |_, event: &TitleChangedEvent, window, _| { + window.set_window_title(&event.title); + }) + .detach(); + + // create address input + let address_state = cx.new(|cx| InputState::new(window, cx).default_value(url)); + + window + .subscribe(&address_state, cx, { + let webview = webview.clone(); + move |state, event: &InputEvent, _, cx| { + if let InputEvent::PressEnter { .. } = event { + let url = state.read(cx).value(); + webview.read(cx).browser().load_url(&url); + } + } + }) + .detach(); + + Self { + address_state, + webview, + } + }) + } +} + +impl Render for Main { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(TextInput::new(&self.address_state)) + .child(self.webview.clone()) + .children(Root::render_modal_layer(window, cx)) + } +} + +fn run() { + Application::new().run(|cx: &mut App| { + if cfg!(target_os = "linux") { + cx.spawn(async move |cx| { + let (tx, rx) = flume::unbounded(); + + cx.background_spawn(async move { + let mut timer = Timer::interval(Duration::from_millis(1000 / 60)); + while timer.next().await.is_some() { + _ = tx.send_async(()).await; + } + }) + .detach(); + + while rx.recv_async().await.is_ok() { + wef::do_message_work(); + } + }) + .detach(); } - - println!("Wef example completed"); + + gpui_component::init(cx); + + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + let main = Main::new(window, cx); + cx.new(|cx| Root::new(main.into(), window, cx)) + }, + ) + .unwrap(); + cx.activate(true); }); - +} + +fn main() -> Result<(), Box> { + wef::launch(Settings::new(), run); Ok(()) } From 2a01cc8ade218e955aeaaae295d73606453e38c6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:15:26 +0800 Subject: [PATCH 13/16] fix dep --- crates/webview/Cargo.toml | 3 +++ examples/wef-example/Cargo.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/webview/Cargo.toml b/crates/webview/Cargo.toml index be7cac6..1e84371 100644 --- a/crates/webview/Cargo.toml +++ b/crates/webview/Cargo.toml @@ -8,7 +8,10 @@ homepage = "https://github.com/longbridge/wef" repository = "https://github.com/longbridge/wef" [dependencies] +gpui-component = { git = "https://github.com/longbridge/gpui-component.git" } +gpui = { git = "https://github.com/zed-industries/zed.git" } wef = { path = "../wef" } +schemars = "1" serde = { version = "1.0.219", features = ["derive"] } raw-window-handle = "0.6.2" rust-i18n = "3" diff --git a/examples/wef-example/Cargo.toml b/examples/wef-example/Cargo.toml index cb93e4f..60de6c2 100644 --- a/examples/wef-example/Cargo.toml +++ b/examples/wef-example/Cargo.toml @@ -11,3 +11,6 @@ wef = { path = "../../crates/wef" } gpui-webview = { path = "../../crates/webview" } gpui-component = { git = "https://github.com/longbridge/gpui-component.git" } gpui = { git = "https://github.com/zed-industries/zed.git" } +futures-util = "0.3.31" +serde = { version = "1.0.219", features = ["derive"] } +flume = "0.11.1" From 96f553ada6a26deeec22b4a2de84e5e8c493ede5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:20:03 +0800 Subject: [PATCH 14/16] . --- CONTRIBUTING.md | 117 +++++------------------------------------------- README.md | 14 +----- 2 files changed, 13 insertions(+), 118 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84a3bf1..cd5d06b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,153 +6,59 @@ Thank you for your interest in contributing to Wef! This document provides guide Before you can build and test Wef, you'll need: -### System Dependencies - -#### Linux (Ubuntu/Debian) -```bash -sudo apt-get update -sudo apt-get install -y libglib2.0-dev pkg-config build-essential -``` - -#### Linux (CentOS/RHEL/Fedora) -```bash -sudo yum install -y glib2-devel pkgconfig gcc-c++ -# or for newer versions: -sudo dnf install -y glib2-devel pkgconfig gcc-c++ -``` - -#### macOS -```bash -# Install Xcode Command Line Tools -xcode-select --install -``` - -#### Windows -- Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) - -### Rust Toolchain - -Install Rust using [rustup](https://rustup.rs/): -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - ## Development Setup 1. **Clone the repository** + ```bash git clone https://github.com/longbridge/wef.git cd wef ``` 2. **Install cargo-wef** + ```bash cargo install --path cargo-wef ``` 3. **Initialize CEF** - ```bash - cargo wef init - ``` - This downloads the Chromium Embedded Framework binaries to `~/.cef` -4. **Build the project** ```bash - cargo wef build - ``` - -5. **Run tests** - ```bash - cargo test --all + cargo wef init ``` -## Project Structure - -- **`wef/`** - Core library implementing CEF3 bindings -- **`cargo-wef/`** - Command-line tool for building wef applications -- **`examples/`** - Example applications demonstrating usage + This downloads the Chromium Embedded Framework binaries to `~/.cef` ## Building and Testing ### Building + ```bash # Build everything cargo wef build # Build specific components cargo build -p wef # Core library -cargo build -p cargo-wef # CLI tool +cargo build -p cargo-wef # CLI tool ``` ### Testing + ```bash # Run all tests cargo test --all -# Test specific components +# Test specific components cargo test -p wef cargo test -p cargo-wef ``` ### Running Examples -```bash -# Run the winit example -cargo wef run --example wef-winit -``` - -## Code Style - -We use the standard Rust formatting tools: ```bash -# Format code -cargo fmt --all - -# Check formatting -cargo fmt --all -- --check - -# Run clippy -cargo clippy --all -- -D warnings +cargo wef run -p wef-example ``` -## Submitting Changes - -1. **Fork the repository** on GitHub -2. **Create a feature branch** from `main` -3. **Make your changes** following the coding standards -4. **Add tests** for new functionality -5. **Ensure all tests pass** with `cargo test --all` -6. **Format your code** with `cargo fmt --all` -7. **Run clippy** with `cargo clippy --all -- -D warnings` -8. **Commit your changes** with clear, descriptive commit messages -9. **Push to your fork** and create a Pull Request - -## Commit Messages - -Use clear, descriptive commit messages following conventional commits: - -- `feat: add new functionality` -- `fix: resolve bug in component` -- `docs: update README` -- `refactor: improve code structure` -- `test: add test coverage` - -## Pull Request Guidelines - -- **Describe your changes** clearly in the PR description -- **Reference related issues** using keywords like "Fixes #123" -- **Keep changes focused** - one feature or fix per PR -- **Update documentation** if you're changing public APIs -- **Add tests** for new functionality -- **Ensure CI passes** before requesting review - -## Issues and Feature Requests - -- Check existing issues before creating new ones -- Use clear, descriptive titles -- Provide reproduction steps for bugs -- Include system information (OS, Rust version, etc.) - ## Development Tips ### CEF Troubleshooting @@ -160,6 +66,7 @@ Use clear, descriptive commit messages following conventional commits: If you encounter CEF-related build issues: 1. **Clear CEF cache** + ```bash rm -rf ~/.cef cargo wef init @@ -173,7 +80,7 @@ If you encounter CEF-related build issues: ### Debugging -- Use `cargo wef run --example wef-winit` to test changes quickly +- Use `cargo wef run -p wef-winit` to test changes quickly - Enable debug logging with `RUST_LOG=debug` - Use the debugger-friendly debug profile for development @@ -185,4 +92,4 @@ If you encounter CEF-related build issues: ## License -By contributing to Wef, you agree that your contributions will be licensed under the Apache License 2.0. \ No newline at end of file +By contributing to Wef, you agree that your contributions will be licensed under the Apache License 2.0. diff --git a/README.md b/README.md index 1c2497a..5a88b1e 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,6 @@ fn main() { } ``` -## Documentation - -For comprehensive documentation, examples, and API reference, see the library documentation within this repository. - -## Project Structure - -This repository contains: - -- **Core Library** - The main wef library (in the root directory) -- **[`tool/`](tool/)** - Command-line tool (`cargo-wef`) for building and managing wef applications -- **[`examples/`](examples/)** - Example applications demonstrating wef usage - ## Development ### Building @@ -81,7 +69,7 @@ cargo wef build cargo test --all # Run an example -cargo wef run --example wef-winit +cargo wef run -p wef-winit ``` ### Requirements From bc9bebe41a2fba8f9177849c39268bd964d91064 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:21:21 +0800 Subject: [PATCH 15/16] Fix test. --- crates/cargo-wef/tests/.gitignore | 2 ++ crates/cargo-wef/tests/bin/Cargo.toml | 11 +++++++++++ crates/cargo-wef/tests/bin/src/bin/bin1.rs | 1 + crates/cargo-wef/tests/bin/src/main.rs | 3 +++ .../cargo-wef/tests/default_members/.gitignore | 1 + .../cargo-wef/tests/default_members/Cargo.toml | 4 ++++ .../tests/default_members/bin1/Cargo.toml | 9 +++++++++ .../tests/default_members/bin1/src/main.rs | 3 +++ .../tests/default_members/bin2/Cargo.toml | 9 +++++++++ .../tests/default_members/bin2/src/main.rs | 3 +++ .../tests/default_members/bin3/Cargo.toml | 9 +++++++++ .../tests/default_members/bin3/src/main.rs | 3 +++ crates/cargo-wef/tests/example/Cargo.toml | 11 +++++++++++ .../cargo-wef/tests/example/examples/example1.rs | 1 + crates/cargo-wef/tests/example/src/lib.rs | 1 + crates/cargo-wef/tests/package_bin/Cargo.toml | 16 ++++++++++++++++ crates/cargo-wef/tests/package_bin/src/main.rs | 3 +++ crates/cargo-wef/tests/workspace/Cargo.toml | 3 +++ crates/cargo-wef/tests/workspace/bin/Cargo.toml | 9 +++++++++ .../tests/workspace/bin/src/bin/bin1.rs | 1 + .../tests/workspace/bin/src/bin/bin2.rs | 1 + crates/cargo-wef/tests/workspace/bin/src/lib.rs | 1 + .../cargo-wef/tests/workspace/example/Cargo.toml | 9 +++++++++ .../tests/workspace/example/examples/example1.rs | 1 + .../cargo-wef/tests/workspace/example/src/lib.rs | 1 + .../tests/workspace/package_bin/Cargo.toml | 9 +++++++++ .../tests/workspace/package_bin/src/main.rs | 3 +++ 27 files changed, 128 insertions(+) create mode 100644 crates/cargo-wef/tests/.gitignore create mode 100644 crates/cargo-wef/tests/bin/Cargo.toml create mode 100644 crates/cargo-wef/tests/bin/src/bin/bin1.rs create mode 100644 crates/cargo-wef/tests/bin/src/main.rs create mode 100644 crates/cargo-wef/tests/default_members/.gitignore create mode 100644 crates/cargo-wef/tests/default_members/Cargo.toml create mode 100644 crates/cargo-wef/tests/default_members/bin1/Cargo.toml create mode 100644 crates/cargo-wef/tests/default_members/bin1/src/main.rs create mode 100644 crates/cargo-wef/tests/default_members/bin2/Cargo.toml create mode 100644 crates/cargo-wef/tests/default_members/bin2/src/main.rs create mode 100644 crates/cargo-wef/tests/default_members/bin3/Cargo.toml create mode 100644 crates/cargo-wef/tests/default_members/bin3/src/main.rs create mode 100644 crates/cargo-wef/tests/example/Cargo.toml create mode 100644 crates/cargo-wef/tests/example/examples/example1.rs create mode 100644 crates/cargo-wef/tests/example/src/lib.rs create mode 100644 crates/cargo-wef/tests/package_bin/Cargo.toml create mode 100644 crates/cargo-wef/tests/package_bin/src/main.rs create mode 100644 crates/cargo-wef/tests/workspace/Cargo.toml create mode 100644 crates/cargo-wef/tests/workspace/bin/Cargo.toml create mode 100644 crates/cargo-wef/tests/workspace/bin/src/bin/bin1.rs create mode 100644 crates/cargo-wef/tests/workspace/bin/src/bin/bin2.rs create mode 100644 crates/cargo-wef/tests/workspace/bin/src/lib.rs create mode 100644 crates/cargo-wef/tests/workspace/example/Cargo.toml create mode 100644 crates/cargo-wef/tests/workspace/example/examples/example1.rs create mode 100644 crates/cargo-wef/tests/workspace/example/src/lib.rs create mode 100644 crates/cargo-wef/tests/workspace/package_bin/Cargo.toml create mode 100644 crates/cargo-wef/tests/workspace/package_bin/src/main.rs diff --git a/crates/cargo-wef/tests/.gitignore b/crates/cargo-wef/tests/.gitignore new file mode 100644 index 0000000..2a6fee4 --- /dev/null +++ b/crates/cargo-wef/tests/.gitignore @@ -0,0 +1,2 @@ +**/target +**/Cargo.lock \ No newline at end of file diff --git a/crates/cargo-wef/tests/bin/Cargo.toml b/crates/cargo-wef/tests/bin/Cargo.toml new file mode 100644 index 0000000..2c12c4e --- /dev/null +++ b/crates/cargo-wef/tests/bin/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] + +[package] +name = "pkg-bin" +version = "0.5.0" +edition = "2024" + +[package.metadata.bundle.bin.bin1] +name = "test-bin" +identifier = "io.github.longbridge.wef.tests.bin" +category = "Utility" diff --git a/crates/cargo-wef/tests/bin/src/bin/bin1.rs b/crates/cargo-wef/tests/bin/src/bin/bin1.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/cargo-wef/tests/bin/src/bin/bin1.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/cargo-wef/tests/bin/src/main.rs b/crates/cargo-wef/tests/bin/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/bin/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cargo-wef/tests/default_members/.gitignore b/crates/cargo-wef/tests/default_members/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/crates/cargo-wef/tests/default_members/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/crates/cargo-wef/tests/default_members/Cargo.toml b/crates/cargo-wef/tests/default_members/Cargo.toml new file mode 100644 index 0000000..9554049 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +resolver = "2" +default-members = ["bin2", "bin3"] +members = ["bin1", "bin2", "bin3"] diff --git a/crates/cargo-wef/tests/default_members/bin1/Cargo.toml b/crates/cargo-wef/tests/default_members/bin1/Cargo.toml new file mode 100644 index 0000000..2f74cce --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin1/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bin1" +version = "0.5.0" +edition = "2024" + +[package.metadata.bundle.bin.bin1] +name = "test-bin1" +identifier = "io.github.longbridge.wef.tests.bin1" +category = "Utility" diff --git a/crates/cargo-wef/tests/default_members/bin1/src/main.rs b/crates/cargo-wef/tests/default_members/bin1/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin1/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cargo-wef/tests/default_members/bin2/Cargo.toml b/crates/cargo-wef/tests/default_members/bin2/Cargo.toml new file mode 100644 index 0000000..f271923 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bin2" +version = "0.5.1" +edition = "2024" + +[package.metadata.bundle.bin.bin2] +name = "test-bin2" +identifier = "io.github.longbridge.wef.tests.bin2" +category = "Utility" diff --git a/crates/cargo-wef/tests/default_members/bin2/src/main.rs b/crates/cargo-wef/tests/default_members/bin2/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin2/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cargo-wef/tests/default_members/bin3/Cargo.toml b/crates/cargo-wef/tests/default_members/bin3/Cargo.toml new file mode 100644 index 0000000..cfcde24 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin3/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bin3" +version = "0.5.2" +edition = "2024" + +[package.metadata.bundle.bin.bin3] +name = "test-bin2" +identifier = "io.github.longbridge.wef.tests.bin3" +category = "Utility" diff --git a/crates/cargo-wef/tests/default_members/bin3/src/main.rs b/crates/cargo-wef/tests/default_members/bin3/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/default_members/bin3/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cargo-wef/tests/example/Cargo.toml b/crates/cargo-wef/tests/example/Cargo.toml new file mode 100644 index 0000000..4abfd8b --- /dev/null +++ b/crates/cargo-wef/tests/example/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] + +[package] +name = "pkg-example" +version = "0.3.2" +edition = "2024" + +[package.metadata.bundle.example.example1] +name = "test-example" +identifier = "io.github.longbridge.wef.tests.example" +category = "Utility" diff --git a/crates/cargo-wef/tests/example/examples/example1.rs b/crates/cargo-wef/tests/example/examples/example1.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/cargo-wef/tests/example/examples/example1.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/cargo-wef/tests/example/src/lib.rs b/crates/cargo-wef/tests/example/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/cargo-wef/tests/example/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/cargo-wef/tests/package_bin/Cargo.toml b/crates/cargo-wef/tests/package_bin/Cargo.toml new file mode 100644 index 0000000..5895680 --- /dev/null +++ b/crates/cargo-wef/tests/package_bin/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] + +[package] +name = "package-bin" +version = "0.3.0" +edition = "2024" + +[package.metadata.bundle] +name = "test-package-bin" +identifier = "io.github.longbridge.wef.tests.package-bin" +category = "Utility" + +[package.metadata.bundle.preview] +name = "test-package-bin-preview" +identifier = "io.github.longbridge.wef.tests.package-bin.preview" +category = "Utility" diff --git a/crates/cargo-wef/tests/package_bin/src/main.rs b/crates/cargo-wef/tests/package_bin/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/package_bin/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/cargo-wef/tests/workspace/Cargo.toml b/crates/cargo-wef/tests/workspace/Cargo.toml new file mode 100644 index 0000000..8fd623b --- /dev/null +++ b/crates/cargo-wef/tests/workspace/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["example", "bin", "package_bin"] diff --git a/crates/cargo-wef/tests/workspace/bin/Cargo.toml b/crates/cargo-wef/tests/workspace/bin/Cargo.toml new file mode 100644 index 0000000..ca5916d --- /dev/null +++ b/crates/cargo-wef/tests/workspace/bin/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pkg-bin" +version = "0.5.0" +edition = "2024" + +[package.metadata.bundle.bin.bin1] +name = "test-bin" +identifier = "io.github.longbridge.wef.tests.bin" +category = "Utility" diff --git a/crates/cargo-wef/tests/workspace/bin/src/bin/bin1.rs b/crates/cargo-wef/tests/workspace/bin/src/bin/bin1.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/cargo-wef/tests/workspace/bin/src/bin/bin1.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/cargo-wef/tests/workspace/bin/src/bin/bin2.rs b/crates/cargo-wef/tests/workspace/bin/src/bin/bin2.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/cargo-wef/tests/workspace/bin/src/bin/bin2.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/cargo-wef/tests/workspace/bin/src/lib.rs b/crates/cargo-wef/tests/workspace/bin/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/cargo-wef/tests/workspace/bin/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/cargo-wef/tests/workspace/example/Cargo.toml b/crates/cargo-wef/tests/workspace/example/Cargo.toml new file mode 100644 index 0000000..12401d7 --- /dev/null +++ b/crates/cargo-wef/tests/workspace/example/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pkg-example" +version = "0.3.2" +edition = "2024" + +[package.metadata.bundle.example.example1] +name = "test-example" +identifier = "io.github.longbridge.wef.tests.example" +category = "Utility" diff --git a/crates/cargo-wef/tests/workspace/example/examples/example1.rs b/crates/cargo-wef/tests/workspace/example/examples/example1.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/cargo-wef/tests/workspace/example/examples/example1.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/cargo-wef/tests/workspace/example/src/lib.rs b/crates/cargo-wef/tests/workspace/example/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/cargo-wef/tests/workspace/example/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/cargo-wef/tests/workspace/package_bin/Cargo.toml b/crates/cargo-wef/tests/workspace/package_bin/Cargo.toml new file mode 100644 index 0000000..6dcc58b --- /dev/null +++ b/crates/cargo-wef/tests/workspace/package_bin/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "package-bin" +version = "0.3.0" +edition = "2024" + +[package.metadata.bundle] +name = "test-package-bin" +identifier = "io.github.longbridge.wef.tests.package-bin" +category = "Utility" diff --git a/crates/cargo-wef/tests/workspace/package_bin/src/main.rs b/crates/cargo-wef/tests/workspace/package_bin/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/crates/cargo-wef/tests/workspace/package_bin/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From ee2a71a23a3c1b9f50b4cd0f06d68e08c2e6fb44 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 29 Sep 2025 14:30:08 +0800 Subject: [PATCH 16/16] Fix linux ci. --- .github/workflows/ci.yml | 9 +-------- script/bootstrap | 8 ++++++++ script/install-linux-deps | 9 +++++++++ 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100755 script/bootstrap create mode 100755 script/install-linux-deps diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db3e53d..6801213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,7 @@ jobs: components: clippy - name: Install system dependencies if: ${{ matrix.run_on != 'windows-latest' }} - run: | - if [ "${{ matrix.run_on }}" = "ubuntu-latest" ]; then - sudo apt-get update - sudo apt-get install -y libglib2.0-dev pkg-config - elif [ "${{ matrix.run_on }}" = "macos-latest" ]; then - # macOS dependencies if needed - echo "macOS setup complete" - fi + run: script/bootstrap - name: Machete if: ${{ matrix.run_on == 'macos-latest' }} uses: bnjbvr/cargo-machete@v0.9.1 diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 0000000..e8d5ea4 --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "Install Linux dependencies..." + script/install-linux-deps +else + echo "Install macOS dependencies..." +fi diff --git a/script/install-linux-deps b/script/install-linux-deps new file mode 100755 index 0000000..e8a2cde --- /dev/null +++ b/script/install-linux-deps @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +sudo apt update +# Test on Ubuntu 24.04 +sudo apt install -y \ + gcc g++ clang libfontconfig-dev libwayland-dev \ + libwebkit2gtk-4.1-dev libxkbcommon-x11-dev libx11-xcb-dev \ + libssl-dev libzstd-dev \ + vulkan-validationlayers libvulkan1