Implement basic cef based desktop app

This commit is contained in:
Timon Schelling 2025-07-19 19:34:00 +00:00
parent 03751dfd7c
commit 809a00979a
21 changed files with 2647 additions and 2636 deletions

View file

@ -42,9 +42,39 @@
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
};
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
version = "138.0.26";
gitRevision = "84f2d27";
chromiumVersion = "138.0.7204.158";
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ=";
__intentionallyOverridingVersion = true;
postInstall = ''
strip $out/lib/*
'';
});
libcefPath = pkgs.runCommand "libcef-path" {} ''
mkdir -p $out
ln -s ${libcef}/include $out/include
find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \;
find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \;
cp -r ${libcef}/share/cef/* $out/
echo '${builtins.toJSON {
type = "minimal";
name = builtins.baseNameOf libcef.src.url;
sha1 = "";
}}' > $out/archive.json
'';
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
buildInputs = with pkgs; [
# System libraries
wayland
wayland.dev
openssl
vulkan-loader
mesa
@ -85,8 +115,8 @@
devShells.default = pkgs.mkShell {
packages = buildInputs ++ buildTools ++ devTools;
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules/";
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}:/${libcefPath}";
CEF_PATH = libcefPath;
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
shellHook = ''

3152
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
[workspace]
members = [
"editor",
"desktop",
"proc-macros",
"frontend/wasm",
"node-graph/gapplication-io",
@ -110,7 +111,7 @@ web-sys = { version = "=0.3.77", features = [
"HtmlImageElement",
"ImageBitmapRenderingContext",
] }
winit = "0.29"
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
url = "2.5"
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
vello = { git = "https://github.com/linebender/vello.git", rev = "3275ec8" } # TODO switch back to stable when a release is made

3
desktop/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

32
desktop/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "graphite-desktop"
version = "0.1.0"
description = "Graphite Desktop"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
edition = "2021"
rust-version = "1.79"
[features]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
[dependencies]
# Local dependencies
graphite-editor = { path = "../editor", features = [
"gpu",
"ron",
"vello",
"decouple-execution",
] }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
base64.workspace = true
thiserror.workspace = true
pollster = "0.3"
cef = "138.5.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
bytemuck = { version = "1.23.1", features = ["derive"] }
include_dir = "0.7.4"

10
desktop/build.rs Normal file
View file

@ -0,0 +1,10 @@
use std::fs::metadata;
fn main() {
let frontend_dir = format!("{}/../frontend/dist", env!("CARGO_MANIFEST_DIR"));
metadata(&frontend_dir).expect("Failed to find frontend directory. Please build the frontend first.");
metadata(format!("{}/index.html", &frontend_dir)).expect("Failed to find index.html in frontend directory.");
println!("cargo:rerun-if-changed=.");
println!("cargo:rerun-if-changed=../frontend");
}

124
desktop/src/cef/context.rs Normal file
View file

@ -0,0 +1,124 @@
use cef::sys::CEF_API_VERSION_LAST;
use cef::{api_hash, args::Args, execute_process, Browser, CefString, Settings};
use cef::{browser_host_create_browser_sync, initialize, BrowserSettings, DictionaryValue, ImplCommandLine, RequestContext, WindowInfo};
use thiserror::Error;
use winit::event::WindowEvent;
use super::input::{handle_window_event, InputState};
use super::EventHandler;
use super::internal::{AppImpl, ClientImpl, NonBrowserAppImpl, RenderHandlerImpl};
pub(crate) struct Setup {}
pub(crate) struct Initialized {}
pub(crate) trait ContextState {}
impl ContextState for Setup {}
impl ContextState for Initialized {}
pub(crate) struct Context<S: ContextState> {
args: Args,
pub(crate) browser: Option<Browser>,
pub(crate) input_state: InputState,
marker: std::marker::PhantomData<S>,
}
impl Context<Setup> {
pub(crate) fn new() -> Result<Context<Setup>, SetupError> {
#[cfg(target_os = "macos")]
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
assert!(loader.load());
loader
};
let _ = api_hash(CEF_API_VERSION_LAST, 0);
let args = Args::new();
let cmd = args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
if !is_browser_process {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = NonBrowserAppImpl::new();
let ret = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
return Err(SetupError::SubprocessFailed(process_type.to_string()));
} else {
return Err(SetupError::Subprocess);
}
}
Ok(Context {
args,
browser: None,
input_state: InputState::default(),
marker: std::marker::PhantomData::<Setup>,
})
}
pub(crate) fn init(self, event_handler: impl EventHandler) -> Result<Context<Initialized>, InitError> {
let mut settings = Settings::default();
settings.windowless_rendering_enabled = 1;
settings.multi_threaded_message_loop = 0;
let mut cef_app = AppImpl::new(event_handler.clone());
let res = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
if res != 1 {
return Err(InitError::InitializationFailed);
}
let render_handler = RenderHandlerImpl::new(event_handler.clone());
let mut client = ClientImpl::new(render_handler);
let url = CefString::from("graphite://frontend/");
let mut window_info = WindowInfo::default();
window_info.windowless_rendering_enabled = 1;
let mut settings = BrowserSettings::default();
settings.windowless_frame_rate = 60;
settings.background_color = 0x0;
let browser = browser_host_create_browser_sync(
Some(&window_info),
Some(&mut client),
Some(&url),
Some(&settings),
Option::<&mut DictionaryValue>::None,
Option::<&mut RequestContext>::None,
);
Ok(Context {
args: self.args,
browser,
input_state: self.input_state,
marker: std::marker::PhantomData::<Initialized>,
})
}
}
impl Context<Initialized> {
pub(crate) fn work(&mut self) {
cef::do_message_loop_work();
}
pub(crate) fn handle_window_event(&mut self, event: &WindowEvent) {
handle_window_event(self, event);
}
pub(crate) fn shutdown(self) {
cef::shutdown();
}
}
#[derive(Error, Debug)]
pub(crate) enum SetupError {
#[error("this is the sub process should exit immediately")]
Subprocess,
#[error("subprocess returned non zero exit code")]
SubprocessFailed(String),
}
#[derive(Error, Debug)]
pub(crate) enum InitError {
#[error("initialization failed")]
InitializationFailed,
}

View file

@ -0,0 +1,384 @@
macro_rules! map_enum {
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
match $target {
$(
$enum::$variant => $code,
)+
_ => 0,
}
};
}
macro_rules! map {
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
match $target {
$(
$variant => $code,
)+
_ => 0,
}
};
}
pub(crate) trait ToVKBits {
fn to_vk_bits(&self) -> i32;
}
impl ToVKBits for winit::keyboard::NamedKey {
fn to_vk_bits(&self) -> i32 {
use winit::keyboard::NamedKey;
map_enum!(
self,
NamedKey,
(0x12, Alt),
(0xA5, AltGraph),
(0x14, CapsLock),
(0x11, Control),
(0x90, NumLock),
(0x91, ScrollLock),
(0x10, Shift),
(0x5B, Meta),
(0x5C, Super),
(0x0D, Enter),
(0x09, Tab),
(0x20, Space),
(0x28, ArrowDown),
(0x25, ArrowLeft),
(0x27, ArrowRight),
(0x26, ArrowUp),
(0x23, End),
(0x24, Home),
(0x22, PageDown),
(0x21, PageUp),
(0x08, Backspace),
(0x0C, Clear),
(0xF7, CrSel),
(0x2E, Delete),
(0xF9, EraseEof),
(0xF8, ExSel),
(0x2D, Insert),
(0x1E, Accept),
(0xF6, Attn),
(0x03, Cancel),
(0x5D, ContextMenu),
(0x1B, Escape),
(0x2B, Execute),
(0x2F, Help),
(0x13, Pause),
(0xFA, Play),
(0x5D, Props),
(0x29, Select),
(0xFB, ZoomIn),
(0xFB, ZoomOut),
(0x2C, PrintScreen),
(0x5F, Standby),
(0x1C, Convert),
(0x18, FinalMode),
(0x1F, ModeChange),
(0x1D, NonConvert),
(0xE5, Process),
(0x15, HangulMode),
(0x19, HanjaMode),
(0x17, JunjaMode),
(0x15, KanaMode),
(0x19, KanjiMode),
(0xB0, MediaFastForward),
(0xB3, MediaPause),
(0xB3, MediaPlay),
(0xB3, MediaPlayPause),
(0xB1, MediaRewind),
(0xB2, MediaStop),
(0xB0, MediaTrackNext),
(0xB1, MediaTrackPrevious),
(0x2A, Print),
(0xAE, AudioVolumeDown),
(0xAF, AudioVolumeUp),
(0xAD, AudioVolumeMute),
(0xB6, LaunchApplication1),
(0xB7, LaunchApplication2),
(0xB4, LaunchMail),
(0xB5, LaunchMediaPlayer),
(0xB5, LaunchMusicPlayer),
(0xA6, BrowserBack),
(0xAB, BrowserFavorites),
(0xA7, BrowserForward),
(0xAC, BrowserHome),
(0xA8, BrowserRefresh),
(0xAA, BrowserSearch),
(0xA9, BrowserStop),
(0xFB, ZoomToggle),
(0x70, F1),
(0x71, F2),
(0x72, F3),
(0x73, F4),
(0x74, F5),
(0x75, F6),
(0x76, F7),
(0x77, F8),
(0x78, F9),
(0x79, F10),
(0x7A, F11),
(0x7B, F12),
(0x7C, F13),
(0x7D, F14),
(0x7E, F15),
(0x7F, F16),
(0x80, F17),
(0x81, F18),
(0x82, F19),
(0x83, F20),
(0x84, F21),
(0x85, F22),
(0x86, F23),
(0x87, F24),
)
}
}
impl ToVKBits for char {
fn to_vk_bits(&self) -> i32 {
map!(
self,
(0x0041, 'a'),
(0x0042, 'b'),
(0x0043, 'c'),
(0x0044, 'd'),
(0x0045, 'e'),
(0x0046, 'f'),
(0x0047, 'g'),
(0x0048, 'h'),
(0x0049, 'i'),
(0x004a, 'j'),
(0x004b, 'k'),
(0x004c, 'l'),
(0x004d, 'm'),
(0x004e, 'n'),
(0x004f, 'o'),
(0x0050, 'p'),
(0x0051, 'q'),
(0x0052, 'r'),
(0x0053, 's'),
(0x0054, 't'),
(0x0055, 'u'),
(0x0056, 'v'),
(0x0057, 'w'),
(0x0058, 'x'),
(0x0059, 'y'),
(0x005a, 'z'),
(0x0041, 'A'),
(0x0042, 'B'),
(0x0043, 'C'),
(0x0044, 'D'),
(0x0045, 'E'),
(0x0046, 'F'),
(0x0047, 'G'),
(0x0048, 'H'),
(0x0049, 'I'),
(0x004a, 'J'),
(0x004b, 'K'),
(0x004c, 'L'),
(0x004d, 'M'),
(0x004e, 'N'),
(0x004f, 'O'),
(0x0050, 'P'),
(0x0051, 'Q'),
(0x0052, 'R'),
(0x0053, 'S'),
(0x0054, 'T'),
(0x0055, 'U'),
(0x0056, 'V'),
(0x0057, 'W'),
(0x0058, 'X'),
(0x0059, 'Y'),
(0x005a, 'Z'),
(0x0031, '1'),
(0x0032, '2'),
(0x0032, '3'),
(0x0033, '4'),
(0x0034, '5'),
(0x0035, '6'),
(0x0036, '7'),
(0x0037, '8'),
(0x0039, '9'),
(0x0030, '0'),
)
}
}
pub(crate) trait ToDomBits {
fn to_dom_bits(&self) -> i32;
}
impl ToDomBits for winit::keyboard::NamedKey {
fn to_dom_bits(&self) -> i32 {
use winit::keyboard::NamedKey;
map_enum!(
self,
NamedKey,
(0x0000, Hyper),
(0x0085, Super),
(0x0025, Control),
(0x0032, Shift),
(0x0040, Alt),
(0x0000, Fn),
(0x0000, FnLock),
(0x0024, Enter),
(0x0009, Escape),
(0x0016, Backspace),
(0x0017, Tab),
(0x0041, Space),
(0x0042, CapsLock),
(0x0043, F1),
(0x0044, F2),
(0x0045, F3),
(0x0046, F4),
(0x0047, F5),
(0x0048, F6),
(0x0049, F7),
(0x004a, F8),
(0x004b, F9),
(0x004c, F10),
(0x005f, F11),
(0x0060, F12),
(0x006b, PrintScreen),
(0x004e, ScrollLock),
(0x007f, Pause),
(0x0076, Insert),
(0x006e, Home),
(0x0070, PageUp),
(0x0077, Delete),
(0x0073, End),
(0x0075, PageDown),
(0x0072, ArrowRight),
(0x0071, ArrowLeft),
(0x0074, ArrowDown),
(0x006f, ArrowUp),
(0x004d, NumLock),
(0x0087, ContextMenu),
(0x007c, Power),
(0x00bf, F13),
(0x00c0, F14),
(0x00c1, F15),
(0x00c2, F16),
(0x00c3, F17),
(0x00c4, F18),
(0x00c5, F19),
(0x00c6, F20),
(0x00c7, F21),
(0x00c8, F22),
(0x00c9, F23),
(0x00ca, F24),
(0x008e, Open),
(0x0092, Help),
(0x008c, Select),
(0x0089, Again),
(0x008b, Undo),
(0x0091, Cut),
(0x008d, Copy),
(0x008f, Paste),
(0x0090, Find),
(0x0079, AudioVolumeMute),
(0x007b, AudioVolumeUp),
(0x007a, AudioVolumeDown),
(0x0065, KanaMode),
(0x0064, Convert),
(0x0066, NonConvert),
(0x0000, Props),
(0x00e9, BrightnessUp),
(0x00e8, BrightnessDown),
(0x00d7, MediaPlay),
(0x00d1, MediaPause),
(0x00af, MediaRecord),
(0x00d8, MediaFastForward),
(0x00b0, MediaRewind),
(0x00ab, MediaTrackNext),
(0x00ad, MediaTrackPrevious),
(0x00ae, MediaStop),
(0x00a9, Eject),
(0x00ac, MediaPlayPause),
(0x00a3, LaunchMail),
(0x024d, LaunchScreenSaver),
(0x00e1, BrowserSearch),
(0x00b4, BrowserHome),
(0x00a6, BrowserBack),
(0x00a7, BrowserForward),
(0x0088, BrowserStop),
(0x00b5, BrowserRefresh),
(0x00a4, BrowserFavorites),
(0x017c, ZoomToggle),
(0x00f0, MailReply),
(0x00f1, MailForward),
(0x00ef, MailSend),
)
}
}
impl ToDomBits for char {
fn to_dom_bits(&self) -> i32 {
map!(
self,
(0x0026, 'a'),
(0x0038, 'b'),
(0x0036, 'c'),
(0x0028, 'd'),
(0x001a, 'e'),
(0x0029, 'f'),
(0x002a, 'g'),
(0x002b, 'h'),
(0x001f, 'i'),
(0x002c, 'j'),
(0x002d, 'k'),
(0x002e, 'l'),
(0x003a, 'm'),
(0x0039, 'n'),
(0x0020, 'o'),
(0x0021, 'p'),
(0x0018, 'q'),
(0x001b, 'r'),
(0x0027, 's'),
(0x001c, 't'),
(0x001e, 'u'),
(0x0037, 'v'),
(0x0019, 'w'),
(0x0035, 'x'),
(0x001d, 'y'),
(0x0034, 'z'),
(0x0026, 'A'),
(0x0038, 'B'),
(0x0036, 'C'),
(0x0028, 'D'),
(0x001a, 'E'),
(0x0029, 'F'),
(0x002a, 'G'),
(0x002b, 'H'),
(0x001f, 'I'),
(0x002c, 'J'),
(0x002d, 'K'),
(0x002e, 'L'),
(0x003a, 'M'),
(0x0039, 'N'),
(0x0020, 'O'),
(0x0021, 'P'),
(0x0018, 'Q'),
(0x001b, 'R'),
(0x0027, 'S'),
(0x001c, 'T'),
(0x001e, 'U'),
(0x0037, 'V'),
(0x0019, 'W'),
(0x0035, 'X'),
(0x001d, 'Y'),
(0x0034, 'Z'),
(0x000a, '1'),
(0x000b, '2'),
(0x000c, '3'),
(0x000d, '4'),
(0x000e, '5'),
(0x000f, '6'),
(0x0010, '7'),
(0x0011, '8'),
(0x0012, '9'),
(0x0013, '0'),
)
}
}

View file

@ -0,0 +1,273 @@
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
use cef::{ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
use winit::dpi::PhysicalPosition;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use super::context::{Context, Initialized};
mod keymap;
use keymap::{ToDomBits, ToVKBits};
pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: &WindowEvent) {
match event {
WindowEvent::Resized(_) => {
if let Some(browser) = &context.browser {
browser.host().unwrap().was_resized();
}
}
WindowEvent::CursorMoved { position, .. } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
host.set_focus(1);
}
context.input_state.update_mouse_position(position);
let mouse_event: MouseEvent = (&context.input_state).into();
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
}
}
WindowEvent::MouseInput { state, button, .. } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
host.set_focus(1);
let mouse_up = match state {
ElementState::Pressed => 0,
ElementState::Released => 1,
};
let cef_button = match button {
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
MouseButton::Forward => None, //TODO: Handle Forward button
MouseButton::Back => None, //TODO: Handle Back button
_ => None,
};
let mut mouse_state = context.input_state.mouse_state.clone();
match button {
MouseButton::Left => {
mouse_state.left = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
MouseButton::Right => {
mouse_state.right = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
MouseButton::Middle => {
mouse_state.middle = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
_ => {}
};
context.input_state.update_mouse_state(mouse_state);
let mouse_event: MouseEvent = (&context.input_state).into();
if let Some(button) = cef_button {
host.send_mouse_click_event(
Some(&mouse_event),
button,
mouse_up,
1, // click count
);
}
}
}
}
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
let mouse_event = (&context.input_state).into();
let line_width = 40; //feels about right, TODO: replace with correct value
let line_height = 30; //feels about right, TODO: replace with correct value
let (delta_x, delta_y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (*x * line_width as f32, *y * line_height as f32),
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
};
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
}
}
}
WindowEvent::ModifiersChanged(modifiers) => {
context.input_state.update_modifiers(&modifiers.state());
}
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
host.set_focus(1);
let (named_key, character) = match &event.logical_key {
winit::keyboard::Key::Named(named_key) => (Some(named_key), None),
winit::keyboard::Key::Character(str) => {
let char = str.chars().next().unwrap_or('\0');
(None, Some(char))
}
_ => return,
};
let mut key_event = KeyEvent::default();
key_event.size = std::mem::size_of::<KeyEvent>();
key_event.focus_on_editable_field = 1;
key_event.modifiers = context.input_state.cef_modifiers(&event.location, event.repeat).raw();
key_event.is_system_key = 0;
if let Some(named_key) = named_key {
key_event.native_key_code = named_key.to_dom_bits();
key_event.windows_key_code = named_key.to_vk_bits();
}
if let Some(char) = character {
key_event.native_key_code = char.to_dom_bits();
key_event.windows_key_code = char.to_vk_bits();
}
match event.state {
ElementState::Pressed => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
host.send_key_event(Some(&key_event));
if let Some(char) = character {
let mut buf = [0; 2];
char.encode_utf16(&mut buf);
key_event.character = buf[0] as u16;
let mut buf = [0; 2];
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
key_event.unmodified_character = buf[0] as u16;
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_CHAR);
host.send_key_event(Some(&key_event));
}
}
ElementState::Released => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_KEYUP);
host.send_key_event(Some(&key_event));
}
};
}
}
}
_ => {}
}
}
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
#[derive(Default, Clone, Debug)]
pub(crate) struct MousePosition {
x: usize,
y: usize,
}
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as usize,
y: position.y as usize,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct InputState {
modifiers: winit::keyboard::ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
}
impl InputState {
fn update_modifiers(&mut self, modifiers: &winit::keyboard::ModifiersState) {
self.modifiers = modifiers.clone();
}
fn update_mouse_position(&mut self, position: &PhysicalPosition<f64>) {
self.mouse_position = position.into();
}
fn update_mouse_state(&mut self, state: MouseState) {
self.mouse_state = state;
}
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(&self, location, is_repeat)
}
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
}
}
impl Into<CefModifiers> for InputState {
fn into(self) -> CefModifiers {
CefModifiers::new(&self, &winit::keyboard::KeyLocation::Standard, false)
}
}
impl Into<MouseEvent> for &InputState {
fn into(self) -> MouseEvent {
MouseEvent {
x: self.mouse_position.x as i32,
y: self.mouse_position.y as i32,
modifiers: self.cef_modifiers_mouse_event().raw(),
}
}
}
struct CefModifiers(u32);
impl CefModifiers {
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
let mut inner = 0;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input_state.modifiers.super_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
}
inner |= match location {
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
winit::keyboard::KeyLocation::Standard => 0,
};
Self(inner)
}
fn raw(&self) -> u32 {
self.0
}
}

View file

@ -0,0 +1,61 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
use super::browser_process_handler::BrowserProcessHandlerImpl;
pub(crate) struct AppImpl<H: EventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
event_handler: H,
}
impl<H: EventHandler> AppImpl<H> {
pub(crate) fn new(event_handler: H) -> App {
App::new(Self {
object: std::ptr::null_mut(),
event_handler,
})
}
}
impl<H: EventHandler> ImplApp for AppImpl<H> {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerImpl::new(self.event_handler.clone()))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}
fn get_raw(&self) -> *mut _cef_app_t {
self.object.cast()
}
}
impl<H: EventHandler> Clone for AppImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: EventHandler> Rc for AppImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: EventHandler> WrapApp for AppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,55 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
use cef::{BrowserProcessHandler, CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
pub(crate) struct BrowserProcessHandlerImpl<H: EventHandler> {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
event_handler: H,
}
impl<H: EventHandler> BrowserProcessHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
})
}
}
impl<H: EventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(Some(&CefString::from("graphite")), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new())));
}
fn get_raw(&self) -> *mut _cef_browser_process_handler_t {
self.object.cast()
}
}
impl<H: EventHandler> Clone for BrowserProcessHandlerImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: EventHandler> Rc for BrowserProcessHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: EventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,52 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{Client, ImplClient, RenderHandler, WrapClient};
pub(crate) struct ClientImpl {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,
}
impl ClientImpl {
pub(crate) fn new(render_handler: RenderHandler) -> Client {
Client::new(Self {
object: std::ptr::null_mut(),
render_handler,
})
}
}
impl ImplClient for ClientImpl {
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn get_raw(&self) -> *mut _cef_client_t {
self.object.cast()
}
}
impl Clone for ClientImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
render_handler: self.render_handler.clone(),
}
}
}
impl Rc for ClientImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapClient for ClientImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,10 @@
mod app;
mod browser_process_handler;
mod client;
mod non_browser_app;
mod render_handler;
pub(crate) use app::AppImpl;
pub(crate) use client::ClientImpl;
pub(crate) use non_browser_app::NonBrowserAppImpl;
pub(crate) use render_handler::RenderHandlerImpl;

View file

@ -0,0 +1,47 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
pub(crate) struct NonBrowserAppImpl {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl NonBrowserAppImpl {
pub(crate) fn new() -> App {
App::new(Self { object: std::ptr::null_mut() })
}
}
impl ImplApp for NonBrowserAppImpl {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}
fn get_raw(&self) -> *mut _cef_app_t {
self.object.cast()
}
}
impl Clone for NonBrowserAppImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for NonBrowserAppImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapApp for NonBrowserAppImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,81 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, ImplRenderHandler, PaintElementType, Rect, RenderHandler, WrapRenderHandler};
use crate::cef::EventHandler;
pub(crate) struct RenderHandlerImpl<H: EventHandler> {
object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H,
}
impl<H: EventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
})
}
}
impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
let view = self.event_handler.view();
*rect = Rect {
x: 0,
y: 0,
width: view.width as i32,
height: view.height as i32,
};
}
}
fn on_paint(
&self,
browser: Option<&mut Browser>,
_type_: PaintElementType,
_dirty_rect_count: usize,
_dirty_rects: Option<&Rect>,
buffer: *const u8,
width: ::std::os::raw::c_int,
height: ::std::os::raw::c_int,
) {
let buffer_size = (width * height * 4) as usize;
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
let draw_successful = self.event_handler.draw(buffer_slice.to_vec(), width as usize, height as usize);
if !draw_successful {
if let Some(browser) = browser {
browser.host().unwrap().was_resized();
}
}
}
fn get_raw(&self) -> *mut _cef_render_handler_t {
self.object.cast()
}
}
impl<H: EventHandler> Clone for RenderHandlerImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: EventHandler> Rc for RenderHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: EventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}

26
desktop/src/cef/mod.rs Normal file
View file

@ -0,0 +1,26 @@
mod input;
mod scheme_handler;
mod context;
mod internal;
pub(crate) trait EventHandler: Clone {
fn view(&self) -> View;
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool;
}
#[derive(Clone)]
pub(crate) struct View {
pub(crate) width: usize,
pub(crate) height: usize,
}
impl View {
pub(crate) fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};

View file

@ -0,0 +1,222 @@
use std::cell::RefCell;
use std::ffi::c_int;
use std::slice::Iter;
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_resource_handler_t, _cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{
Browser, Callback, CefString, Frame, ImplRequest, ImplResourceHandler, ImplResponse, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, ResourceReadCallback, Response,
SchemeRegistrar, WrapResourceHandler, WrapSchemeHandlerFactory,
};
use include_dir::{include_dir, Dir};
pub(crate) struct GraphiteSchemeHandlerFactory {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
}
impl GraphiteSchemeHandlerFactory {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
let mut scheme_options = 0;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
registrar.add_custom_scheme(Some(&CefString::from("graphite")), scheme_options);
}
}
}
impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
if let Some(scheme_name) = scheme_name {
if scheme_name.to_string() != "graphite" {
return None;
}
if let Some(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url.strip_prefix("graphite://").unwrap();
let domain = path.split('/').next().unwrap_or("");
let path = path.strip_prefix(domain).unwrap_or("");
let path = path.trim_start_matches('/');
return match domain {
"frontend" => {
if path.is_empty() {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new("index.html")))
} else {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new(path)))
}
}
_ => None,
};
}
return None;
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
struct GraphiteFrontendResourceHandler<'a> {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
data: Option<RefCell<Iter<'a, u8>>>,
mimetype: Option<String>,
}
impl<'a> GraphiteFrontendResourceHandler<'a> {
pub fn new(path: &str) -> Self {
let file = FRONTEND.get_file(path);
let data = if let Some(file) = file {
Some(RefCell::new(file.contents().iter()))
} else {
println!("Failed to find asset at path: {}", path);
None
};
let mimetype = if let Some(file) = file {
let ext = file.path().extension().and_then(|s| s.to_str()).unwrap_or("");
// We know what file types will be in the assets this should be fine
match ext {
"html" => Some("text/html".to_string()),
"css" => Some("text/css".to_string()),
"wasm" => Some("application/wasm".to_string()),
"js" => Some("application/javascript".to_string()),
"png" => Some("image/png".to_string()),
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
"svg" => Some("image/svg+xml".to_string()),
"xml" => Some("application/xml".to_string()),
"json" => Some("application/json".to_string()),
"ico" => Some("image/x-icon".to_string()),
"woff" => Some("font/woff".to_string()),
"woff2" => Some("font/woff2".to_string()),
"ttf" => Some("font/ttf".to_string()),
"otf" => Some("font/otf".to_string()),
"webmanifest" => Some("application/manifest+json".to_string()),
"graphite" => Some("application/graphite+json".to_string()),
_ => None,
}
} else {
None
};
Self {
object: std::ptr::null_mut(),
data,
mimetype,
}
}
}
impl<'a> ImplResourceHandler for GraphiteFrontendResourceHandler<'a> {
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
if let Some(handle_request) = handle_request {
*handle_request = 1;
}
1
}
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
if let Some(response_length) = response_length {
*response_length = -1; // Indicating that the length is unknown
}
if let Some(response) = response {
if let Some(_) = &self.data {
if let Some(mimetype) = &self.mimetype {
let cef_mime = CefString::from(mimetype.as_str());
response.set_mime_type(Some(&cef_mime));
} else {
response.set_mime_type(None);
}
response.set_status(200);
} else {
response.set_status(404);
response.set_mime_type(Some(&CefString::from("text/plain")));
}
}
}
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
let mut read = 0;
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
if let Some(data) = &self.data {
let mut data = data.borrow_mut();
for i in 0..bytes_to_read as usize {
if let Some(&byte) = data.next() {
out[i] = byte;
read += 1;
} else {
break;
}
}
}
if let Some(bytes_read) = bytes_read {
*bytes_read = read;
}
if read > 0 {
1 // Indicating that data was read
} else {
0 // Indicating no data was read
}
}
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}
impl WrapSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
impl<'a> WrapResourceHandler for GraphiteFrontendResourceHandler<'a> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
self.object = object;
}
}
impl Clone for GraphiteSchemeHandlerFactory {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl<'a> Clone for GraphiteFrontendResourceHandler<'a> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
data: self.data.clone(),
mimetype: self.mimetype.clone(),
}
}
}
impl Rc for GraphiteSchemeHandlerFactory {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<'a> Rc for GraphiteFrontendResourceHandler<'a> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}

314
desktop/src/main.rs Normal file
View file

@ -0,0 +1,314 @@
use std::fmt::Debug;
use std::process::exit;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread;
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::event::*;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::{Window, WindowId};
mod cef;
use cef::Setup;
mod render;
use render::{FrameBuffer, GraphicsState};
pub(crate) enum CustomEvent {
UiUpdate,
Resized,
DoBrowserWork,
}
pub(crate) struct WindowState {
width: Option<usize>,
height: Option<usize>,
ui_fb: Option<FrameBuffer>,
preview_fb: Option<FrameBuffer>,
graphics_state: Option<GraphicsState>,
event_loop_proxy: Option<EventLoopProxy<CustomEvent>>,
}
impl WindowState {
fn new() -> Self {
Self {
width: None,
height: None,
ui_fb: None,
preview_fb: None,
graphics_state: None,
event_loop_proxy: None,
}
}
fn handle(self) -> WindowStateHandle {
WindowStateHandle { inner: Arc::new(Mutex::new(self)) }
}
}
impl Debug for WindowState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowState")
.field("width", &self.width.is_some())
.field("height", &self.height.is_some())
.field("ui_fb", &self.ui_fb.is_some())
.field("preview_fb", &self.preview_fb.is_some())
.field("graphics_state", &self.graphics_state.is_some())
.finish()
}
}
pub(crate) struct WindowStateHandle {
inner: Arc<Mutex<WindowState>>,
}
impl WindowStateHandle {
fn with<'a, P>(&self, p: P) -> Result<(), PoisonError<MutexGuard<'a, WindowState>>>
where
P: FnOnce(&mut WindowState),
{
match self.inner.lock() {
Ok(mut guard) => Ok(p(&mut guard)),
Err(_) => todo!("not error handling yet"),
}
}
}
impl Clone for WindowStateHandle {
fn clone(&self) -> Self {
Self { inner: self.inner.clone() }
}
}
#[derive(Clone)]
struct CefEventHandler {
window_state: WindowStateHandle,
}
impl CefEventHandler {
fn new(window_state: WindowStateHandle) -> Self {
Self { window_state }
}
}
impl cef::EventHandler for CefEventHandler {
fn view(&self) -> cef::View {
let mut w = 1;
let mut h = 1;
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
..
} => {
w = *width;
h = *height;
}
_ => {}
})
.unwrap();
cef::View::new(w, h)
}
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool {
let fb = FrameBuffer::new(buffer, width, height)
.map_err(|e| {
panic!("Failed to create FrameBuffer: {}", e);
})
.unwrap();
let mut correct_size = true;
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::UiUpdate);
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
if width != s.width.unwrap_or(1) || height != s.height.unwrap_or(1) {
correct_size = false;
} else {
s.ui_fb = Some(fb);
}
})
.unwrap();
correct_size
}
}
struct WinitApp {
window_state: WindowStateHandle,
cef_context: cef::Context<cef::Initialized>,
window: Option<Arc<Window>>,
}
impl WinitApp {
fn new(window_state: WindowStateHandle, cef_context: cef::Context<cef::Initialized>) -> Self {
Self {
window_state,
cef_context,
window: None,
}
}
}
impl ApplicationHandler<CustomEvent> for WinitApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.window_state
.with(|s| match s {
WindowState { width: Some(w), height: Some(h), .. } => {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("CEF Offscreen Rendering")
.with_inner_size(winit::dpi::LogicalSize::new(*w as u32, *h as u32)),
)
.unwrap(),
);
let graphics_state = pollster::block_on(GraphicsState::new(window.clone()));
self.window = Some(window.clone());
s.graphics_state = Some(graphics_state);
let _ = thread::spawn(move || loop {
thread::sleep(Duration::from_millis(100));
window.request_redraw();
});
println!("Winit window created and ready");
}
_ => {}
})
.unwrap();
}
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
match event {
CustomEvent::DoBrowserWork => {
self.cef_context.work();
}
CustomEvent::UiUpdate | CustomEvent::Resized => {
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
self.cef_context.handle_window_event(&event);
match event {
WindowEvent::CloseRequested => {
println!("The close button was pressed; stopping");
event_loop.exit();
}
WindowEvent::Resized(physical_size) => {
self.window_state
.with(|s| {
s.width = Some(physical_size.width as usize);
s.height = Some(physical_size.height as usize);
if let Some(elp) = &s.event_loop_proxy {
let _ = elp.send_event(CustomEvent::Resized);
}
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
}
WindowEvent::RedrawRequested => {
self.cef_context.work();
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
graphics_state: Some(graphics_state),
ui_fb,
..
} => {
if let Some(fb) = &*ui_fb {
graphics_state.update_texture(fb);
if fb.width() != *width && fb.height() != *height {
graphics_state.resize(*width, *height);
}
} else {
graphics_state.resize(*width, *height);
}
match graphics_state.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
graphics_state.resize(*width, *height);
}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
})
.unwrap();
}
_ => {}
}
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
}
}
fn main() {
let cef_context = match cef::Context::<Setup>::new() {
Ok(c) => c,
Err(cef::SetupError::Subprocess) => exit(0),
Err(cef::SetupError::SubprocessFailed(t)) => {
println!("Subprocess of type {t} failed");
exit(1);
}
};
let window_state = WindowState::new().handle();
window_state
.with(|s| {
s.width = Some(1200);
s.height = Some(800);
})
.unwrap();
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
window_state.with(|s| s.event_loop_proxy = Some(event_loop.create_proxy())).unwrap();
let cef_context = match cef_context.init(CefEventHandler::new(window_state.clone())) {
Ok(c) => c,
Err(cef::InitError::InitializationFailed) => {
println!("Cef initialization failed");
exit(1);
}
};
println!("Cef initialized successfully");
let mut winit_app = WinitApp::new(window_state, cef_context);
event_loop.run_app(&mut winit_app).unwrap();
winit_app.cef_context.shutdown();
}

View file

@ -0,0 +1,36 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
let coords = (xy / 2. + 0.5);
out.tex_coords = vec2f(coords.x, 1. - coords.y);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}

328
desktop/src/render/mod.rs Normal file
View file

@ -0,0 +1,328 @@
use std::sync::Arc;
use thiserror::Error;
use winit::window::Window;
pub(crate) struct FrameBuffer {
buffer: Vec<u8>,
width: usize,
height: usize,
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")]
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
}
impl FrameBuffer {
pub(crate) fn new(buffer: Vec<u8>, width: usize, height: usize) -> Result<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
&self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
}
}
}
pub(crate) struct GraphicsState {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
render_pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
}
impl GraphicsState {
pub(crate) async fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(window).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
label: None,
memory_hints: Default::default(),
},
None,
)
.await
.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
// Create shader module
let shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen_texture.wgsl"));
// Create sampler
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[&texture_bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
let mut graphics_state = Self {
surface,
device,
queue,
config,
texture: None,
bind_group: None,
render_pipeline,
sampler,
};
// Initialize with a test pattern so we always have something to render
let width = 800;
let height = 600;
let initial_data = vec![34u8; width * height * 4]; // Gray texture #222222FF
let fb = FrameBuffer::new(initial_data, width, height)
.map_err(|e| {
panic!("Failed to create initial FrameBuffer: {}", e);
})
.unwrap();
graphics_state.update_texture(&fb);
graphics_state
}
pub(crate) fn resize(&mut self, width: usize, height: usize) {
if width > 0 && height > 0 && (self.config.width != width as u32 || self.config.height != height as u32) {
self.config.width = width as u32;
self.config.height = height as u32;
self.surface.configure(&self.device, &self.config);
}
}
pub(crate) fn update_texture(&mut self, frame_buffer: &FrameBuffer) {
let data = frame_buffer.buffer();
let width = frame_buffer.width() as u32;
let height = frame_buffer.height() as u32;
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
label: Some("texture_bind_group"),
});
self.texture = Some(texture);
self.bind_group = Some(bind_group);
}
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
} else {
println!("No bind group available - showing clear color only");
}
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
}

View file

@ -217,26 +217,30 @@ impl ApplicationIo for WasmApplicationIo {
}
#[cfg(not(target_arch = "wasm32"))]
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
log::trace!("Spawning window");
todo!("winit api changed, calling create_window on EventLoop is deprecated");
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
use winit::platform::wayland::EventLoopBuilderExtWayland;
// log::trace!("Spawning window");
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
#[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
let event_loop = winit::event_loop::EventLoop::new().unwrap();
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
// use winit::platform::wayland::EventLoopBuilderExtWayland;
let window = winit::window::WindowBuilder::new()
.with_title("Graphite")
.with_inner_size(winit::dpi::PhysicalSize::new(800, 600))
.build(&event_loop)
.unwrap();
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
// let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
// #[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
// let event_loop = winit::event_loop::EventLoop::new().unwrap();
SurfaceHandle {
window_id: SurfaceId(window.id().into()),
surface: Arc::new(window),
}
// let window = event_loop
// .create_window(
// winit::window::WindowAttributes::default()
// .with_title("Graphite")
// .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)),
// )
// .unwrap();
// SurfaceHandle {
// window_id: SurfaceId(window.id().into()),
// surface: Arc::new(window),
// }
}
#[cfg(target_arch = "wasm32")]