This commit is contained in:
not-elm
2025-08-10 21:28:45 +09:00
commit 23bdc65da3
91 changed files with 20122 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
mod app;
mod browser_process_handler;
mod browsers;
mod client_handler;
mod context_menu_handler;
mod display_handler;
mod localhost;
mod renderer_handler;
mod request_context_handler;
pub use app::*;
pub use browser_process_handler::*;
pub use browsers::*;
pub use client_handler::*;
pub use context_menu_handler::*;
pub use localhost::*;
pub use renderer_handler::*;
pub use request_context_handler::*;

View File

@@ -0,0 +1,85 @@
use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder;
use crate::util::{SCHEME_CEF, cef_scheme_flags};
use cef::rc::{Rc, RcImpl};
use cef::{
BrowserProcessHandler, CefString, CommandLine, ImplApp, ImplCommandLine, ImplSchemeRegistrar,
SchemeRegistrar, WrapApp,
};
use cef_dll_sys::{_cef_app_t, cef_base_ref_counted_t};
/// ## Reference
///
/// - [`CefApp Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefApp.html)
#[derive(Default)]
pub struct BrowserProcessAppBuilder {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl BrowserProcessAppBuilder {
pub fn build() -> cef::App {
cef::App::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Clone for BrowserProcessAppBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
self.object
};
Self { object }
}
}
impl Rc for BrowserProcessAppBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplApp for BrowserProcessAppBuilder {
fn on_before_command_line_processing(
&self,
_: Option<&CefString>,
command_line: Option<&mut CommandLine>,
) {
let Some(command_line) = command_line else {
return;
};
//TODO: フラグで切り替えるようにする
command_line.append_switch(Some(&"use-mock-keychain".into()));
#[cfg(feature = "debug")]
{
command_line.append_switch(Some(&"disable-gpu".into()));
command_line.append_switch(Some(&"disable-gpu-compositing".into()));
command_line.append_switch(Some(&"disable-software-rasterizer".into()));
}
}
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerBuilder::build())
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
registrar.add_custom_scheme(Some(&SCHEME_CEF.into()), cef_scheme_flags() as _);
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_app_t {
self.object as *mut cef::sys::_cef_app_t
}
}
impl WrapApp for BrowserProcessAppBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,63 @@
use cef::rc::{Rc, RcImpl};
use cef::*;
/// ## Reference
///
/// - [`CefBrowserProcessHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowserProcessHandler.html)
pub struct BrowserProcessHandlerBuilder {
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
}
impl BrowserProcessHandlerBuilder {
pub fn build() -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Rc for BrowserProcessHandlerBuilder {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapBrowserProcessHandler for BrowserProcessHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_browser_process_handler_t, Self>) {
self.object = object;
}
}
impl Clone for BrowserProcessHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplBrowserProcessHandler for BrowserProcessHandlerBuilder {
fn on_before_child_process_launch(&self, command_line: Option<&mut CommandLine>) {
let Some(command_line) = command_line else {
return;
};
command_line.append_switch(Some(&"disable-web-security".into()));
command_line.append_switch(Some(&"allow-running-insecure-content".into()));
command_line.append_switch(Some(&"disable-session-crashed-bubble".into()));
command_line.append_switch(Some(&"ignore-certificate-errors".into()));
command_line.append_switch(Some(&"ignore-ssl-errors".into()));
command_line.append_switch(Some(&"enable-logging=stderr".into()));
}
#[inline]
fn get_raw(&self) -> *mut cef_dll_sys::_cef_browser_process_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,492 @@
use crate::browser_process::BrpHandler;
use crate::browser_process::ClientHandlerBuilder;
use crate::browser_process::client_handler::{IpcEventRaw, JsEmitEventHandler};
use crate::prelude::IntoString;
use crate::prelude::*;
use async_channel::{Sender, TryRecvError};
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy_remote::BrpMessage;
use cef::{
Browser, BrowserHost, BrowserSettings, Client, CompositionUnderline, ImplBrowser,
ImplBrowserHost, ImplFrame, ImplListValue, ImplProcessMessage, ImplRequestContext,
MouseButtonType, ProcessId, Range, RequestContext, RequestContextSettings, WindowInfo,
browser_host_create_browser_sync, process_message_create,
};
use cef_dll_sys::{cef_event_flags_t, cef_mouse_button_type_t};
#[allow(deprecated)]
use raw_window_handle::RawWindowHandle;
use std::cell::Cell;
use std::rc::Rc;
mod devtool_render_handler;
mod keyboard;
use crate::browser_process::browsers::devtool_render_handler::DevToolRenderHandlerBuilder;
use crate::browser_process::display_handler::{DisplayHandlerBuilder, SystemCursorIconSenderInner};
pub use keyboard::*;
pub struct WebviewBrowser {
pub client: Browser,
pub host: BrowserHost,
pub size: SharedViewSize,
}
pub struct Browsers {
browsers: HashMap<Entity, WebviewBrowser>,
sender: TextureSender,
receiver: TextureReceiver,
ime_caret: SharedImeCaret,
}
impl Default for Browsers {
fn default() -> Self {
let (sender, receiver) = async_channel::unbounded::<RenderTexture>();
Browsers {
browsers: HashMap::default(),
sender,
receiver,
ime_caret: Rc::new(Cell::new(0)),
}
}
}
impl Browsers {
#[allow(clippy::too_many_arguments)]
pub fn create_browser(
&mut self,
webview: Entity,
uri: &str,
webview_size: Vec2,
requester: Requester,
ipc_event_sender: Sender<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
window_handle: Option<RawWindowHandle>,
) {
let mut context = Self::request_context(requester);
let size = Rc::new(Cell::new(webview_size));
let browser = browser_host_create_browser_sync(
Some(&WindowInfo {
windowless_rendering_enabled: true as _,
external_begin_frame_enabled: true as _,
parent_view: match window_handle {
Some(RawWindowHandle::AppKit(handle)) => handle.ns_view.as_ptr(),
Some(RawWindowHandle::Win32(handle)) => handle.hwnd.get() as _,
Some(RawWindowHandle::Xlib(handle)) => handle.window as _,
Some(RawWindowHandle::Wayland(handle)) => handle.surface.as_ptr(),
_ => std::ptr::null_mut(),
},
// shared_texture_enabled: true as _,
..Default::default()
}),
Some(&mut self.client_handler(
webview,
size.clone(),
ipc_event_sender,
brp_sender,
system_cursor_icon_sender,
)),
Some(&uri.into()),
Some(&BrowserSettings {
windowless_frame_rate: 60,
..Default::default()
}),
None,
context.as_mut(),
)
.expect("Failed to create browser");
self.browsers.insert(
webview,
WebviewBrowser {
host: browser.host().expect("Failed to get browser host"),
client: browser,
size,
},
);
}
pub fn send_external_begin_frame(&mut self) {
for browser in self.browsers.values_mut() {
browser.host.send_external_begin_frame();
}
}
pub fn send_mouse_move<'a>(
&self,
webview: &Entity,
buttons: impl IntoIterator<Item = &'a MouseButton>,
position: Vec2,
mouse_leave: bool,
) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: modifiers_from_mouse_buttons(buttons),
};
browser
.host
.send_mouse_move_event(Some(&mouse_event), mouse_leave as _);
}
}
pub fn send_mouse_click(
&self,
webview: &Entity,
position: Vec2,
button: PointerButton,
mouse_up: bool,
) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: match button {
PointerButton::Primary => cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON,
PointerButton::Secondary => cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON,
PointerButton::Middle => cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON,
} as _, // No modifiers for simplicity
};
let mouse_button = match button {
PointerButton::Secondary => cef_mouse_button_type_t::MBT_RIGHT,
PointerButton::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
_ => cef_mouse_button_type_t::MBT_LEFT,
};
browser.host.set_focus(true as _);
browser.host.send_mouse_click_event(
Some(&mouse_event),
MouseButtonType::from(mouse_button),
mouse_up as _,
1,
);
}
}
/// [`SendMouseWheelEvent`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowserHost.html#acd5d057bd5230baa9a94b7853ba755f7)
pub fn send_mouse_wheel(&self, webview: &Entity, position: Vec2, delta: Vec2) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: 0,
};
browser
.host
.send_mouse_wheel_event(Some(&mouse_event), delta.x as _, delta.y as _);
}
}
#[inline]
pub fn send_key(&self, webview: &Entity, event: cef::KeyEvent) {
if let Some(browser) = self.get_focused_browser(webview) {
browser.host.send_key_event(Some(&event));
}
}
pub fn emit_event(&self, webview: &Entity, id: impl Into<String>, event: &serde_json::Value) {
if let Some(mut process_message) =
process_message_create(Some(&PROCESS_MESSAGE_HOST_EMIT.into()))
&& let Some(argument_list) = process_message.argument_list()
&& let Some(browser) = self.browsers.get(webview)
&& let Some(frame) = browser.client.main_frame()
{
argument_list.set_string(0, Some(&id.into().as_str().into()));
argument_list.set_string(1, Some(&event.to_string().as_str().into()));
frame.send_process_message(
ProcessId::from(cef_dll_sys::cef_process_id_t::PID_RENDERER),
Some(&mut process_message),
);
};
}
pub fn resize(&self, webview: &Entity, size: Vec2) {
if let Some(browser) = self.browsers.get(webview) {
browser.size.set(size);
browser.host.was_resized();
}
}
/// Closes the browser associated with the given webview entity.
///
/// The browser will be removed from the hash map after closing.
pub fn close(&mut self, webview: &Entity) {
if let Some(browser) = self.browsers.remove(webview) {
browser.host.close_browser(true as _);
debug!("Closed browser with webview: {:?}", webview);
}
}
#[inline]
pub fn try_receive_texture(&self) -> core::result::Result<RenderTexture, TryRecvError> {
self.receiver.try_recv()
}
/// Shows the DevTools for the specified webview.
pub fn show_devtool(&self, webview: &Entity) {
let Some(browser) = self.browsers.get(webview) else {
return;
};
browser.host.show_dev_tools(
Some(&WindowInfo::default()),
Some(&mut ClientHandlerBuilder::new(DevToolRenderHandlerBuilder::build()).build()),
Some(&BrowserSettings::default()),
None,
);
}
/// Closes the DevTools for the specified webview.
pub fn close_devtools(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.close_dev_tools();
}
}
/// Navigate backwards.
///
/// ## Reference
///
/// - [`GoBack`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowser.html#a85b02760885c070e4ad2a2705cea56cb)
pub fn go_back(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview)
&& browser.client.can_go_back() == 1
{
browser.client.go_back();
}
}
/// Navigate forwards.
///
/// ## Reference
///
/// - [`GoForward`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowser.html#aa8e97fc210ee0e73f16b2d98482419d0)
pub fn go_forward(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview)
&& browser.client.can_go_forward() == 1
{
browser.client.go_forward();
}
}
/// Returns the current zoom level for the specified webview.
///
/// ## Reference
///
/// - [`GetZoomLevel`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a524d4a358287dab284c0dfec6d6d229e)
pub fn zoom_level(&self, webview: &Entity) -> Option<f64> {
self.browsers
.get(webview)
.map(|browser| browser.host.zoom_level())
}
/// Sets the zoom level for the specified webview.
///
/// ## Reference
///
/// - [`SetZoomLevel`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#af2b7bf250ac78345117cd575190f2f7b)
pub fn set_zoom_level(&self, webview: &Entity, zoom_level: f64) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.set_zoom_level(zoom_level);
}
}
/// Sets whether the audio is muted for the specified webview.
///
/// ## Reference
///
/// - [`SetAudioMuted`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a153d179c9ff202c8bb8869d2e9a820a2)
pub fn set_audio_muted(&self, webview: &Entity, muted: bool) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.set_audio_muted(muted as _);
}
}
#[inline]
pub fn reload(&self) {
for browser in self.browsers.values() {
if let Some(frame) = browser.client.main_frame() {
let url = frame.url().into_string();
info!("Reloading browser with URL: {}", url);
frame.load_url(Some(&url.as_str().into()));
}
}
}
/// ## Reference
///
/// - [`ImeSetComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a567b41fb2d3917843ece3b57adc21ebe)
pub fn set_ime_composition(&self, text: &str, cursor_utf16: Option<u32>) {
let underlines = make_underlines_for(text, cursor_utf16.map(|i| (i, i)));
let i = text.encode_utf16().count();
let selection_range = Range {
from: i as _,
to: i as _,
};
let replacement_range = self.ime_caret_range();
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser.host.ime_set_composition(
Some(&text.into()),
underlines.len(),
Some(&underlines[0]),
Some(&replacement_range),
Some(&selection_range),
);
}
}
/// ## Reference
///
/// [`ImeSetComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a567b41fb2d3917843ece3b57adc21ebe)
pub fn ime_finish_composition(&self, keep_selection: bool) {
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser.host.ime_finish_composing_text(keep_selection as _);
}
}
pub fn set_ime_commit_text(&self, text: &str) {
let replacement_range = self.ime_caret_range();
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser
.host
.ime_commit_text(Some(&text.into()), Some(&replacement_range), 0)
}
}
fn request_context(requester: Requester) -> Option<RequestContext> {
let mut context = cef::request_context_create_context(
Some(&RequestContextSettings::default()),
Some(&mut RequestContextHandlerBuilder::build()),
);
if let Some(context) = context.as_mut() {
context.register_scheme_handler_factory(
Some(&SCHEME_CEF.into()),
Some(&HOST_CEF.into()),
Some(&mut LocalSchemaHandlerBuilder::build(requester)),
);
}
context
}
fn client_handler(
&self,
webview: Entity,
size: SharedViewSize,
ipc_event_sender: Sender<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
) -> Client {
ClientHandlerBuilder::new(RenderHandlerBuilder::build(
webview,
self.sender.clone(),
size.clone(),
self.ime_caret.clone(),
))
.with_display_handler(DisplayHandlerBuilder::build(system_cursor_icon_sender))
.with_message_handler(JsEmitEventHandler::new(webview, ipc_event_sender))
.with_message_handler(BrpHandler::new(brp_sender))
.build()
}
#[inline]
fn ime_caret_range(&self) -> Range {
let caret = self.ime_caret.get();
Range {
from: caret,
to: caret,
}
}
#[inline]
fn get_focused_browser(&self, webview: &Entity) -> Option<&WebviewBrowser> {
self.browsers
.get(webview)
.and_then(|b| b.client.focused_frame().is_some().then_some(b))
}
}
pub fn modifiers_from_mouse_buttons<'a>(buttons: impl IntoIterator<Item = &'a MouseButton>) -> u32 {
let mut modifiers = cef_event_flags_t::EVENTFLAG_NONE as u32;
for button in buttons {
match button {
MouseButton::Left => modifiers |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32,
MouseButton::Right => {
modifiers |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32
}
MouseButton::Middle => {
modifiers |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32
}
_ => {}
}
}
modifiers
}
pub fn make_underlines_for(
text: &str,
selection_utf16: Option<(u32, u32)>,
) -> Vec<CompositionUnderline> {
let len16 = utf16_len(text);
let base = CompositionUnderline {
size: size_of::<CompositionUnderline>(),
range: Range { from: 0, to: len16 },
color: 0,
background_color: 0,
thick: 0,
style: Default::default(),
};
if let Some((from, to)) = selection_utf16
&& from < to
{
let sel = CompositionUnderline {
size: size_of::<CompositionUnderline>(),
range: Range { from, to },
color: 0,
background_color: 0,
thick: 1,
style: Default::default(),
};
return vec![base, sel];
}
vec![base]
}
#[inline]
fn utf16_len(s: &str) -> u32 {
s.encode_utf16().count() as u32
}
#[allow(dead_code)]
fn utf16_index_from_byte(s: &str, byte_idx: usize) -> u32 {
s[..byte_idx].encode_utf16().count() as u32
}
#[cfg(test)]
mod tests {
use crate::prelude::modifiers_from_mouse_buttons;
use bevy::prelude::*;
#[test]
fn test_modifiers_from_mouse_buttons() {
let buttons = vec![&MouseButton::Left, &MouseButton::Right];
let modifiers = modifiers_from_mouse_buttons(buttons);
assert_eq!(
modifiers,
cef_dll_sys::cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32
| cef_dll_sys::cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32
);
}
}

View File

@@ -0,0 +1,57 @@
use cef::rc::{Rc, RcImpl};
use cef::{Browser, ImplRenderHandler, Rect, RenderHandler, WrapRenderHandler, sys};
/// ## Reference
///
/// - [`CefRenderHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html)
pub struct DevToolRenderHandlerBuilder {
object: *mut RcImpl<sys::cef_render_handler_t, Self>,
}
impl DevToolRenderHandlerBuilder {
pub fn build() -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl Rc for DevToolRenderHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderHandler for DevToolRenderHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for DevToolRenderHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRenderHandler for DevToolRenderHandlerBuilder {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
rect.width = 800;
rect.height = 800;
}
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,979 @@
//! ## Reference
//!
//! - [`cef_key_event_t`](https://cef-builds.spotifycdn.com/docs/106.1/structcef__key__event__t.html)
//! - [KeyboardCodes](https://chromium.googlesource.com/external/Webkit/+/safari-4-branch/WebCore/platform/KeyboardCodes.h)
use bevy::input::ButtonState;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::{ButtonInput, KeyCode};
use cef_dll_sys::{cef_event_flags_t, cef_key_event_t, cef_key_event_type_t};
pub fn keyboard_modifiers(input: &ButtonInput<KeyCode>) -> u32 {
let mut flags = 0u32;
if input.pressed(KeyCode::ControlLeft) || input.pressed(KeyCode::ControlRight) {
flags |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input.pressed(KeyCode::AltLeft) || input.pressed(KeyCode::AltRight) {
flags |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input.pressed(KeyCode::ShiftLeft) || input.pressed(KeyCode::ShiftRight) {
flags |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input.pressed(KeyCode::SuperLeft) || input.pressed(KeyCode::SuperRight) {
flags |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input.pressed(KeyCode::CapsLock) {
flags |= cef_event_flags_t::EVENTFLAG_CAPS_LOCK_ON as u32;
}
if input.pressed(KeyCode::NumLock) {
flags |= cef_event_flags_t::EVENTFLAG_NUM_LOCK_ON as u32;
}
flags
}
pub fn create_cef_key_event(
modifiers: u32,
_input: &ButtonInput<KeyCode>,
key_event: &KeyboardInput,
) -> Option<cef::KeyEvent> {
let key_type = match key_event.state {
// ButtonState::Pressed if input.just_pressed(key_event.key_code) => {
// cef_key_event_type_t::KEYEVENT_RAWKEYDOWN
// }
ButtonState::Pressed => cef_key_event_type_t::KEYEVENT_CHAR,
ButtonState::Released => cef_key_event_type_t::KEYEVENT_KEYUP,
};
let windows_key_code = keycode_to_windows_vk(key_event.key_code);
let character = key_event
.text
.as_ref()
.and_then(|text| text.chars().next())
.unwrap_or('\0') as u16;
Some(cef::KeyEvent::from(cef_key_event_t {
size: core::mem::size_of::<cef_key_event_t>(),
type_: key_type,
modifiers,
windows_key_code,
native_key_code: to_native_key_code(&key_event.key_code) as _,
character,
unmodified_character: character,
is_system_key: false as _,
focus_on_editable_field: false as _,
}))
}
// fn is_not_character_key_code(keycode: &KeyCode) -> bool {
// match keycode {
// // Function keys are not character keys
// KeyCode::F1
// | KeyCode::F2
// | KeyCode::F3
// | KeyCode::F4
// | KeyCode::F5
// | KeyCode::F6
// | KeyCode::F7
// | KeyCode::F8
// | KeyCode::F9
// | KeyCode::F10
// | KeyCode::F11
// | KeyCode::F12 => true,
//
// // Navigation keys are not character keys
// KeyCode::ArrowLeft
// | KeyCode::ArrowUp
// | KeyCode::ArrowRight
// | KeyCode::ArrowDown
// | KeyCode::Home
// | KeyCode::End
// | KeyCode::PageUp
// | KeyCode::PageDown => true,
//
// // Modifier keys are not character keys
// KeyCode::ShiftLeft
// | KeyCode::ShiftRight
// | KeyCode::ControlLeft
// | KeyCode::ControlRight
// | KeyCode::AltLeft
// | KeyCode::AltRight
// | KeyCode::SuperLeft
// | KeyCode::SuperRight => true,
//
// // Lock keys are not character keys
// KeyCode::CapsLock | KeyCode::NumLock | KeyCode::ScrollLock => true,
//
// // Special control keys are not character keys
// KeyCode::Escape
// | KeyCode::Tab
// | KeyCode::Enter
// | KeyCode::Backspace
// | KeyCode::Delete
// | KeyCode::Insert => true,
//
// // All other keys (letters, numbers, punctuation, space, numpad) are character keys
// _ => false,
// }
// }
fn keycode_to_windows_vk(keycode: KeyCode) -> i32 {
match keycode {
// Letters
KeyCode::KeyA => 0x41,
KeyCode::KeyB => 0x42,
KeyCode::KeyC => 0x43,
KeyCode::KeyD => 0x44,
KeyCode::KeyE => 0x45,
KeyCode::KeyF => 0x46,
KeyCode::KeyG => 0x47,
KeyCode::KeyH => 0x48,
KeyCode::KeyI => 0x49,
KeyCode::KeyJ => 0x4A,
KeyCode::KeyK => 0x4B,
KeyCode::KeyL => 0x4C,
KeyCode::KeyM => 0x4D,
KeyCode::KeyN => 0x4E,
KeyCode::KeyO => 0x4F,
KeyCode::KeyP => 0x50,
KeyCode::KeyQ => 0x51,
KeyCode::KeyR => 0x52,
KeyCode::KeyS => 0x53,
KeyCode::KeyT => 0x54,
KeyCode::KeyU => 0x55,
KeyCode::KeyV => 0x56,
KeyCode::KeyW => 0x57,
KeyCode::KeyX => 0x58,
KeyCode::KeyY => 0x59,
KeyCode::KeyZ => 0x5A,
// Numbers
KeyCode::Digit0 => 0x30,
KeyCode::Digit1 => 0x31,
KeyCode::Digit2 => 0x32,
KeyCode::Digit3 => 0x33,
KeyCode::Digit4 => 0x34,
KeyCode::Digit5 => 0x35,
KeyCode::Digit6 => 0x36,
KeyCode::Digit7 => 0x37,
KeyCode::Digit8 => 0x38,
KeyCode::Digit9 => 0x39,
// Function keys
KeyCode::F1 => 0x70,
KeyCode::F2 => 0x71,
KeyCode::F3 => 0x72,
KeyCode::F4 => 0x73,
KeyCode::F5 => 0x74,
KeyCode::F6 => 0x75,
KeyCode::F7 => 0x76,
KeyCode::F8 => 0x77,
KeyCode::F9 => 0x78,
KeyCode::F10 => 0x79,
KeyCode::F11 => 0x7A,
KeyCode::F12 => 0x7B,
// Special keys
KeyCode::Enter => 0x0D,
KeyCode::Space => 0x20,
KeyCode::Backspace => 0x08,
KeyCode::Delete => 0x2E,
KeyCode::Tab => 0x09,
KeyCode::Escape => 0x1B,
KeyCode::Insert => 0x2D,
KeyCode::Home => 0x24,
KeyCode::End => 0x23,
KeyCode::PageUp => 0x21,
KeyCode::PageDown => 0x22,
// Arrow keys
KeyCode::ArrowLeft => 0x25,
KeyCode::ArrowUp => 0x26,
KeyCode::ArrowRight => 0x27,
KeyCode::ArrowDown => 0x28,
// Modifier keys
KeyCode::ShiftLeft | KeyCode::ShiftRight => 0x10,
KeyCode::ControlLeft | KeyCode::ControlRight => 0x11,
KeyCode::AltLeft | KeyCode::AltRight => 0x12,
KeyCode::SuperLeft => 0x5B, // Left Windows key
KeyCode::SuperRight => 0x5C, // Right Windows key
// Lock keys
KeyCode::CapsLock => 0x14,
KeyCode::NumLock => 0x90,
KeyCode::ScrollLock => 0x91,
// Punctuation
KeyCode::Semicolon => 0xBA,
KeyCode::Equal => 0xBB,
KeyCode::Comma => 0xBC,
KeyCode::Minus => 0xBD,
KeyCode::Period => 0xBE,
KeyCode::Slash => 0xBF,
KeyCode::Backquote => 0xC0,
KeyCode::BracketLeft => 0xDB,
KeyCode::Backslash => 0xDC,
KeyCode::BracketRight => 0xDD,
KeyCode::Quote => 0xDE,
// Numpad
KeyCode::Numpad0 => 0x60,
KeyCode::Numpad1 => 0x61,
KeyCode::Numpad2 => 0x62,
KeyCode::Numpad3 => 0x63,
KeyCode::Numpad4 => 0x64,
KeyCode::Numpad5 => 0x65,
KeyCode::Numpad6 => 0x66,
KeyCode::Numpad7 => 0x67,
KeyCode::Numpad8 => 0x68,
KeyCode::Numpad9 => 0x69,
KeyCode::NumpadMultiply => 0x6A,
KeyCode::NumpadAdd => 0x6B,
KeyCode::NumpadSubtract => 0x6D,
KeyCode::NumpadDecimal => 0x6E,
KeyCode::NumpadDivide => 0x6F,
// Default case for unhandled keys
_ => 0,
}
}
// fn is_special_key(keycode: &KeyCode) -> bool {
// matches!(
// keycode,
// KeyCode::Enter
// | KeyCode::Space
// | KeyCode::Backspace
// | KeyCode::Delete
// | KeyCode::Tab
// | KeyCode::Escape
// | KeyCode::Insert
// | KeyCode::Home
// | KeyCode::End
// | KeyCode::PageUp
// | KeyCode::PageDown
// )
// }
/// Native key codes for different platforms based on MDN documentation
/// [`Keyboard_event_key_values`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
fn to_native_key_code(keycode: &KeyCode) -> u32 {
match keycode {
// Letters - Platform specific native codes
KeyCode::KeyA => {
if cfg!(target_os = "macos") {
0x00
} else {
0x41
} // Linux/default
}
KeyCode::KeyB => {
if cfg!(target_os = "macos") {
0x0B
} else {
0x42
}
}
KeyCode::KeyC => {
if cfg!(target_os = "macos") {
0x08
} else {
0x43
}
}
KeyCode::KeyD => {
if cfg!(target_os = "macos") {
0x02
} else {
0x44
}
}
KeyCode::KeyE => {
if cfg!(target_os = "macos") {
0x0E
} else {
0x45
}
}
KeyCode::KeyF => {
if cfg!(target_os = "macos") {
0x03
} else {
0x46
}
}
KeyCode::KeyG => {
if cfg!(target_os = "macos") {
0x05
} else {
0x47
}
}
KeyCode::KeyH => {
if cfg!(target_os = "macos") {
0x04
} else {
0x48
}
}
KeyCode::KeyI => {
if cfg!(target_os = "macos") {
0x22
} else {
0x49
}
}
KeyCode::KeyJ => {
if cfg!(target_os = "macos") {
0x26
} else {
0x4A
}
}
KeyCode::KeyK => {
if cfg!(target_os = "macos") {
0x28
} else {
0x4B
}
}
KeyCode::KeyL => {
if cfg!(target_os = "macos") {
0x25
} else {
0x4C
}
}
KeyCode::KeyM => {
if cfg!(target_os = "macos") {
0x2E
} else {
0x4D
}
}
KeyCode::KeyN => {
if cfg!(target_os = "macos") {
0x2D
} else {
0x4E
}
}
KeyCode::KeyO => {
if cfg!(target_os = "macos") {
0x1F
} else {
0x4F
}
}
KeyCode::KeyP => {
if cfg!(target_os = "macos") {
0x23
} else {
0x50
}
}
KeyCode::KeyQ => {
if cfg!(target_os = "macos") {
0x0C
} else {
0x51
}
}
KeyCode::KeyR => {
if cfg!(target_os = "macos") {
0x0F
} else {
0x52
}
}
KeyCode::KeyS => {
if cfg!(target_os = "macos") {
0x01
} else {
0x53
}
}
KeyCode::KeyT => {
if cfg!(target_os = "macos") {
0x11
} else {
0x54
}
}
KeyCode::KeyU => {
if cfg!(target_os = "macos") {
0x20
} else {
0x55
}
}
KeyCode::KeyV => {
if cfg!(target_os = "macos") {
0x09
} else {
0x56
}
}
KeyCode::KeyW => {
if cfg!(target_os = "macos") {
0x0D
} else {
0x57
}
}
KeyCode::KeyX => {
if cfg!(target_os = "macos") {
0x07
} else {
0x58
}
}
KeyCode::KeyY => {
if cfg!(target_os = "macos") {
0x10
} else {
0x59
}
}
KeyCode::KeyZ => {
if cfg!(target_os = "macos") {
0x06
} else {
0x5A
}
}
// Numbers
KeyCode::Digit0 => {
if cfg!(target_os = "macos") {
0x1D
} else {
0x30
}
}
KeyCode::Digit1 => {
if cfg!(target_os = "macos") {
0x12
} else {
0x31
}
}
KeyCode::Digit2 => {
if cfg!(target_os = "macos") {
0x13
} else {
0x32
}
}
KeyCode::Digit3 => {
if cfg!(target_os = "macos") {
0x14
} else {
0x33
}
}
KeyCode::Digit4 => {
if cfg!(target_os = "macos") {
0x15
} else {
0x34
}
}
KeyCode::Digit5 => {
if cfg!(target_os = "macos") {
0x17
} else {
0x35
}
}
KeyCode::Digit6 => {
if cfg!(target_os = "macos") {
0x16
} else {
0x36
}
}
KeyCode::Digit7 => {
if cfg!(target_os = "macos") {
0x1A
} else {
0x37
}
}
KeyCode::Digit8 => {
if cfg!(target_os = "macos") {
0x1C
} else {
0x38
}
}
KeyCode::Digit9 => {
if cfg!(target_os = "macos") {
0x19
} else {
0x39
}
}
// Function keys
KeyCode::F1 => {
if cfg!(target_os = "macos") {
0x7A
} else {
0x70
}
}
KeyCode::F2 => {
if cfg!(target_os = "macos") {
0x78
} else {
0x71
}
}
KeyCode::F3 => {
if cfg!(target_os = "macos") {
0x63
} else {
0x72
}
}
KeyCode::F4 => {
if cfg!(target_os = "macos") {
0x76
} else {
0x73
}
}
KeyCode::F5 => {
if cfg!(target_os = "macos") {
0x60
} else {
0x74
}
}
KeyCode::F6 => {
if cfg!(target_os = "macos") {
0x61
} else {
0x75
}
}
KeyCode::F7 => {
if cfg!(target_os = "macos") {
0x62
} else {
0x76
}
}
KeyCode::F8 => {
if cfg!(target_os = "macos") {
0x64
} else {
0x77
}
}
KeyCode::F9 => {
if cfg!(target_os = "macos") {
0x65
} else {
0x78
}
}
KeyCode::F10 => {
if cfg!(target_os = "macos") {
0x6D
} else {
0x79
}
}
KeyCode::F11 => {
if cfg!(target_os = "macos") {
0x67
} else {
0x7A
}
}
KeyCode::F12 => {
if cfg!(target_os = "macos") {
0x6F
} else {
0x7B
}
}
// Special keys
KeyCode::Enter => {
if cfg!(target_os = "macos") {
0x24
} else {
0x0D
}
}
KeyCode::Space => {
if cfg!(target_os = "macos") {
0x31
} else {
0x20
}
}
KeyCode::Backspace => {
if cfg!(target_os = "macos") {
0x33
} else {
0x08
}
}
KeyCode::Delete => {
if cfg!(target_os = "macos") {
0x75
} else {
0x2E
}
}
KeyCode::Tab => {
if cfg!(target_os = "macos") {
0x30
} else {
0x09
}
}
KeyCode::Escape => {
if cfg!(target_os = "macos") {
0x35
} else {
0x1B
}
}
KeyCode::Insert => {
if cfg!(target_os = "macos") {
0x72
} else {
0x2D
}
}
KeyCode::Home => {
if cfg!(target_os = "macos") {
0x73
} else {
0x24
}
}
KeyCode::End => {
if cfg!(target_os = "macos") {
0x77
} else {
0x23
}
}
KeyCode::PageUp => {
if cfg!(target_os = "macos") {
0x74
} else {
0x21
}
}
KeyCode::PageDown => {
if cfg!(target_os = "macos") {
0x79
} else {
0x22
}
}
// Arrow keys
KeyCode::ArrowLeft => {
if cfg!(target_os = "macos") {
0x7B
} else {
0x25
}
}
KeyCode::ArrowUp => {
if cfg!(target_os = "macos") {
0x7E
} else {
0x26
}
}
KeyCode::ArrowRight => {
if cfg!(target_os = "macos") {
0x7C
} else {
0x27
}
}
KeyCode::ArrowDown => {
if cfg!(target_os = "macos") {
0x7D
} else {
0x28
}
}
// Modifier keys
KeyCode::ShiftLeft => {
if cfg!(target_os = "macos") {
0x38
} else {
0xA0
}
}
KeyCode::ShiftRight => {
if cfg!(target_os = "macos") {
0x3C
} else {
0xA1
}
}
KeyCode::ControlLeft => {
if cfg!(target_os = "macos") {
0x3B
} else {
0xA2
}
}
KeyCode::ControlRight => {
if cfg!(target_os = "macos") {
0x3E
} else {
0xA3
}
}
KeyCode::AltLeft => {
if cfg!(target_os = "macos") {
0x3A
} else {
0xA4
}
}
KeyCode::AltRight => {
if cfg!(target_os = "macos") {
0x3D
} else {
0xA5
}
}
KeyCode::SuperLeft => {
if cfg!(target_os = "macos") {
0x37
} else {
0x5B
}
}
KeyCode::SuperRight => {
if cfg!(target_os = "macos") {
0x36
} else {
0x5C
}
}
// Lock keys
KeyCode::CapsLock => {
if cfg!(target_os = "macos") {
0x39
} else {
0x14
}
}
KeyCode::NumLock => {
if cfg!(target_os = "macos") {
0x47
} else {
0x90
}
}
KeyCode::ScrollLock => 0x91,
// Punctuation
KeyCode::Semicolon => {
if cfg!(target_os = "macos") {
0x29
} else {
0xBA
}
}
KeyCode::Equal => {
if cfg!(target_os = "macos") {
0x18
} else {
0xBB
}
}
KeyCode::Comma => {
if cfg!(target_os = "macos") {
0x2B
} else {
0xBC
}
}
KeyCode::Minus => {
if cfg!(target_os = "macos") {
0x1B
} else {
0xBD
}
}
KeyCode::Period => {
if cfg!(target_os = "macos") {
0x2F
} else {
0xBE
}
}
KeyCode::Slash => {
if cfg!(target_os = "macos") {
0x2C
} else {
0xBF
}
}
KeyCode::Backquote => {
if cfg!(target_os = "macos") {
0x32
} else {
0xC0
}
}
KeyCode::BracketLeft => {
if cfg!(target_os = "macos") {
0x21
} else {
0xDB
}
}
KeyCode::Backslash => {
if cfg!(target_os = "macos") {
0x2A
} else {
0xDC
}
}
KeyCode::BracketRight => {
if cfg!(target_os = "macos") {
0x1E
} else {
0xDD
}
}
KeyCode::Quote => {
if cfg!(target_os = "macos") {
0x27
} else {
0xDE
}
}
// Numpad
KeyCode::Numpad0 => {
if cfg!(target_os = "macos") {
0x52
} else {
0x60
}
}
KeyCode::Numpad1 => {
if cfg!(target_os = "macos") {
0x53
} else {
0x61
}
}
KeyCode::Numpad2 => {
if cfg!(target_os = "macos") {
0x54
} else {
0x62
}
}
KeyCode::Numpad3 => {
if cfg!(target_os = "macos") {
0x55
} else {
0x63
}
}
KeyCode::Numpad4 => {
if cfg!(target_os = "macos") {
0x56
} else {
0x64
}
}
KeyCode::Numpad5 => {
if cfg!(target_os = "macos") {
0x57
} else {
0x65
}
}
KeyCode::Numpad6 => {
if cfg!(target_os = "macos") {
0x58
} else {
0x66
}
}
KeyCode::Numpad7 => {
if cfg!(target_os = "macos") {
0x59
} else {
0x67
}
}
KeyCode::Numpad8 => {
if cfg!(target_os = "macos") {
0x5B
} else {
0x68
}
}
KeyCode::Numpad9 => {
if cfg!(target_os = "macos") {
0x5C
} else {
0x69
}
}
KeyCode::NumpadMultiply => {
if cfg!(target_os = "macos") {
0x43
} else {
0x6A
}
}
KeyCode::NumpadAdd => {
if cfg!(target_os = "macos") {
0x45
} else {
0x6B
}
}
KeyCode::NumpadSubtract => {
if cfg!(target_os = "macos") {
0x4E
} else {
0x6D
}
}
KeyCode::NumpadDecimal => {
if cfg!(target_os = "macos") {
0x41
} else {
0x6E
}
}
KeyCode::NumpadDivide => {
if cfg!(target_os = "macos") {
0x4B
} else {
0x6F
}
}
// Default case for unhandled keys
_ => 0,
}
}

View File

@@ -0,0 +1,129 @@
mod brp_handler;
mod js_emit_event_handler;
use crate::browser_process::ContextMenuHandlerBuilder;
use crate::prelude::IntoString;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, Client, ContextMenuHandler, DisplayHandler, Frame, ImplClient, ImplProcessMessage,
ListValue, ProcessId, ProcessMessage, RenderHandler, WrapClient, sys,
};
use std::os::raw::c_int;
pub use brp_handler::BrpHandler;
pub use js_emit_event_handler::{IpcEventRaw, JsEmitEventHandler};
pub trait ProcessMessageHandler {
fn process_name(&self) -> &'static str;
fn handle_message(&self, browser: &mut Browser, frame: &mut Frame, args: Option<ListValue>);
}
/// ## Reference
///
/// - [`CefBrowser Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowser.html)
pub struct ClientHandlerBuilder {
object: *mut RcImpl<sys::cef_client_t, Self>,
render_handler: RenderHandler,
context_menu_handler: ContextMenuHandler,
message_handlers: Vec<std::rc::Rc<dyn ProcessMessageHandler>>,
display_handler: Option<DisplayHandler>,
}
impl ClientHandlerBuilder {
pub fn new(render_handler: RenderHandler) -> Self {
Self {
object: std::ptr::null_mut(),
render_handler,
context_menu_handler: ContextMenuHandlerBuilder::build(),
message_handlers: Vec::new(),
display_handler: None,
}
}
pub fn with_display_handler(mut self, display_handler: DisplayHandler) -> Self {
self.display_handler = Some(display_handler);
self
}
pub fn with_message_handler(mut self, handler: impl ProcessMessageHandler + 'static) -> Self {
self.message_handlers.push(std::rc::Rc::new(handler));
self
}
pub fn build(self) -> Client {
Client::new(self)
}
}
impl Rc for ClientHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapClient for ClientHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_client_t, Self>) {
self.object = object;
}
}
impl Clone for ClientHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
render_handler: self.render_handler.clone(),
context_menu_handler: self.context_menu_handler.clone(),
message_handlers: self.message_handlers.clone(),
display_handler: self.display_handler.clone(),
}
}
}
impl ImplClient for ClientHandlerBuilder {
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn display_handler(&self) -> Option<DisplayHandler> {
self.display_handler.clone()
}
fn on_process_message_received(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_: ProcessId,
message: Option<&mut ProcessMessage>,
) -> c_int {
if let Some(message) = message
&& let Some(browser) = browser
&& let Some(frame) = frame
&& let Some(name) = Some(message.name().into_string())
&& let Some(handler) = self
.message_handlers
.iter()
.find(|h| h.process_name() == name.as_str())
{
{
let args = message.argument_list();
handler.handle_message(browser, frame, args);
}
};
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_client_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,68 @@
use crate::browser_process::client_handler::ProcessMessageHandler;
use crate::prelude::PROCESS_MESSAGE_BRP;
use crate::util::IntoString;
use async_channel::Sender;
use bevy::tasks::IoTaskPool;
use bevy_remote::{BrpMessage, BrpRequest};
use cef::{
Browser, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ListValue, ProcessId,
process_message_create,
};
use cef_dll_sys::cef_process_id_t;
pub struct BrpHandler {
sender: Sender<BrpMessage>,
}
impl BrpHandler {
pub const fn new(sender: Sender<BrpMessage>) -> Self {
Self { sender }
}
}
impl ProcessMessageHandler for BrpHandler {
fn process_name(&self) -> &'static str {
PROCESS_MESSAGE_BRP
}
fn handle_message(&self, _browser: &mut Browser, frame: &mut Frame, args: Option<ListValue>) {
if let Some(args) = args
&& let Ok(request) = serde_json::from_str::<BrpRequest>(&args.string(1).into_string())
{
let id = args.string(0).into_string();
let frame = frame.clone();
let brp_sender = self.sender.clone();
IoTaskPool::get()
.spawn(async move {
let (tx, rx) = async_channel::unbounded();
if brp_sender
.send(BrpMessage {
method: request.method,
params: request.params,
sender: tx,
})
.await
.is_err()
{
return;
}
if let Ok(result) = rx.recv().await
&& let Some(mut message) =
process_message_create(Some(&PROCESS_MESSAGE_BRP.into()))
&& let Some(argument_list) = message.argument_list()
{
argument_list.set_string(0, Some(&id.as_str().into()));
argument_list.set_string(
1,
Some(&serde_json::to_string(&result).unwrap().as_str().into()),
);
frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_RENDERER),
Some(&mut message),
);
}
})
.detach();
}
}
}

View File

@@ -0,0 +1,39 @@
use crate::browser_process::client_handler::ProcessMessageHandler;
use crate::prelude::{IntoString, PROCESS_MESSAGE_JS_EMIT};
use async_channel::Sender;
use bevy::prelude::Entity;
use cef::{Browser, Frame, ImplListValue, ListValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct IpcEventRaw {
pub webview: Entity,
pub payload: String,
}
pub struct JsEmitEventHandler {
webview: Entity,
sender: Sender<IpcEventRaw>,
}
impl JsEmitEventHandler {
pub const fn new(webview: Entity, sender: Sender<IpcEventRaw>) -> Self {
Self { sender, webview }
}
}
impl ProcessMessageHandler for JsEmitEventHandler {
fn process_name(&self) -> &'static str {
PROCESS_MESSAGE_JS_EMIT
}
fn handle_message(&self, _browser: &mut Browser, _frame: &mut Frame, args: Option<ListValue>) {
if let Some(args) = args {
let event = IpcEventRaw {
webview: self.webview, // Placeholder, should be set correctly
payload: args.string(0).into_string(),
};
let _ = self.sender.send_blocking(event);
}
}
}

View File

@@ -0,0 +1,50 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplContextMenuHandler, WrapContextMenuHandler, sys};
/// ## Reference
///
/// - [`CefContextMenuHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefContextMenuHandler.html)
pub struct ContextMenuHandlerBuilder {
object: *mut RcImpl<sys::_cef_context_menu_handler_t, Self>,
}
impl ContextMenuHandlerBuilder {
pub fn build() -> cef::ContextMenuHandler {
cef::ContextMenuHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapContextMenuHandler for ContextMenuHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_context_menu_handler_t, Self>) {
self.object = object;
}
}
impl Rc for ContextMenuHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for ContextMenuHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplContextMenuHandler for ContextMenuHandlerBuilder {
#[inline]
fn get_raw(&self) -> *mut sys::cef_context_menu_handler_t {
self.object as *mut sys::cef_context_menu_handler_t
}
}

View File

@@ -0,0 +1,165 @@
use async_channel::Sender;
use bevy::log::{error, info, trace, warn};
use bevy::window::SystemCursorIcon;
use cef::rc::{ConvertParam, Rc, RcImpl};
use cef::{
Browser, CefString, CursorInfo, CursorType, ImplDisplayHandler, LogSeverity,
WrapDisplayHandler, sys,
};
use cef_dll_sys::{cef_cursor_type_t, cef_log_severity_t};
use std::os::raw::c_int;
pub type SystemCursorIconSenderInner = Sender<SystemCursorIcon>;
/// ## Reference
///
/// - [`CefDisplayHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/112.3/classCefDisplayHandler.html#af1cc8410a0b1a97166923428d3794636)
pub struct DisplayHandlerBuilder {
object: *mut RcImpl<sys::cef_display_handler_t, Self>,
cursor_icon: SystemCursorIconSenderInner,
}
impl DisplayHandlerBuilder {
pub fn build(cursor_icon: SystemCursorIconSenderInner) -> cef::DisplayHandler {
cef::DisplayHandler::new(Self {
object: core::ptr::null_mut(),
cursor_icon,
})
}
}
impl Rc for DisplayHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for DisplayHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
cursor_icon: self.cursor_icon.clone(),
}
}
}
impl WrapDisplayHandler for DisplayHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_display_handler_t, Self>) {
self.object = object;
}
}
impl ImplDisplayHandler for DisplayHandlerBuilder {
fn on_console_message(
&self,
_: Option<&mut Browser>,
level: LogSeverity,
message: Option<&CefString>,
source: Option<&CefString>,
line: c_int,
) -> c_int {
let message = format!(
"{}\nline:{line}\n{}",
source.map(|s| s.to_string()).unwrap_or_default(),
message.map(|m| m.to_string()).unwrap_or_default()
);
match level.into_raw() {
cef_log_severity_t::LOGSEVERITY_ERROR => {
error!("{message}");
}
cef_log_severity_t::LOGSEVERITY_WARNING => {
warn!("{message}");
}
cef_log_severity_t::LOGSEVERITY_VERBOSE => {
trace!("{message}");
}
_ => {
info!("{message}");
}
}
1
}
fn on_cursor_change(
&self,
_browser: Option<&mut Browser>,
_cursor: *mut u8,
type_: CursorType,
_: Option<&CursorInfo>,
) -> c_int {
let _ = self
.cursor_icon
.send_blocking(to_system_cursor_icon(type_.into_raw()));
1
}
#[inline]
fn get_raw(&self) -> *mut sys::cef_display_handler_t {
self.object.cast()
}
}
pub fn to_system_cursor_icon(cursor_type: cef_dll_sys::cef_cursor_type_t) -> SystemCursorIcon {
match cursor_type {
cef_cursor_type_t::CT_POINTER => SystemCursorIcon::Default,
cef_cursor_type_t::CT_CROSS => SystemCursorIcon::Crosshair,
cef_cursor_type_t::CT_HAND => SystemCursorIcon::Pointer,
cef_cursor_type_t::CT_IBEAM => SystemCursorIcon::Text,
cef_cursor_type_t::CT_WAIT => SystemCursorIcon::Wait,
cef_cursor_type_t::CT_HELP => SystemCursorIcon::Help,
cef_cursor_type_t::CT_EASTRESIZE => SystemCursorIcon::EResize,
cef_cursor_type_t::CT_NORTHRESIZE => SystemCursorIcon::NResize,
cef_cursor_type_t::CT_NORTHEASTRESIZE => SystemCursorIcon::NeResize,
cef_cursor_type_t::CT_NORTHWESTRESIZE => SystemCursorIcon::NwResize,
cef_cursor_type_t::CT_SOUTHRESIZE => SystemCursorIcon::SResize,
cef_cursor_type_t::CT_SOUTHEASTRESIZE => SystemCursorIcon::SeResize,
cef_cursor_type_t::CT_SOUTHWESTRESIZE => SystemCursorIcon::SwResize,
cef_cursor_type_t::CT_WESTRESIZE => SystemCursorIcon::WResize,
cef_cursor_type_t::CT_NORTHSOUTHRESIZE => SystemCursorIcon::NsResize,
cef_cursor_type_t::CT_EASTWESTRESIZE => SystemCursorIcon::EwResize,
cef_cursor_type_t::CT_NORTHEASTSOUTHWESTRESIZE => SystemCursorIcon::NeswResize,
cef_cursor_type_t::CT_NORTHWESTSOUTHEASTRESIZE => SystemCursorIcon::NwseResize,
cef_cursor_type_t::CT_COLUMNRESIZE => SystemCursorIcon::ColResize,
cef_cursor_type_t::CT_ROWRESIZE => SystemCursorIcon::RowResize,
cef_cursor_type_t::CT_MIDDLEPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_EASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHEASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHWESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHEASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHWESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_WESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_MOVE => SystemCursorIcon::Move,
cef_cursor_type_t::CT_VERTICALTEXT => SystemCursorIcon::VerticalText,
cef_cursor_type_t::CT_CELL => SystemCursorIcon::Cell,
cef_cursor_type_t::CT_CONTEXTMENU => SystemCursorIcon::ContextMenu,
cef_cursor_type_t::CT_ALIAS => SystemCursorIcon::Alias,
cef_cursor_type_t::CT_PROGRESS => SystemCursorIcon::Progress,
cef_cursor_type_t::CT_NODROP => SystemCursorIcon::NoDrop,
cef_cursor_type_t::CT_COPY => SystemCursorIcon::Copy,
cef_cursor_type_t::CT_NONE => SystemCursorIcon::Default,
cef_cursor_type_t::CT_NOTALLOWED => SystemCursorIcon::NotAllowed,
cef_cursor_type_t::CT_ZOOMIN => SystemCursorIcon::ZoomIn,
cef_cursor_type_t::CT_ZOOMOUT => SystemCursorIcon::ZoomOut,
cef_cursor_type_t::CT_GRAB => SystemCursorIcon::Grab,
cef_cursor_type_t::CT_GRABBING => SystemCursorIcon::Grabbing,
cef_cursor_type_t::CT_MIDDLE_PANNING_VERTICAL => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_MIDDLE_PANNING_HORIZONTAL => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_CUSTOM => SystemCursorIcon::Default,
cef_cursor_type_t::CT_DND_NONE => SystemCursorIcon::Default,
cef_cursor_type_t::CT_DND_MOVE => SystemCursorIcon::Move,
cef_cursor_type_t::CT_DND_COPY => SystemCursorIcon::Copy,
cef_cursor_type_t::CT_DND_LINK => SystemCursorIcon::Alias,
cef_cursor_type_t::CT_NUM_VALUES => SystemCursorIcon::Default,
_ => SystemCursorIcon::Default,
}
}

View File

@@ -0,0 +1,281 @@
mod data_responser;
mod headers_responser;
use crate::browser_process::localhost::data_responser::{DataResponser, parse_bytes_single_range};
use crate::browser_process::localhost::headers_responser::HeadersResponser;
use crate::prelude::IntoString;
use async_channel::{Receiver, Sender};
use bevy::asset::Asset;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, Callback, CefString, Frame, ImplCallback, ImplRequest, ImplResourceHandler,
ImplResponse, ImplSchemeHandlerFactory, Request, ResourceHandler, ResourceReadCallback,
Response, SchemeHandlerFactory, WrapResourceHandler, WrapSchemeHandlerFactory, sys,
};
use cef_dll_sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
use serde::{Deserialize, Serialize};
use std::os::raw::c_int;
use std::sync::{Arc, Mutex};
/// `cef://` scheme response asset.
#[derive(Asset, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Debug, Serialize, Deserialize)]
pub struct CefResponse {
/// The media type.
pub mime_type: String,
/// The status code of the response, e.g., 200 for OK, 404 for Not Found.
pub status_code: u32,
/// The response data, typically HTML or other content.
pub data: Vec<u8>,
}
impl Default for CefResponse {
fn default() -> Self {
Self {
mime_type: "text/html".to_string(),
status_code: 404,
data: b"<!DOCTYPE html><html><body><h1>404 Not Found</h1></body></html>".to_vec(),
}
}
}
#[derive(Debug, Clone, Component)]
pub struct Responser(pub Sender<CefResponse>);
#[derive(Resource, Debug, Clone, Deref)]
pub struct Requester(pub Sender<CefRequest>);
#[derive(Resource, Debug, Clone)]
pub struct RequesterReceiver(pub Receiver<CefRequest>);
#[derive(Debug, Clone)]
pub struct CefRequest {
pub uri: String,
pub responser: Responser,
}
/// Use to register a local schema handler for the CEF browser.
///
/// ## Reference
///
/// - [`CefSchemeHandlerFactory Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefSchemeHandlerFactory.html)
pub struct LocalSchemaHandlerBuilder {
object: *mut RcImpl<sys::_cef_scheme_handler_factory_t, Self>,
requester: Requester,
}
impl LocalSchemaHandlerBuilder {
pub fn build(requester: Requester) -> SchemeHandlerFactory {
SchemeHandlerFactory::new(Self {
object: std::ptr::null_mut(),
requester,
})
}
}
impl Rc for LocalSchemaHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapSchemeHandlerFactory for LocalSchemaHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
impl Clone for LocalSchemaHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
requester: self.requester.clone(),
}
}
}
impl ImplSchemeHandlerFactory for LocalSchemaHandlerBuilder {
fn create(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_scheme_name: Option<&CefString>,
_request: Option<&mut Request>,
) -> Option<ResourceHandler> {
Some(LocalResourceHandlerBuilder::build(self.requester.clone()))
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_scheme_handler_factory_t {
self.object.cast()
}
}
struct LocalResourceHandlerBuilder {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
requester: Requester,
headers: Arc<Mutex<HeadersResponser>>,
data: Arc<Mutex<DataResponser>>,
}
impl LocalResourceHandlerBuilder {
fn build(requester: Requester) -> ResourceHandler {
ResourceHandler::new(Self {
object: std::ptr::null_mut(),
requester,
headers: Arc::new(Mutex::new(HeadersResponser::default())),
data: Arc::new(Mutex::new(DataResponser::default())),
})
}
}
impl WrapResourceHandler for LocalResourceHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_resource_handler_t, Self>) {
self.object = object;
}
}
impl Clone for LocalResourceHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
requester: self.requester.clone(),
headers: self.headers.clone(),
data: self.data.clone(),
}
}
}
impl Rc for LocalResourceHandlerBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplResourceHandler for LocalResourceHandlerBuilder {
fn open(
&self,
request: Option<&mut Request>,
handle_request: Option<&mut c_int>,
callback: Option<&mut Callback>,
) -> c_int {
let Some(request) = request else {
// Cancel the request if no request is provided
return 0;
};
let range_header_value = request.header_by_name(Some(&"Range".into())).into_string();
let range = parse_bytes_single_range(&range_header_value);
let Some(callback) = callback.cloned() else {
// If no callback is provided, we cannot handle the request
return 0;
};
if let Some(handle_request) = handle_request {
*handle_request = 0;
}
let url = request.url().into_string();
let requester = self.requester.clone();
let headers_responser = self.headers.clone();
let data_responser = self.data.clone();
IoTaskPool::get()
.spawn(async move {
let (tx, rx) = async_channel::bounded(1);
let _ = requester
.send(CefRequest {
uri: url
.strip_prefix("cef://localhost/")
.unwrap_or_default()
.to_string(),
responser: Responser(tx),
})
.await;
let response = rx.recv().await.unwrap_or_default();
headers_responser.lock().unwrap().prepare(&response, &range);
data_responser
.lock()
.unwrap()
.prepare(response.data, &range);
callback.cont();
})
.detach();
1
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
_redirect_url: Option<&mut CefString>,
) {
let Ok(responser) = self.headers.lock() else {
return;
};
if let Some(response) = response {
response.set_mime_type(Some(&responser.mime_type.as_str().into()));
response.set_status(responser.status_code as _);
for (name, value) in &responser.headers {
response.set_header_by_name(
Some(&name.as_str().into()),
Some(&value.as_str().into()),
false as _,
);
}
}
if let Some(response_length) = response_length {
*response_length = responser.response_length as _;
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn read(
&self,
data_out: *mut u8,
bytes_to_read: c_int,
bytes_read: Option<&mut c_int>,
_: Option<&mut ResourceReadCallback>,
) -> c_int {
let Some(bytes_read) = bytes_read else {
// If no bytes_read is provided, we cannot read data
return 0;
};
let Ok(mut responser) = self.data.lock() else {
return 0;
};
match responser.read(bytes_to_read as _) {
Some(data) if !data.is_empty() => {
let n = data.len();
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), data_out, n);
}
*bytes_read = n as i32;
1
}
_ => {
*bytes_read = 0;
0
}
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,236 @@
use bevy::prelude::*;
#[derive(Default, Clone)]
pub struct DataResponser {
data: Vec<u8>,
offset: usize,
end_offset: usize,
}
impl DataResponser {
/// Prepares the data and headers for the response.
///
/// The range header values only support the `bytes` range unit type and single range.
/// TODO: Support multiple ranges.
pub fn prepare(&mut self, data: Vec<u8>, range: &Option<(usize, Option<usize>)>) {
if let Some((start, end)) = range {
self.offset = *start;
self.end_offset = end.unwrap_or(data.len() - 1) + 1;
self.data = data;
} else {
self.offset = 0;
self.end_offset = data.len();
self.data = data;
}
}
pub fn read(&mut self, bytes_to_read: isize) -> Option<&[u8]> {
if self.offset >= self.data.len() {
return None;
}
let start = self.offset;
let end = if bytes_to_read < 0 {
self.data.len()
} else {
(self.offset as isize + bytes_to_read) as usize
};
let end = end.min(self.end_offset);
if start >= end || start >= self.data.len() {
return None;
}
let slice = &self.data[start..end.min(self.data.len())];
self.offset += slice.len();
Some(slice)
}
}
pub fn parse_bytes_single_range(range_header_value: &str) -> Option<(usize, Option<usize>)> {
let ranges = parse_bytes_range(range_header_value)?;
ranges.first().cloned()
}
/// Parses the `Range` header value from a request and returns the start of the range.
///
/// ## Reference
///
/// - [`Range_requests`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests)
fn parse_bytes_range(range_header_value: &str) -> Option<Vec<(usize, Option<usize>)>> {
if !range_header_value.starts_with("bytes=") {
return None;
}
let mut ranges = Vec::new();
let value = range_header_value.trim_start_matches("bytes=");
// bytes=100-200,300-400 => ["100-200", "300-400"]
let byte_ranges = value.split(",");
for range in byte_ranges {
// 100-200 => ["100", "200"]
let mut split = range.split("-");
let start = split.next()?;
let end = split.next();
let start = start.parse::<usize>().ok()?;
let end = end.and_then(|e| e.parse::<usize>().ok());
ranges.push((start, end));
}
Some(ranges)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn range_start_is_none_if_empty() {
assert_eq!(parse_bytes_range(""), None);
}
#[test]
fn range_only_start_offset() {
assert_eq!(parse_bytes_range("bytes=100-"), Some(vec![(100, None)]));
}
#[test]
fn range_one_bytes() {
assert_eq!(
parse_bytes_range("bytes=100-200"),
Some(vec![(100, Some(200))])
);
}
#[test]
fn range_multiple_ranges() {
assert_eq!(
parse_bytes_range("bytes=100-200,300-400"),
Some(vec![(100, Some(200)), (300, Some(400))])
);
}
#[test]
fn data_responser_new_with_start_and_end() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((2, Some(7))));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 2);
assert_eq!(responser.end_offset, 7);
}
#[test]
fn data_responser_new_with_start_only() {
let data = vec![1, 2, 3, 4, 5];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((3, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 3);
assert_eq!(responser.end_offset, 5);
}
#[test]
fn data_responser_new_with_zero_start() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((0, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 0);
assert_eq!(responser.end_offset, 2);
}
#[test]
fn data_responser_new_with_empty_data() {
let data = vec![];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((0, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 0);
assert_eq!(responser.end_offset, 0);
}
#[test]
fn data_responser_new_with_start_beyond_data_length() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((5, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 5);
assert_eq!(responser.end_offset, 3);
}
#[test]
fn data_responser_new_with_end_beyond_data_length() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((1, Some(10))));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 1);
assert_eq!(responser.end_offset, 10);
}
#[test]
fn data_responser_read_no_end_data_smaller_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, None)));
let result = responser.read(10);
assert_eq!(result, Some(&[3, 4, 5][..]));
}
#[test]
fn data_responser_read_no_end_data_larger_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, None)));
let result1 = responser.read(3);
assert_eq!(result1, Some(&[3, 4, 5][..]));
let result2 = responser.read(3);
assert_eq!(result2, Some(&[6, 7, 8][..]));
}
#[test]
fn data_responser_read_with_end_data_smaller_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, Some(6))));
let result = responser.read(10);
assert_eq!(result, Some(&[3, 4, 5, 6][..]));
}
#[test]
fn data_responser_read_with_end_data_larger_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((1, Some(7))));
let result1 = responser.read(3);
assert_eq!(result1, Some(&[2, 3, 4][..]));
let result2 = responser.read(3);
assert_eq!(result2, Some(&[5, 6, 7][..]));
}
#[test]
fn data_responser_read_consecutive_calls_until_end() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((1, Some(6))));
let result1 = responser.read(2);
assert_eq!(result1, Some(&[2, 3][..]));
let result2 = responser.read(2);
assert_eq!(result2, Some(&[4, 5][..]));
let result3 = responser.read(2);
assert_eq!(result3, Some(&[6][..]));
let result4 = responser.read(2);
assert_eq!(result4, None);
}
}

View File

@@ -0,0 +1,236 @@
use crate::prelude::CefResponse;
#[derive(Clone, Default, Debug)]
pub struct HeadersResponser {
pub mime_type: String,
pub status_code: u32,
pub headers: Vec<(String, String)>,
pub response_length: usize,
}
impl HeadersResponser {
pub fn prepare(&mut self, cef_response: &CefResponse, range: &Option<(usize, Option<usize>)>) {
self.mime_type = cef_response.mime_type.clone();
self.status_code = if range.is_some() {
206 // Partial Content
} else {
cef_response.status_code
};
self.headers.clear();
self.response_length = obtain_response_length(&cef_response.data, range);
if let Some(content_range) = content_range_header_value(&cef_response.data, range) {
self.headers
.push(("Content-Range".to_string(), content_range));
self.headers
.push(("Accept-Ranges".to_string(), "bytes".to_string()));
}
}
}
/// Create a `Content-Range` header value based on the provided data and range.
///
/// If the range is `None`, since the request type is not a range request, it returns `None`.
///
/// ## Reference
///
/// - [206 Partial Content](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/206)
fn content_range_header_value(
data: &[u8],
range: &Option<(usize, Option<usize>)>,
) -> Option<String> {
let (start, end) = range.as_ref()?;
Some(format!(
"bytes {}-{}/{}",
start,
end.unwrap_or(data.len() - 1),
data.len()
))
}
fn obtain_response_length(data: &[u8], range: &Option<(usize, Option<usize>)>) -> usize {
match range {
Some((start, end)) => end.unwrap_or(data.len() - 1) - start + 1,
None => data.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::utils::default;
#[test]
fn test_obtain_response_length_no_range() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &None);
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_empty_data_no_range() {
let data = b"";
let result = obtain_response_length(data, &None);
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_range_with_end() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, Some(5))));
assert_eq!(result, 5);
}
#[test]
fn test_obtain_response_length_range_partial() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((7, Some(12))));
assert_eq!(result, 5);
}
#[test]
fn test_obtain_response_length_range_without_end() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((7, None)));
assert_eq!(result, 6);
}
#[test]
fn test_obtain_response_length_range_from_start() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, None)));
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_range_zero_length() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((5, Some(5))));
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_range_end_equals_data_len() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, Some(13))));
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_empty_data_with_range() {
let data = b"";
let result = obtain_response_length(data, &Some((0, None)));
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_large_data() {
let data = vec![0u8; 1024];
let result = obtain_response_length(&data, &None);
assert_eq!(result, 1024);
}
#[test]
fn test_obtain_response_length_large_data_with_range() {
let data = vec![0u8; 1024];
let result = obtain_response_length(&data, &Some((100, Some(200))));
assert_eq!(result, 100);
}
#[test]
fn test_content_range_header_value_no_range() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &None);
assert_eq!(result, None);
}
#[test]
fn test_content_range_header_value_range_with_end() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, Some(5))));
assert_eq!(result, Some("bytes 0-4/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_without_end() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((7, None)));
assert_eq!(result, Some("bytes 7-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_from_start() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, None)));
assert_eq!(result, Some("bytes 0-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_partial() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((7, Some(12))));
assert_eq!(result, Some("bytes 7-11/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_single_byte() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((5, Some(6))));
assert_eq!(result, Some("bytes 5-5/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_last_byte() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((12, Some(13))));
assert_eq!(result, Some("bytes 12-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_single_byte_data() {
let data = b"a";
let result = content_range_header_value(data, &Some((0, None)));
assert_eq!(result, Some("bytes 0-0/1".to_string()));
}
#[test]
fn test_content_range_header_value_large_data() {
let data = vec![0u8; 1024];
let result = content_range_header_value(&data, &Some((100, Some(200))));
assert_eq!(result, Some("bytes 100-199/1024".to_string()));
}
#[test]
fn test_content_range_header_value_large_data_no_end() {
let data = vec![0u8; 1024];
let result = content_range_header_value(&data, &Some((500, None)));
assert_eq!(result, Some("bytes 500-1023/1024".to_string()));
}
#[test]
fn test_content_range_header_value_zero_start() {
let data = b"test";
let result = content_range_header_value(data, &Some((0, Some(2))));
assert_eq!(result, Some("bytes 0-1/4".to_string()));
}
#[test]
fn test_content_range_header_value_range_end_equals_data_len() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, Some(13))));
assert_eq!(result, Some("bytes 0-12/13".to_string()));
}
#[test]
fn status_code_is_206_for_partial_content() {
let data = b"Hello, World!";
let mut headers_responser = HeadersResponser::default();
headers_responser.prepare(
&CefResponse {
data: data.to_vec(),
..default()
},
&Some((0, Some(5))),
);
assert_eq!(headers_responser.status_code, 206);
}
}

View File

@@ -0,0 +1,167 @@
use async_channel::{Receiver, Sender};
use bevy::prelude::{Entity, Event};
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, CefString, ImplRenderHandler, PaintElementType, Range, Rect, RenderHandler,
WrapRenderHandler, sys,
};
use cef_dll_sys::cef_paint_element_type_t;
use std::cell::Cell;
use std::os::raw::c_int;
pub type TextureSender = Sender<RenderTexture>;
pub type TextureReceiver = Receiver<RenderTexture>;
/// The texture structure passed from [`CefRenderHandler::OnPaint`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html#a6547d5c9dd472e6b84706dc81d3f1741).
#[derive(Debug, Clone, PartialEq, Event)]
pub struct RenderTexture {
/// The entity of target rendering webview.
pub webview: Entity,
/// The type of the paint element.
pub ty: RenderPaintElementType,
/// The width of the texture.
pub width: u32,
/// The height of the texture.
pub height: u32,
/// This buffer will be `width` *`height` * 4 bytes in size and represents a BGRA image with an upper-left origin
pub buffer: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderPaintElementType {
/// The main frame of the browser.
View,
/// The popup frame of the browser.
Popup,
}
pub type SharedViewSize = std::rc::Rc<Cell<bevy::prelude::Vec2>>;
pub type SharedImeCaret = std::rc::Rc<Cell<u32>>;
/// ## Reference
///
/// - [`CefRenderHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html)
pub struct RenderHandlerBuilder {
object: *mut RcImpl<sys::cef_render_handler_t, Self>,
webview: Entity,
texture_sender: TextureSender,
size: SharedViewSize,
ime_caret: SharedImeCaret,
}
impl RenderHandlerBuilder {
pub fn build(
webview: Entity,
texture_sender: TextureSender,
size: SharedViewSize,
ime_caret: SharedImeCaret,
) -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
webview,
texture_sender,
size,
ime_caret,
})
}
}
impl Rc for RenderHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderHandler for RenderHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for RenderHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
webview: self.webview,
texture_sender: self.texture_sender.clone(),
size: self.size.clone(),
ime_caret: self.ime_caret.clone(),
}
}
}
impl ImplRenderHandler for RenderHandlerBuilder {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
let size = self.size.get();
rect.width = size.x as _;
rect.height = size.y as _;
}
}
fn on_text_selection_changed(
&self,
_browser: Option<&mut Browser>,
_: Option<&CefString>,
selected_range: Option<&Range>,
) {
if let Some(selected_range) = selected_range {
self.ime_caret.set(selected_range.to);
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn on_paint(
&self,
_browser: Option<&mut Browser>,
type_: PaintElementType,
_dirty_rects_count: usize,
_dirty_rects: Option<&Rect>,
buffer: *const u8,
width: c_int,
height: c_int,
) {
let ty = match type_.as_ref() {
cef_paint_element_type_t::PET_POPUP => RenderPaintElementType::Popup,
_ => RenderPaintElementType::View,
};
let texture = RenderTexture {
webview: self.webview,
ty,
width: width as u32,
height: height as u32,
buffer: unsafe {
std::slice::from_raw_parts(buffer, (width * height * 4) as usize).to_vec()
},
};
let _ = self.texture_sender.send_blocking(texture);
}
/// MEMO: This method only supports on Windows
///
/// In Windows, this method is more performant than `on_paint`?
#[cfg(target_os = "windows")]
fn on_accelerated_paint(
&self,
_browser: Option<&mut Browser>,
_type_: PaintElementType,
_dirty_rects_count: usize,
_dirty_rects: Option<&Rect>,
_: Option<&AcceleratedPaintInfo>,
) {
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,50 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplRequestContextHandler, WrapRequestContextHandler, sys};
/// ## Reference
///
/// - [`CefRequestContextHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRequestContextHandler.html)
pub struct RequestContextHandlerBuilder {
object: *mut RcImpl<sys::cef_request_context_handler_t, Self>,
}
impl RequestContextHandlerBuilder {
pub fn build() -> cef::RequestContextHandler {
cef::RequestContextHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapRequestContextHandler for RequestContextHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_request_context_handler_t, Self>) {
self.object = object;
}
}
impl Rc for RequestContextHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for RequestContextHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRequestContextHandler for RequestContextHandlerBuilder {
#[inline]
fn get_raw(&self) -> *mut sys::_cef_request_context_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,52 @@
use cef::{load_library, unload_library};
use std::env::home_dir;
/// This loader is a modified version of [LibraryLoader](cef::library_loader::LibraryLoader) that can load the framework located in the home directory.
pub struct DebugLibraryLoader {
path: std::path::PathBuf,
}
impl Default for DebugLibraryLoader {
fn default() -> Self {
Self::new()
}
}
impl DebugLibraryLoader {
const FRAMEWORK_PATH: &'static str =
"Chromium Embedded Framework.framework/Chromium Embedded Framework";
pub fn new() -> Self {
let path = home_dir()
.unwrap()
.join(".local")
.join("share")
.join("cef")
.join(Self::FRAMEWORK_PATH)
.canonicalize()
.unwrap();
Self { path }
}
// See [cef_load_library] for more documentation.
pub fn load(&self) -> bool {
Self::load_library(&self.path)
}
fn load_library(name: &std::path::Path) -> bool {
use std::os::unix::ffi::OsStrExt;
let Ok(name) = std::ffi::CString::new(name.as_os_str().as_bytes()) else {
return false;
};
unsafe { load_library(Some(&*name.as_ptr().cast())) == 1 }
}
}
impl Drop for DebugLibraryLoader {
fn drop(&mut self) {
if unload_library() != 1 {
eprintln!("cannot unload framework {}", self.path.display());
}
}
}

View File

@@ -0,0 +1,15 @@
mod browser_process;
mod debug;
mod render_process;
mod util;
pub mod prelude {
pub use crate::browser_process::*;
pub use crate::debug::*;
pub use crate::render_process::app::*;
pub use crate::render_process::brp::*;
pub use crate::render_process::emit::*;
pub use crate::render_process::execute_render_process;
pub use crate::render_process::render_process_handler::*;
pub use crate::util::*;
}

View File

@@ -0,0 +1,28 @@
use crate::prelude::RenderProcessAppBuilder;
use cef::args::Args;
use cef::{api_hash, execute_process, sys};
pub mod app;
pub mod brp;
pub mod emit;
pub mod listen;
pub mod render_process_handler;
/// Execute the CEF render process.
pub fn execute_render_process() {
let args = Args::new();
#[cfg(target_os = "macos")]
let _loader = {
let loader =
cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true);
assert!(loader.load());
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let mut app = RenderProcessAppBuilder::build();
execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
}

View File

@@ -0,0 +1,63 @@
use crate::prelude::RenderProcessHandlerBuilder;
use crate::util::{SCHEME_CEF, cef_scheme_flags};
use cef::rc::{Rc, RcImpl};
use cef::{ImplApp, ImplSchemeRegistrar, RenderProcessHandler, SchemeRegistrar, WrapApp};
use cef_dll_sys::{_cef_app_t, cef_base_ref_counted_t};
#[derive(Default)]
pub struct RenderProcessAppBuilder {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl RenderProcessAppBuilder {
pub fn build() -> cef::App {
cef::App::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Clone for RenderProcessAppBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
self.object
};
Self { object }
}
}
impl Rc for RenderProcessAppBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplApp for RenderProcessAppBuilder {
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(RenderProcessHandler::new(
RenderProcessHandlerBuilder::build(),
))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
registrar.add_custom_scheme(Some(&SCHEME_CEF.into()), cef_scheme_flags() as _);
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_app_t {
self.object as *mut _cef_app_t
}
}
impl WrapApp for RenderProcessAppBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,181 @@
use crate::prelude::{BRP_PROMISES, PROCESS_MESSAGE_BRP, v8_value_to_json};
use cef::rc::{ConvertParam, ConvertReturnValue, Rc, RcImpl};
use cef::{
CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ImplV8Value,
ProcessId, V8Handler, V8Value, WrapV8Handler, process_message_create, sys,
v8_value_create_promise, v8_value_create_string,
};
use cef_dll_sys::{_cef_v8_handler_t, _cef_v8_value_t, cef_process_id_t, cef_string_t};
use std::os::raw::c_int;
use std::ptr::write;
/// Implements the `window.brp` function in JavaScript.
///
/// The function definition is `async <T>(request: BrpRequest) -> Promise<T>`.
///
/// The flow from the execution of the function to the return of the result to the Javascript side is as follows:
/// 1. Send `BrpRequest` to the browser process.
/// 2. The browser process receives `BrpResult` and sends it to the render process.
/// 3. The render process receives the result in `on_process_message_received`.
/// 4. The render process resolves the result to the `Promise` of `window.brp`.
pub struct BrpBuilder {
object: *mut RcImpl<_cef_v8_handler_t, Self>,
frame: Frame,
}
impl BrpBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for BrpBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for BrpBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for BrpBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for BrpBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
ret: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_BRP.into()))
&& let Some(promise) = v8_value_create_promise()
{
if let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(brp_request) = v8_value_to_json(arg)
&& let Ok(brp_request) = serde_json::to_string(&brp_request)
&& let Some(ret) = ret
{
let id = uuid::Uuid::new_v4().to_string();
arguments_list.set_string(0, Some(&id.as_str().into()));
arguments_list.set_string(1, Some(&brp_request.as_str().into()));
self.frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
ret.replace(promise.clone());
let mut promises = BRP_PROMISES.lock().unwrap();
promises.insert(id, promise);
} else {
let mut exception =
v8_value_create_string(Some(&"Failed to execute BRP request".into()));
promise.resolve_promise(exception.as_mut());
}
}
1
}
fn init_methods(object: &mut _cef_v8_handler_t) {
init_methods::<Self>(object);
}
fn get_raw(&self) -> *mut _cef_v8_handler_t {
self.object.cast()
}
}
fn init_methods<I: ImplV8Handler>(object: &mut _cef_v8_handler_t) {
object.execute = Some(execute::<I>);
}
extern "C" fn execute<I: ImplV8Handler>(
self_: *mut _cef_v8_handler_t,
name: *const cef_string_t,
object: *mut _cef_v8_value_t,
arguments_count: usize,
arguments: *const *mut _cef_v8_value_t,
retval: *mut *mut _cef_v8_value_t,
exception: *mut cef_string_t,
) -> c_int {
let (arg_self_, arg_name, arg_object, arg_arguments_count, arg_arguments, _, arg_exception) = (
self_,
name,
object,
arguments_count,
arguments,
retval,
exception,
);
let arg_self_: &RcImpl<_, I> = RcImpl::get(arg_self_);
let arg_name = if arg_name.is_null() {
None
} else {
Some(arg_name.into())
};
let arg_name = arg_name.as_ref();
let mut arg_object =
unsafe { arg_object.as_mut() }.map(|arg| (arg as *mut _cef_v8_value_t).wrap_result());
let arg_object = arg_object.as_mut();
let vec_arguments = unsafe { arg_arguments.as_ref() }.map(|arg| {
let arg =
unsafe { std::slice::from_raw_parts(std::ptr::from_ref(arg), arg_arguments_count) };
arg.iter()
.map(|arg| {
if arg.is_null() {
None
} else {
Some((*arg).wrap_result())
}
})
.collect::<Vec<_>>()
});
let arg_arguments = vec_arguments.as_deref();
let mut arg_retval: Option<V8Value> = None;
let arg = Some(&mut arg_retval);
let mut arg_exception = if arg_exception.is_null() {
None
} else {
Some(arg_exception.into())
};
let arg_exception = arg_exception.as_mut();
let r = ImplV8Handler::execute(
&arg_self_.interface,
arg_name,
arg_object,
arg_arguments,
arg,
arg_exception,
);
if let Some(ret) = arg_retval {
// When the result is received, the pointer should be updated here
// and the exception should also be updated.
unsafe {
write(retval, ret.into_raw());
}
}
r
}

View File

@@ -0,0 +1,82 @@
use crate::prelude::PROCESS_MESSAGE_JS_EMIT;
use crate::util::v8_value_to_json;
use cef::rc::{Rc, RcImpl};
use cef::{
CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ProcessId,
V8Handler, V8Value, WrapV8Handler, process_message_create, sys,
};
use cef_dll_sys::cef_process_id_t;
use std::os::raw::c_int;
pub struct EmitBuilder {
object: *mut RcImpl<sys::_cef_v8_handler_t, Self>,
frame: Frame,
}
impl EmitBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for EmitBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for EmitBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for EmitBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for EmitBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_JS_EMIT.into()))
&& let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(arg) = v8_value_to_json(arg)
&& let Ok(arg) = serde_json::to_string(&arg)
{
arguments_list.set_string(0, Some(&arg.as_str().into()));
self.frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
}
1
}
fn get_raw(&self) -> *mut sys::_cef_v8_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,77 @@
use crate::prelude::LISTEN_EVENTS;
use crate::util::IntoString;
use cef::rc::{Rc, RcImpl};
use cef::{CefString, Frame, ImplV8Handler, ImplV8Value, V8Handler, V8Value, WrapV8Handler, sys};
use std::os::raw::c_int;
pub struct ListenBuilder {
object: *mut RcImpl<sys::_cef_v8_handler_t, Self>,
frame: Frame,
}
impl ListenBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for ListenBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for ListenBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for ListenBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for ListenBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(arguments) = arguments
&& let Some(Some(id)) = arguments.first()
&& 0 < id.is_string()
&& let Some(Some(callback)) = arguments.get(1)
&& 0 < callback.is_function()
{
LISTEN_EVENTS
.lock()
.unwrap()
.insert(id.string_value().into_string(), callback.clone());
}
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_v8_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,191 @@
use crate::prelude::{EmitBuilder, IntoString};
use crate::render_process::brp::BrpBuilder;
use crate::render_process::listen::ListenBuilder;
use crate::util::json_to_v8;
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
use bevy::platform::collections::HashMap;
use bevy_remote::BrpResult;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplRenderProcessHandler,
ImplV8Context, ImplV8Value, ProcessId, ProcessMessage, V8Context, V8Propertyattribute, V8Value,
WrapRenderProcessHandler, sys, v8_value_create_function, v8_value_create_object,
};
use std::os::raw::c_int;
use std::sync::Mutex;
pub(crate) static BRP_PROMISES: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
pub(crate) static LISTEN_EVENTS: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
pub const PROCESS_MESSAGE_BRP: &str = "brp";
pub const PROCESS_MESSAGE_HOST_EMIT: &str = "host-emit";
pub const PROCESS_MESSAGE_JS_EMIT: &str = "js-emit";
pub struct RenderProcessHandlerBuilder {
object: *mut RcImpl<sys::_cef_render_process_handler_t, Self>,
}
impl RenderProcessHandlerBuilder {
pub fn build() -> RenderProcessHandlerBuilder {
RenderProcessHandlerBuilder {
object: core::ptr::null_mut(),
}
}
}
impl WrapRenderProcessHandler for RenderProcessHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_process_handler_t, Self>) {
self.object = object;
}
}
impl Rc for RenderProcessHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for RenderProcessHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
fn on_context_created(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
if let Some(g) = context.and_then(|c| c.global())
&& let Some(frame) = frame
&& let Some(mut cef) = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
)
&& let Some(mut brp) = v8_value_create_function(
Some(&"brp".into()),
Some(&mut BrpBuilder::build(frame.clone())),
)
&& let Some(mut emit) = v8_value_create_function(
Some(&"emit".into()),
Some(&mut EmitBuilder::build(frame.clone())),
)
&& let Some(mut listen) = v8_value_create_function(
Some(&"listen".into()),
Some(&mut ListenBuilder::build(frame.clone())),
)
{
cef.set_value_bykey(
Some(&"brp".into()),
Some(&mut brp),
V8Propertyattribute::default(),
);
cef.set_value_bykey(
Some(&"emit".into()),
Some(&mut emit),
V8Propertyattribute::default(),
);
cef.set_value_bykey(
Some(&"listen".into()),
Some(&mut listen),
V8Propertyattribute::default(),
);
g.set_value_bykey(
Some(&"cef".into()),
Some(&mut cef),
V8Propertyattribute::default(),
);
};
}
fn on_process_message_received(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_: ProcessId,
message: Option<&mut ProcessMessage>,
) -> c_int {
if let Some(message) = message
&& let Some(frame) = frame
&& let Some(ctx) = frame.v8_context()
{
match message.name().into_string().as_str() {
PROCESS_MESSAGE_BRP => {
handle_brp_message(message, ctx);
}
PROCESS_MESSAGE_HOST_EMIT => {
handle_listen_message(message, ctx);
}
_ => {}
}
};
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_process_handler_t {
self.object.cast()
}
}
fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) {
let Some(argument_list) = message.argument_list() else {
return;
};
let id = argument_list.string(0).into_string();
let payload = argument_list.string(1).into_string();
let Ok(Some(promise)) = BRP_PROMISES.lock().map(|mut p| p.remove(&id)) else {
return;
};
if let Ok(brp_result) = serde_json::from_str::<BrpResult>(&payload) {
ctx.enter();
match brp_result {
Ok(v) => {
promise.resolve_promise(json_to_v8(v).as_mut());
}
Err(e) => {
promise.reject_promise(Some(&e.message.as_str().into()));
}
}
ctx.exit();
}
}
fn handle_listen_message(message: &ProcessMessage, mut ctx: V8Context) {
let Some(argument_list) = message.argument_list() else {
return;
};
let id = argument_list.string(0).into_string();
let payload = argument_list.string(1).into_string();
ctx.enter();
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&payload)
&& let Ok(events) = LISTEN_EVENTS.lock()
{
let mut obj = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
);
let Some(callback) = events.get(&id) else {
return;
};
callback.execute_function_with_context(
Some(&mut ctx),
obj.as_mut(),
Some(&[json_to_v8(value)]),
);
}
ctx.exit();
}

View File

@@ -0,0 +1,140 @@
pub mod v8_accessor;
mod v8_handler_wrapper;
pub mod v8_interceptor;
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
use cef::rc::ConvertParam;
use cef::{
CefStringList, CefStringUserfreeUtf16, CefStringUtf16, ImplV8Value, V8Propertyattribute,
v8_value_create_array, v8_value_create_bool, v8_value_create_double, v8_value_create_int,
v8_value_create_null, v8_value_create_object, v8_value_create_string,
};
use cef_dll_sys::_cef_string_utf16_t;
use cef_dll_sys::cef_scheme_options_t::{
CEF_SCHEME_OPTION_CORS_ENABLED, CEF_SCHEME_OPTION_LOCAL, CEF_SCHEME_OPTION_SECURE,
CEF_SCHEME_OPTION_STANDARD,
};
use std::env::home_dir;
use std::path::PathBuf;
pub const SCHEME_CEF: &str = "cef";
pub const HOST_CEF: &str = "localhost";
pub fn cef_scheme_flags() -> u32 {
CEF_SCHEME_OPTION_STANDARD as u32
| CEF_SCHEME_OPTION_SECURE as u32
| CEF_SCHEME_OPTION_LOCAL as u32
| CEF_SCHEME_OPTION_CORS_ENABLED as u32
}
pub fn debug_chromium_libraries_path() -> PathBuf {
debug_chromium_embedded_framework_dir_path().join("Libraries")
}
pub fn debug_chromium_embedded_framework_dir_path() -> PathBuf {
debug_cef_path().join("Chromium Embedded Framework.framework")
}
pub fn debug_cef_path() -> PathBuf {
home_dir().unwrap().join(".local").join("share").join("cef")
}
pub fn debug_render_process_path() -> PathBuf {
cargo_bin_path().join("bevy_cef_debug_render_process")
}
pub fn cargo_bin_path() -> PathBuf {
home_dir().unwrap().join(".cargo").join("bin")
}
pub trait IntoString {
fn into_string(self) -> String;
}
impl IntoString for CefStringUserfreeUtf16 {
fn into_string(self) -> String {
let ptr: *mut _cef_string_utf16_t = self.into_raw();
CefStringUtf16::from(ptr).to_string()
}
}
pub fn v8_value_to_json(v8: &cef::V8Value) -> Option<serde_json::Value> {
if v8.is_bool().is_positive() {
Some(serde_json::Value::Bool(v8.bool_value().is_positive()))
} else if v8.is_int().is_positive() {
Some(serde_json::Value::Number(serde_json::Number::from(
v8.int_value(),
)))
} else if v8.is_double().is_positive() {
Some(serde_json::Value::Number(
serde_json::Number::from_f64(v8.double_value()).unwrap(),
))
} else if v8.is_string().is_positive() {
Some(serde_json::Value::String(v8.string_value().into_string()))
} else if v8.is_null().is_positive() || v8.is_undefined().is_positive() {
Some(serde_json::Value::Null)
} else if v8.is_array().is_positive() {
let mut array = Vec::new();
let mut keys = CefStringList::new();
v8.keys(Some(&mut keys));
for key in keys.into_iter() {
if let Some(v) = v8.value_bykey(Some(&key.as_str().into()))
&& let Some(serialized) = v8_value_to_json(&v)
{
{
array.push(serialized);
}
}
}
Some(serde_json::Value::Array(array))
} else if v8.is_object().is_positive() {
let mut object = serde_json::Map::new();
let mut keys = CefStringList::new();
v8.keys(Some(&mut keys));
for key in keys.into_iter() {
if let Some(v) = v8.value_bykey(Some(&key.as_str().into()))
&& let Some(serialized) = v8_value_to_json(&v)
{
{
object.insert(key, serialized);
}
}
}
Some(serde_json::Value::Object(object))
} else {
None
}
}
pub fn json_to_v8(v: serde_json::Value) -> Option<cef::V8Value> {
match v {
serde_json::Value::Null => v8_value_create_null(),
serde_json::Value::Bool(b) => v8_value_create_bool(b as _),
serde_json::Value::Number(n) if n.is_i64() => v8_value_create_int(n.as_i64()? as i32),
serde_json::Value::Number(n) => v8_value_create_double(n.as_f64()?),
serde_json::Value::String(s) => v8_value_create_string(Some(&s.as_str().into())),
serde_json::Value::Array(arr) => {
let v8_array = v8_value_create_array(arr.len() as _)?;
for (i, item) in arr.into_iter().enumerate() {
v8_array.set_value_byindex(i as _, json_to_v8(item).as_mut());
}
Some(v8_array)
}
serde_json::Value::Object(obj) => {
let v8_object = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
)?;
for (key, value) in obj {
v8_object.set_value_bykey(
Some(&key.as_str().into()),
json_to_v8(value).as_mut(),
V8Propertyattribute::default(),
);
}
Some(v8_object)
}
}
}

View File

@@ -0,0 +1,48 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplV8Accessor, V8Accessor, WrapV8Accessor, sys};
use cef_dll_sys::_cef_v8_accessor_t;
pub struct V8DefaultAccessorBuilder {
object: *mut RcImpl<_cef_v8_accessor_t, Self>,
}
impl V8DefaultAccessorBuilder {
pub fn build() -> V8Accessor {
V8Accessor::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Rc for V8DefaultAccessorBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for V8DefaultAccessorBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl WrapV8Accessor for V8DefaultAccessorBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_accessor_t, Self>) {
self.object = object;
}
}
impl ImplV8Accessor for V8DefaultAccessorBuilder {
#[inline]
fn get_raw(&self) -> *mut _cef_v8_accessor_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,70 @@
// use std::os::raw::c_int;
// use bevy::ecs::reflect::ReflectCommandExt;
// use cef::rc::{Rc, RcImpl};
// use cef::{sys, CefString, ImplV8Handler, V8Handler, V8Value, WrapV8Handler};
// use cef_dll_sys::{_cef_v8_handler_t, _cef_v8_value_t, cef_string_t};
//
// pub struct V8HandlerWrapBuilder<T> {
// base: T,
// }
//
// impl<T> V8HandlerWrapBuilder<T> {
// pub fn build(base: T) -> V8Handler {
// V8Handler::new(Self {
// base,
// })
// }
// }
//
// impl<T: Rc> Rc for V8HandlerWrapBuilder<T> {
// fn as_base(&self) -> &sys::cef_base_ref_counted_t {
// self.base.as_base()
// }
// }
//
// impl<T: ImplV8Handler> ImplV8Handler for V8HandlerWrapBuilder<T> {
// fn execute(&self, name: Option<&CefString>, object: Option<&mut V8Value>, arguments: Option<&[Option<V8Value>]>, retval: Option<&mut Option<V8Value>>, exception: Option<&mut CefString>) -> c_int {
// self.base.execute(
// name,
// object,
// arguments,
// retval,
// exception,
// )
// }
//
// fn get_raw(&self) -> *mut _cef_v8_handler_t {
// self.base.get_raw()
// }
//
// fn init_methods(object: &mut _cef_v8_handler_t) {
// T::init_methods(object);
//
// }
// }
//
// impl<T: Clone> Clone for V8HandlerWrapBuilder<T> {
// fn clone(&self) -> Self {
// Self{
// base: self.base.clone(),
// }
// }
// }
//
// impl <T: WrapV8Handler> cef::WrapV8Handler for V8HandlerWrapBuilder<T> {
// fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
// self.base.wrap_rc(object);
// }
// }
//
// // extern "C" fn execute<I: ImplV8Handler>(
// // self_: *mut _cef_v8_handler_t,
// // name: *const cef_string_t,
// // object: *mut _cef_v8_value_t,
// // arguments_count: usize,
// // arguments: *const *mut _cef_v8_value_t,
// // retval: *mut *mut _cef_v8_value_t,
// // exception: *mut cef_string_t,
// // ) -> ::std::os::raw::c_int {
// //
// // }

View File

@@ -0,0 +1,48 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplV8Interceptor, V8Interceptor, WrapV8Interceptor, sys};
use cef_dll_sys::_cef_v8_interceptor_t;
pub struct V8DefaultInterceptorBuilder {
object: *mut RcImpl<_cef_v8_interceptor_t, Self>,
}
impl V8DefaultInterceptorBuilder {
pub fn build() -> V8Interceptor {
V8Interceptor::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapV8Interceptor for V8DefaultInterceptorBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_interceptor_t, Self>) {
self.object = object;
}
}
impl Rc for V8DefaultInterceptorBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for V8DefaultInterceptorBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplV8Interceptor for V8DefaultInterceptorBuilder {
#[inline]
fn get_raw(&self) -> *mut _cef_v8_interceptor_t {
self.object.cast()
}
}