mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 21:48:11 +08:00
Merge branch 'master' into refactor-line-gizmos
This commit is contained in:
Vendored
+5
-6
@@ -39,12 +39,11 @@
|
|||||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||||
// Svelte config
|
// Svelte config
|
||||||
"svelte.plugin.svelte.compilerWarnings": {
|
"svelte.plugin.svelte.compilerWarnings": {
|
||||||
// NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
"css-unused-selector": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"css-unused-selector": "ignore",
|
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"vite-plugin-svelte-css-no-scopable-elements": "ignore",
|
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-no-static-element-interactions": "ignore",
|
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-no-noninteractive-element-interactions": "ignore",
|
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-click-events-have-key-events": "ignore"
|
|
||||||
},
|
},
|
||||||
// VS Code config
|
// VS Code config
|
||||||
"html.format.wrapLineLength": 200,
|
"html.format.wrapLineLength": 200,
|
||||||
|
|||||||
Generated
+35
-2
@@ -2077,14 +2077,19 @@ dependencies = [
|
|||||||
name = "graphite-desktop"
|
name = "graphite-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cef",
|
"cef",
|
||||||
|
"core-foundation",
|
||||||
"derivative",
|
"derivative",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures",
|
"futures",
|
||||||
"glam",
|
"glam",
|
||||||
"graphite-desktop-wrapper",
|
"graphite-desktop-wrapper",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
"libc",
|
||||||
|
"objc2-io-surface",
|
||||||
|
"objc2-metal 0.3.1",
|
||||||
"open",
|
"open",
|
||||||
"rfd",
|
"rfd",
|
||||||
"ron",
|
"ron",
|
||||||
@@ -2093,6 +2098,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"vello",
|
"vello",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
|
"windows",
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3489,7 +3495,7 @@ dependencies = [
|
|||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3544,6 +3550,19 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-io-surface"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"libc",
|
||||||
|
"objc2 0.6.1",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-link-presentation"
|
name = "objc2-link-presentation"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3568,6 +3587,20 @@ dependencies = [
|
|||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-metal"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"block2 0.6.1",
|
||||||
|
"dispatch2",
|
||||||
|
"objc2 0.6.1",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3578,7 +3611,7 @@ dependencies = [
|
|||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+18
-18
@@ -1,22 +1,22 @@
|
|||||||
# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`.
|
|
||||||
accepted = [
|
accepted = [
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Apache-2.0",
|
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSL-1.0",
|
"BSL-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"CC0-1.0",
|
"CC0-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"ISC",
|
"ISC", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MIT-0",
|
"MIT-0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MIT",
|
"MIT", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MPL-2.0",
|
"MPL-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"OpenSSL",
|
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Unicode-3.0",
|
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Zlib",
|
"Zlib", # Keep this list in sync with those in `/deny.toml`
|
||||||
"NCSA",
|
"NCSA", # Keep this list in sync with those in `/deny.toml`
|
||||||
"bzip2-1.0.6",
|
"bzip2-1.0.6", # Keep this list in sync with those in `/deny.toml`
|
||||||
|
"OFL-1.1", # Keep this list in sync with those in `/deny.toml`
|
||||||
]
|
]
|
||||||
workarounds = ["ring"]
|
workarounds = ["ring"]
|
||||||
ignore-build-dependencies = true
|
ignore-build-dependencies = true
|
||||||
|
|||||||
@@ -63,25 +63,25 @@ ignore = [
|
|||||||
# See https://spdx.org/licenses/ for list of possible licenses
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
#
|
#
|
||||||
# Keep this list in sync with those in `/about.toml` and `/frontend/vite.config.ts`.
|
|
||||||
allow = [
|
allow = [
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
||||||
"Apache-2.0",
|
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSL-1.0",
|
"BSL-1.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"CC0-1.0",
|
"CC0-1.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"ISC",
|
"ISC", # Keep this list in sync with those in `/about.toml`
|
||||||
"MIT-0",
|
"MIT-0", # Keep this list in sync with those in `/about.toml`
|
||||||
"MIT",
|
"MIT", # Keep this list in sync with those in `/about.toml`
|
||||||
"MPL-2.0",
|
"MPL-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"OpenSSL",
|
"OpenSSL", # Keep this list in sync with those in `/about.toml`
|
||||||
"Unicode-3.0",
|
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
|
||||||
"Zlib",
|
"Zlib", # Keep this list in sync with those in `/about.toml`
|
||||||
"NCSA",
|
"NCSA", # Keep this list in sync with those in `/about.toml`
|
||||||
"bzip2-1.0.6",
|
"bzip2-1.0.6", # Keep this list in sync with those in `/about.toml`
|
||||||
|
"OFL-1.1", # Keep this list in sync with those in `/about.toml`
|
||||||
]
|
]
|
||||||
# The confidence threshold for detecting a license from license text.
|
# The confidence threshold for detecting a license from license text.
|
||||||
# The higher the value, the more closely the license text must be to the
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
|||||||
+30
-1
@@ -9,9 +9,15 @@ edition = "2024"
|
|||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gpu"]
|
default = ["gpu", "accelerated_paint"]
|
||||||
gpu = ["graphite-desktop-wrapper/gpu"]
|
gpu = ["graphite-desktop-wrapper/gpu"]
|
||||||
|
|
||||||
|
# Hardware acceleration features
|
||||||
|
accelerated_paint = ["accelerated_paint_dmabuf", "accelerated_paint_d3d11", "accelerated_paint_iosurface"]
|
||||||
|
accelerated_paint_dmabuf = ["libc", "ash"]
|
||||||
|
accelerated_paint_d3d11 = ["windows", "ash"]
|
||||||
|
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# # Local dependencies
|
# # Local dependencies
|
||||||
graphite-desktop-wrapper = { path = "wrapper" }
|
graphite-desktop-wrapper = { path = "wrapper" }
|
||||||
@@ -32,3 +38,26 @@ vello = { workspace = true }
|
|||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
|
|
||||||
|
# Hardware acceleration dependencies
|
||||||
|
ash = { version = "0.38", optional = true }
|
||||||
|
|
||||||
|
# Windows-specific dependencies
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { version = "0.58", features = [
|
||||||
|
"Win32_Graphics_Direct3D11",
|
||||||
|
"Win32_Graphics_Direct3D12",
|
||||||
|
"Win32_Graphics_Dxgi",
|
||||||
|
"Win32_Graphics_Dxgi_Common",
|
||||||
|
"Win32_Foundation"
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
|
# macOS-specific dependencies
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2-io-surface = { version = "0.3", optional = true }
|
||||||
|
objc2-metal = { version = "0.3", optional = true }
|
||||||
|
core-foundation = { version = "0.9", optional = true }
|
||||||
|
|
||||||
|
# Linux-specific dependencies
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
libc = { version = "0.2", optional = true }
|
||||||
|
|||||||
+49
-29
@@ -1,18 +1,19 @@
|
|||||||
use crate::CustomEvent;
|
use crate::CustomEvent;
|
||||||
use crate::cef::WindowSize;
|
use crate::cef::WindowSize;
|
||||||
use crate::consts::APP_NAME;
|
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
|
||||||
use crate::render::GraphicsState;
|
use crate::render::GraphicsState;
|
||||||
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
||||||
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||||
|
|
||||||
use rfd::AsyncFileDialog;
|
use rfd::AsyncFileDialog;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::StartCause;
|
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
use winit::event_loop::ControlFlow;
|
use winit::event_loop::ControlFlow;
|
||||||
@@ -23,7 +24,7 @@ use winit::window::WindowId;
|
|||||||
use crate::cef;
|
use crate::cef;
|
||||||
|
|
||||||
pub(crate) struct WinitApp {
|
pub(crate) struct WinitApp {
|
||||||
cef_context: cef::Context<cef::Initialized>,
|
cef_context: Box<dyn cef::CefContext>,
|
||||||
window: Option<Arc<Window>>,
|
window: Option<Arc<Window>>,
|
||||||
cef_schedule: Option<Instant>,
|
cef_schedule: Option<Instant>,
|
||||||
window_size_sender: Sender<WindowSize>,
|
window_size_sender: Sender<WindowSize>,
|
||||||
@@ -31,11 +32,23 @@ pub(crate) struct WinitApp {
|
|||||||
wgpu_context: WgpuContext,
|
wgpu_context: WgpuContext,
|
||||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
||||||
desktop_wrapper: DesktopWrapper,
|
desktop_wrapper: DesktopWrapper,
|
||||||
|
last_ui_update: Instant,
|
||||||
|
avg_frame_time: f32,
|
||||||
|
start_render_sender: SyncSender<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinitApp {
|
impl WinitApp {
|
||||||
pub(crate) fn new(cef_context: cef::Context<cef::Initialized>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
|
pub(crate) fn new(cef_context: Box<dyn cef::CefContext>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
|
||||||
let desktop_wrapper = DesktopWrapper::new();
|
let rendering_loop_proxy = event_loop_proxy.clone();
|
||||||
|
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
||||||
|
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
|
||||||
|
let _ = start_render_receiver.recv();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cef_context,
|
cef_context,
|
||||||
window: None,
|
window: None,
|
||||||
@@ -44,7 +57,10 @@ impl WinitApp {
|
|||||||
window_size_sender,
|
window_size_sender,
|
||||||
wgpu_context,
|
wgpu_context,
|
||||||
event_loop_proxy,
|
event_loop_proxy,
|
||||||
desktop_wrapper,
|
desktop_wrapper: DesktopWrapper::new(),
|
||||||
|
last_ui_update: Instant::now(),
|
||||||
|
avg_frame_time: 0.,
|
||||||
|
start_render_sender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +71,7 @@ impl WinitApp {
|
|||||||
tracing::error!("Failed to serialize frontend messages");
|
tracing::error!("Failed to serialize frontend messages");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.cef_context.send_web_message(bytes.as_slice());
|
self.cef_context.send_web_message(bytes);
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
|
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
|
||||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||||
@@ -152,23 +168,20 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||||||
// Set a timeout in case we miss any cef schedule requests
|
// Set a timeout in case we miss any cef schedule requests
|
||||||
let timeout = Instant::now() + Duration::from_millis(10);
|
let timeout = Instant::now() + Duration::from_millis(10);
|
||||||
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
|
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
|
||||||
self.cef_context.work();
|
|
||||||
|
|
||||||
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
|
|
||||||
if let Some(schedule) = self.cef_schedule
|
if let Some(schedule) = self.cef_schedule
|
||||||
&& schedule < Instant::now()
|
&& schedule < Instant::now()
|
||||||
{
|
{
|
||||||
self.cef_schedule = None;
|
self.cef_schedule = None;
|
||||||
self.cef_context.work();
|
// Poll cef message loop multiple times to avoid message loop starvation
|
||||||
}
|
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
|
||||||
if let StartCause::ResumeTimeReached { .. } = cause {
|
self.cef_context.work();
|
||||||
if let Some(window) = &self.window {
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(window) = &self.window.as_ref() {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
@@ -177,7 +190,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||||||
.with_min_inner_size(winit::dpi::LogicalSize::new(400, 300))
|
.with_min_inner_size(winit::dpi::LogicalSize::new(400, 300))
|
||||||
.with_inner_size(winit::dpi::LogicalSize::new(1200, 800));
|
.with_inner_size(winit::dpi::LogicalSize::new(1200, 800));
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
use crate::consts::APP_ID;
|
use crate::consts::APP_ID;
|
||||||
use winit::platform::wayland::ActiveEventLoopExtWayland;
|
use winit::platform::wayland::ActiveEventLoopExtWayland;
|
||||||
@@ -220,6 +233,11 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||||||
if let Some(graphics_state) = self.graphics_state.as_mut() {
|
if let Some(graphics_state) = self.graphics_state.as_mut() {
|
||||||
graphics_state.resize(texture.width(), texture.height());
|
graphics_state.resize(texture.width(), texture.height());
|
||||||
graphics_state.bind_ui_texture(texture);
|
graphics_state.bind_ui_texture(texture);
|
||||||
|
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
|
||||||
|
self.last_ui_update = Instant::now();
|
||||||
|
if elapsed < 0.5 {
|
||||||
|
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -236,7 +254,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||||
let Some(event) = self.cef_context.handle_window_event(event) else { return };
|
self.cef_context.handle_window_event(&event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
@@ -251,16 +269,18 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
let Some(ref mut graphics_state) = self.graphics_state else { return };
|
let Some(ref mut graphics_state) = self.graphics_state else { return };
|
||||||
// Only rerender once we have a new ui texture to display
|
// Only rerender once we have a new ui texture to display
|
||||||
|
if let Some(window) = &self.window {
|
||||||
match graphics_state.render() {
|
match graphics_state.render(window.as_ref()) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
tracing::warn!("lost surface");
|
tracing::warn!("lost surface");
|
||||||
|
}
|
||||||
|
Err(wgpu::SurfaceError::OutOfMemory) => {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("{:?}", e),
|
||||||
}
|
}
|
||||||
Err(wgpu::SurfaceError::OutOfMemory) => {
|
let _ = self.start_render_sender.try_send(());
|
||||||
event_loop.exit();
|
|
||||||
}
|
|
||||||
Err(e) => tracing::error!("{:?}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
|
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
|
||||||
|
|||||||
+37
-1
@@ -1,3 +1,18 @@
|
|||||||
|
//! CEF (Chromium Embedded Framework) integration for Graphite Desktop
|
||||||
|
//!
|
||||||
|
//! This module provides CEF browser integration with hardware-accelerated texture sharing.
|
||||||
|
//!
|
||||||
|
//! # Hardware Acceleration
|
||||||
|
//!
|
||||||
|
//! The texture import system supports platform-specific hardware acceleration:
|
||||||
|
//!
|
||||||
|
//! - **Linux**: DMA-BUF via Vulkan external memory (`accelerated_paint_dmabuf` feature)
|
||||||
|
//! - **Windows**: D3D11 shared textures via either Vulkan or D3D12 interop (`accelerated_paint_d3d11` feature)
|
||||||
|
//! - **macOS**: IOSurface via Metal/Vulkan interop (`accelerated_paint_iosurface` feature)
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! The system gracefully falls back to CPU textures when hardware acceleration is unavailable.
|
||||||
|
|
||||||
use crate::CustomEvent;
|
use crate::CustomEvent;
|
||||||
use crate::render::FrameBufferRef;
|
use crate::render::FrameBufferRef;
|
||||||
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
|
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
|
||||||
@@ -5,20 +20,29 @@ use std::sync::mpsc::Receiver;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
mod consts;
|
||||||
mod context;
|
mod context;
|
||||||
mod dirs;
|
mod dirs;
|
||||||
mod input;
|
mod input;
|
||||||
mod internal;
|
mod internal;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
mod platform;
|
||||||
mod scheme_handler;
|
mod scheme_handler;
|
||||||
mod utility;
|
mod utility;
|
||||||
|
|
||||||
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
mod texture_import;
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
use texture_import::SharedTextureHandle;
|
||||||
|
|
||||||
|
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
|
||||||
use winit::event_loop::EventLoopProxy;
|
use winit::event_loop::EventLoopProxy;
|
||||||
|
|
||||||
pub(crate) trait CefEventHandler: Clone {
|
pub(crate) trait CefEventHandler: Clone {
|
||||||
fn window_size(&self) -> WindowSize;
|
fn window_size(&self) -> WindowSize;
|
||||||
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
|
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
|
||||||
/// Scheudule the main event loop to run the cef event loop after the timeout
|
/// Scheudule the main event loop to run the cef event loop after the timeout
|
||||||
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
||||||
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
|
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
|
||||||
@@ -128,4 +152,16 @@ impl CefEventHandler for CefHandler {
|
|||||||
};
|
};
|
||||||
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
|
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
|
||||||
|
match shared_texture.import_texture(&self.wgpu_context.device) {
|
||||||
|
Ok(texture) => {
|
||||||
|
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to import shared texture: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
|
||||||
|
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
|
||||||
+9
-155
@@ -1,161 +1,15 @@
|
|||||||
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
|
mod multithreaded;
|
||||||
use cef::{App, BrowserSettings, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, RenderHandler, RequestContext, WindowInfo, browser_host_create_browser_sync, initialize};
|
mod singlethreaded;
|
||||||
use cef::{Browser, CefString, Settings, api_hash, args::Args, execute_process};
|
|
||||||
use thiserror::Error;
|
|
||||||
use winit::event::WindowEvent;
|
|
||||||
|
|
||||||
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
mod builder;
|
||||||
|
pub(crate) use builder::{CefContextBuilder, InitError};
|
||||||
|
|
||||||
use super::input::InputState;
|
pub(crate) trait CefContext {
|
||||||
use super::ipc::{MessageType, SendMessage};
|
fn work(&mut self);
|
||||||
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
|
||||||
use super::{CefEventHandler, input};
|
|
||||||
|
|
||||||
use super::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
|
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
|
||||||
|
|
||||||
pub(crate) struct Setup {}
|
fn notify_of_resize(&self);
|
||||||
pub(crate) struct Initialized {}
|
|
||||||
pub(crate) trait ContextState {}
|
|
||||||
impl ContextState for Setup {}
|
|
||||||
impl ContextState for Initialized {}
|
|
||||||
|
|
||||||
pub(crate) struct Context<S: ContextState> {
|
fn send_web_message(&self, message: Vec<u8>);
|
||||||
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 = RenderProcessAppImpl::app();
|
|
||||||
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 CefEventHandler) -> Result<Context<Initialized>, InitError> {
|
|
||||||
let settings = Settings {
|
|
||||||
windowless_rendering_enabled: 1,
|
|
||||||
multi_threaded_message_loop: 0,
|
|
||||||
external_message_pump: 1,
|
|
||||||
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
|
|
||||||
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
|
|
||||||
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
|
|
||||||
|
|
||||||
let result = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
|
||||||
if result != 1 {
|
|
||||||
let cef_exit_code = cef::get_exit_code() as u32;
|
|
||||||
if cef_exit_code == cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED as u32 {
|
|
||||||
return Err(InitError::AlreadyRunning);
|
|
||||||
}
|
|
||||||
return Err(InitError::InitializationFailed(cef_exit_code));
|
|
||||||
}
|
|
||||||
|
|
||||||
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
|
|
||||||
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
|
|
||||||
|
|
||||||
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
|
|
||||||
|
|
||||||
let window_info = WindowInfo {
|
|
||||||
windowless_rendering_enabled: 1,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let settings = BrowserSettings {
|
|
||||||
windowless_frame_rate: 60,
|
|
||||||
background_color: 0x0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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.clone(),
|
|
||||||
browser,
|
|
||||||
input_state: self.input_state.clone(),
|
|
||||||
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) -> Option<WindowEvent> {
|
|
||||||
input::handle_window_event(self, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn notify_of_resize(&self) {
|
|
||||||
if let Some(browser) = &self.browser {
|
|
||||||
browser.host().unwrap().was_resized();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn send_web_message(&self, message: &[u8]) {
|
|
||||||
self.send_message(MessageType::SendToJS, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: ContextState> Drop for Context<S> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.browser.is_some() {
|
|
||||||
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(u32),
|
|
||||||
#[error("Another instance is already running")]
|
|
||||||
AlreadyRunning,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use cef::args::Args;
|
||||||
|
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
|
||||||
|
use cef::{
|
||||||
|
App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, RenderHandler, RequestContext, Settings, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::CefContext;
|
||||||
|
use super::singlethreaded::SingleThreadedCefContext;
|
||||||
|
use crate::cef::CefHandler;
|
||||||
|
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||||
|
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
||||||
|
use crate::cef::input::InputState;
|
||||||
|
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
|
||||||
|
|
||||||
|
pub(crate) struct CefContextBuilder {
|
||||||
|
pub(crate) args: Args,
|
||||||
|
pub(crate) is_sub_process: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for CefContextBuilder {}
|
||||||
|
|
||||||
|
impl CefContextBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
#[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_sub_process = cmd.has_switch(Some(&switch)) == 1;
|
||||||
|
|
||||||
|
Self { args, is_sub_process }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_sub_process(&self) -> bool {
|
||||||
|
self.is_sub_process
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn execute_sub_process(&self) -> SetupError {
|
||||||
|
let cmd = self.args.as_cmd_line().unwrap();
|
||||||
|
let switch = CefString::from("type");
|
||||||
|
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
|
||||||
|
let mut app = RenderProcessAppImpl::app();
|
||||||
|
let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut());
|
||||||
|
if ret >= 0 {
|
||||||
|
SetupError::SubprocessFailed(process_type.to_string())
|
||||||
|
} else {
|
||||||
|
SetupError::Subprocess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
|
||||||
|
let settings = Settings {
|
||||||
|
windowless_rendering_enabled: 1,
|
||||||
|
multi_threaded_message_loop: 0,
|
||||||
|
external_message_pump: 1,
|
||||||
|
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
|
||||||
|
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.initialize_inner(&event_handler, settings)?;
|
||||||
|
|
||||||
|
create_browser(event_handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
|
||||||
|
let settings = Settings {
|
||||||
|
windowless_rendering_enabled: 1,
|
||||||
|
multi_threaded_message_loop: 1,
|
||||||
|
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
|
||||||
|
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.initialize_inner(&event_handler, settings)?;
|
||||||
|
|
||||||
|
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler) {
|
||||||
|
Ok(context) => {
|
||||||
|
super::multithreaded::CONTEXT.with(|b| {
|
||||||
|
*b.borrow_mut() = Some(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to initialize CEF context: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(super::multithreaded::MultiThreadedCefContextProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_inner(self, event_handler: &CefHandler, settings: Settings) -> Result<(), InitError> {
|
||||||
|
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
|
||||||
|
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
||||||
|
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
|
||||||
|
|
||||||
|
if result != 1 {
|
||||||
|
let cef_exit_code = cef::get_exit_code() as u32;
|
||||||
|
if cef_exit_code == cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED as u32 {
|
||||||
|
return Err(InitError::AlreadyRunning);
|
||||||
|
}
|
||||||
|
return Err(InitError::InitializationFailed(cef_exit_code));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_browser(event_handler: CefHandler) -> Result<SingleThreadedCefContext, InitError> {
|
||||||
|
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
|
||||||
|
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
|
||||||
|
|
||||||
|
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
|
||||||
|
|
||||||
|
let window_info = WindowInfo {
|
||||||
|
windowless_rendering_enabled: 1,
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
shared_texture_enabled: if crate::cef::platform::should_enable_hardware_acceleration() { 1 } else { 0 },
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = BrowserSettings {
|
||||||
|
windowless_frame_rate: crate::consts::CEF_WINDOWLESS_FRAME_RATE,
|
||||||
|
background_color: 0x0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(browser) = browser {
|
||||||
|
Ok(SingleThreadedCefContext {
|
||||||
|
browser,
|
||||||
|
input_state: InputState::default(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to create browser");
|
||||||
|
Err(InitError::BrowserCreationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::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(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum InitError {
|
||||||
|
#[error("Initialization failed")]
|
||||||
|
InitializationFailed(u32),
|
||||||
|
#[error("Browser creation failed")]
|
||||||
|
BrowserCreationFailed,
|
||||||
|
#[error("Another instance is already running")]
|
||||||
|
AlreadyRunning,
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use cef::sys::cef_thread_id_t;
|
||||||
|
use cef::{Task, ThreadId, post_task};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
|
||||||
|
use crate::cef::internal::task::ClosureTask;
|
||||||
|
|
||||||
|
use super::CefContext;
|
||||||
|
use super::singlethreaded::SingleThreadedCefContext;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
pub(super) static CONTEXT: RefCell<Option<SingleThreadedCefContext>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct MultiThreadedCefContextProxy;
|
||||||
|
|
||||||
|
impl CefContext for MultiThreadedCefContextProxy {
|
||||||
|
fn work(&mut self) {
|
||||||
|
// CEF handles its own message loop in multi-threaded mode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_window_event(&mut self, event: &WindowEvent) {
|
||||||
|
let event_clone = event.clone();
|
||||||
|
run_on_ui_thread(move || {
|
||||||
|
CONTEXT.with(|b| {
|
||||||
|
if let Some(context) = b.borrow_mut().as_mut() {
|
||||||
|
context.handle_window_event(&event_clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_of_resize(&self) {
|
||||||
|
run_on_ui_thread(move || {
|
||||||
|
CONTEXT.with(|b| {
|
||||||
|
if let Some(context) = b.borrow_mut().as_mut() {
|
||||||
|
context.notify_of_resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_web_message(&self, message: Vec<u8>) {
|
||||||
|
run_on_ui_thread(move || {
|
||||||
|
CONTEXT.with(|b| {
|
||||||
|
if let Some(context) = b.borrow_mut().as_mut() {
|
||||||
|
context.send_web_message(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MultiThreadedCefContextProxy {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
cef::shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn run_on_ui_thread<F>(closure: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() + Send + 'static,
|
||||||
|
{
|
||||||
|
let closure_task = ClosureTask::new(closure);
|
||||||
|
let mut task = Task::new(closure_task);
|
||||||
|
post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task));
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use cef::{Browser, ImplBrowser, ImplBrowserHost};
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
|
||||||
|
use crate::cef::input;
|
||||||
|
use crate::cef::input::InputState;
|
||||||
|
use crate::cef::ipc::{MessageType, SendMessage};
|
||||||
|
|
||||||
|
use super::CefContext;
|
||||||
|
|
||||||
|
pub(super) struct SingleThreadedCefContext {
|
||||||
|
pub(super) browser: Browser,
|
||||||
|
pub(super) input_state: InputState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CefContext for SingleThreadedCefContext {
|
||||||
|
fn work(&mut self) {
|
||||||
|
cef::do_message_loop_work();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_window_event(&mut self, event: &WindowEvent) {
|
||||||
|
input::handle_window_event(&self.browser, &mut self.input_state, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_of_resize(&self) {
|
||||||
|
self.browser.host().unwrap().was_resized();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_web_message(&self, message: Vec<u8>) {
|
||||||
|
self.send_message(MessageType::SendToJS, &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SingleThreadedCefContext {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
cef::shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendMessage for SingleThreadedCefContext {
|
||||||
|
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||||
|
let Some(frame) = self.browser.main_frame() else {
|
||||||
|
tracing::error!("Main frame is not available, cannot send message");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
frame.send_message(message_type, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+125
-127
@@ -1,164 +1,153 @@
|
|||||||
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
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 cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
|
||||||
use winit::dpi::PhysicalPosition;
|
use winit::dpi::PhysicalPosition;
|
||||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
|
|
||||||
use super::context::{Context, Initialized};
|
|
||||||
|
|
||||||
mod keymap;
|
mod keymap;
|
||||||
use keymap::{ToDomBits, ToVKBits};
|
use keymap::{ToDomBits, ToVKBits};
|
||||||
|
|
||||||
pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: WindowEvent) -> Option<WindowEvent> {
|
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CursorMoved { position, .. } => {
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
if let Some(host) = browser.host() {
|
||||||
if let Some(host) = browser.host() {
|
host.set_focus(1);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input_state.update_mouse_position(position);
|
||||||
|
let mouse_event: MouseEvent = (input_state).into();
|
||||||
|
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
|
||||||
}
|
}
|
||||||
WindowEvent::MouseInput { state, button, .. } => {
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
if let Some(host) = browser.host() {
|
||||||
if let Some(host) = browser.host() {
|
host.set_focus(1);
|
||||||
host.set_focus(1);
|
|
||||||
|
|
||||||
let mouse_up = match state {
|
let mouse_up = match state {
|
||||||
ElementState::Pressed => 0,
|
ElementState::Pressed => 0,
|
||||||
ElementState::Released => 1,
|
ElementState::Released => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cef_button = match button {
|
let cef_button = match button {
|
||||||
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
|
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::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::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
|
||||||
MouseButton::Forward => None, //TODO: Handle Forward button
|
MouseButton::Forward => None, //TODO: Handle Forward button
|
||||||
MouseButton::Back => None, //TODO: Handle Back button
|
MouseButton::Back => None, //TODO: Handle Back button
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut mouse_state = context.input_state.mouse_state.clone();
|
let mut mouse_state = input_state.mouse_state.clone();
|
||||||
match button {
|
match button {
|
||||||
MouseButton::Left => {
|
MouseButton::Left => {
|
||||||
mouse_state.left = match state {
|
mouse_state.left = match state {
|
||||||
ElementState::Pressed => true,
|
ElementState::Pressed => true,
|
||||||
ElementState::Released => false,
|
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
input_state.update_mouse_state(mouse_state);
|
||||||
|
|
||||||
|
let mouse_event: MouseEvent = 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: _, .. } => {
|
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
if let Some(host) = browser.host() {
|
||||||
if let Some(host) = browser.host() {
|
let mouse_event = input_state.into();
|
||||||
let mouse_event = (&context.input_state).into();
|
let line_width = 40; //feels about right, TODO: replace with correct value
|
||||||
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 line_height = 30; //feels about right, TODO: replace with correct value
|
let (delta_x, delta_y) = match delta {
|
||||||
let (delta_x, delta_y) = match delta {
|
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
||||||
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),
|
||||||
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);
|
||||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::ModifiersChanged(modifiers) => {
|
WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
context.input_state.update_modifiers(&modifiers.state());
|
input_state.update_modifiers(&modifiers.state());
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||||
if let Some(browser) = &context.browser {
|
if let Some(host) = browser.host() {
|
||||||
if let Some(host) = browser.host() {
|
host.set_focus(1);
|
||||||
host.set_focus(1);
|
|
||||||
|
|
||||||
let (named_key, character) = match &event.logical_key {
|
let (named_key, character) = match &event.logical_key {
|
||||||
winit::keyboard::Key::Named(named_key) => (
|
winit::keyboard::Key::Named(named_key) => (
|
||||||
Some(named_key),
|
Some(named_key),
|
||||||
match named_key {
|
match named_key {
|
||||||
winit::keyboard::NamedKey::Space => Some(' '),
|
winit::keyboard::NamedKey::Space => Some(' '),
|
||||||
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
winit::keyboard::Key::Character(str) => {
|
winit::keyboard::Key::Character(str) => {
|
||||||
let char = str.chars().next().unwrap_or('\0');
|
let char = str.chars().next().unwrap_or('\0');
|
||||||
(None, Some(char))
|
(None, Some(char))
|
||||||
}
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut key_event = KeyEvent {
|
|
||||||
size: size_of::<KeyEvent>(),
|
|
||||||
focus_on_editable_field: 1,
|
|
||||||
modifiers: context.input_state.cef_modifiers(&event.location, event.repeat).raw(),
|
|
||||||
is_system_key: 0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
} else if let Some(char) = character {
|
|
||||||
key_event.native_key_code = char.to_dom_bits();
|
|
||||||
key_event.windows_key_code = char.to_vk_bits();
|
|
||||||
}
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
match event.state {
|
let mut key_event = KeyEvent {
|
||||||
ElementState::Pressed => {
|
size: size_of::<KeyEvent>(),
|
||||||
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
|
focus_on_editable_field: 1,
|
||||||
host.send_key_event(Some(&key_event));
|
modifiers: input_state.cef_modifiers(&event.location, event.repeat).raw(),
|
||||||
|
is_system_key: 0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(char) = character {
|
if let Some(named_key) = named_key {
|
||||||
let mut buf = [0; 2];
|
key_event.native_key_code = named_key.to_dom_bits();
|
||||||
char.encode_utf16(&mut buf);
|
key_event.windows_key_code = named_key.to_vk_bits();
|
||||||
key_event.character = buf[0];
|
} else if let Some(char) = character {
|
||||||
let mut buf = [0; 2];
|
key_event.native_key_code = char.to_dom_bits();
|
||||||
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
|
key_event.windows_key_code = char.to_vk_bits();
|
||||||
key_event.unmodified_character = buf[0];
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
|
||||||
|
key_event.unmodified_character = buf[0];
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e => return Some(e),
|
_ => {}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -227,6 +216,15 @@ impl From<&InputState> for MouseEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<&mut InputState> for MouseEvent {
|
||||||
|
fn from(val: &mut InputState) -> Self {
|
||||||
|
MouseEvent {
|
||||||
|
x: val.mouse_position.x as i32,
|
||||||
|
y: val.mouse_position.y as i32,
|
||||||
|
modifiers: val.cef_modifiers_mouse_event().raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CefModifiers(u32);
|
struct CefModifiers(u32);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ mod browser_process_app;
|
|||||||
mod browser_process_client;
|
mod browser_process_client;
|
||||||
mod browser_process_handler;
|
mod browser_process_handler;
|
||||||
mod browser_process_life_span_handler;
|
mod browser_process_life_span_handler;
|
||||||
mod render_handler;
|
|
||||||
mod render_process_app;
|
mod render_process_app;
|
||||||
mod render_process_handler;
|
mod render_process_handler;
|
||||||
mod render_process_v8_handler;
|
mod render_process_v8_handler;
|
||||||
|
|
||||||
|
pub(super) mod render_handler;
|
||||||
|
pub(super) mod task;
|
||||||
|
|
||||||
pub(super) use browser_process_app::BrowserProcessAppImpl;
|
pub(super) use browser_process_app::BrowserProcessAppImpl;
|
||||||
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
||||||
pub(super) use render_handler::RenderHandlerImpl;
|
pub(super) use render_handler::RenderHandlerImpl;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#[cfg(target_os = "linux")]
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use cef::rc::{Rc, RcImpl};
|
use cef::rc::{Rc, RcImpl};
|
||||||
@@ -34,12 +35,28 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
|
|||||||
|
|
||||||
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||||
if let Some(cmd) = command_line {
|
if let Some(cmd) = command_line {
|
||||||
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
|
#[cfg(not(feature = "accelerated_paint"))]
|
||||||
cmd.append_switch(Some(&CefString::from("disable-gpu")));
|
{
|
||||||
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
|
// Disable GPU acceleration when accelerated_paint feature is not enabled
|
||||||
|
cmd.append_switch(Some(&CefString::from("disable-gpu")));
|
||||||
|
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
{
|
||||||
|
// Enable GPU acceleration switches for better performance
|
||||||
|
cmd.append_switch(Some(&CefString::from("enable-gpu-rasterization")));
|
||||||
|
cmd.append_switch(Some(&CefString::from("enable-accelerated-2d-canvas")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "accelerated_paint", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// Use Vulkan for accelerated painting
|
||||||
|
cmd.append_switch_with_value(Some(&CefString::from("use-angle")), Some(&CefString::from("vulkan")));
|
||||||
|
}
|
||||||
|
|
||||||
// Tell CEF to use Wayland if available
|
// Tell CEF to use Wayland if available
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let use_wayland = env::var("WAYLAND_DISPLAY")
|
let use_wayland = env::var("WAYLAND_DISPLAY")
|
||||||
.ok()
|
.ok()
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_brows
|
|||||||
use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
|
use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
|
||||||
|
|
||||||
use crate::cef::CefEventHandler;
|
use crate::cef::CefEventHandler;
|
||||||
use crate::cef::scheme_handler::{GRAPHITE_SCHEME, GraphiteSchemeHandlerFactory};
|
use crate::cef::consts::GRAPHITE_SCHEME;
|
||||||
|
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||||
|
|
||||||
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
||||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub(crate) struct RenderHandlerImpl<H: CefEventHandler> {
|
|||||||
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
||||||
event_handler: H,
|
event_handler: H,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
||||||
pub(crate) fn new(event_handler: H) -> Self {
|
pub(crate) fn new(event_handler: H) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -47,6 +48,23 @@ impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
|||||||
self.event_handler.draw(frame_buffer)
|
self.event_handler.draw(frame_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rect_count: usize, _dirty_rects: Option<&Rect>, info: Option<&cef::AcceleratedPaintInfo>) {
|
||||||
|
use crate::cef::texture_import::shared_texture_handle::SharedTextureHandle;
|
||||||
|
|
||||||
|
if type_ != PaintElementType::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_handle = SharedTextureHandle::new(info.unwrap());
|
||||||
|
if let SharedTextureHandle::Unsupported = shared_handle {
|
||||||
|
tracing::error!("Platform does not support accelerated painting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.event_handler.draw_gpu(shared_handle);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
||||||
self.object.cast()
|
self.object.cast()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{ImplTask, WrapTask};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
// Closure-based task wrapper following CEF patterns
|
||||||
|
pub struct ClosureTask<F> {
|
||||||
|
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
|
||||||
|
pub(crate) closure: RefCell<Option<F>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
|
||||||
|
pub fn new(closure: F) -> Self {
|
||||||
|
Self {
|
||||||
|
object: std::ptr::null_mut(),
|
||||||
|
closure: RefCell::new(Some(closure)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
|
||||||
|
fn execute(&self) {
|
||||||
|
if let Some(closure) = self.closure.borrow_mut().take() {
|
||||||
|
closure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_task_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
unsafe {
|
||||||
|
if !self.object.is_null() {
|
||||||
|
let rc_impl = &mut *self.object;
|
||||||
|
rc_impl.interface.add_ref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
object: self.object,
|
||||||
|
closure: RefCell::new(None), // Closure can only be executed once
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-18
@@ -1,6 +1,4 @@
|
|||||||
use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
use cef::{CefString, Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||||
|
|
||||||
use super::{Context, Initialized};
|
|
||||||
|
|
||||||
pub(crate) enum MessageType {
|
pub(crate) enum MessageType {
|
||||||
SendToJS,
|
SendToJS,
|
||||||
@@ -39,21 +37,6 @@ pub(crate) struct MessageInfo {
|
|||||||
pub(crate) trait SendMessage {
|
pub(crate) trait SendMessage {
|
||||||
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
||||||
}
|
}
|
||||||
impl SendMessage for Context<Initialized> {
|
|
||||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
|
||||||
let Some(browser) = &self.browser else {
|
|
||||||
tracing::error!("Browser is not initialized, cannot send message");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(frame) = browser.main_frame() else {
|
|
||||||
tracing::error!("Main frame is not available, cannot send message");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
frame.send_message(message_type, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SendMessage for Option<V8Context> {
|
impl SendMessage for Option<V8Context> {
|
||||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||||
let Some(context) = self else {
|
let Some(context) = self else {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#[cfg(feature = "accelerated_paint")]
|
||||||
|
pub fn should_enable_hardware_acceleration() -> bool {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Check if running on Wayland or X11
|
||||||
|
let has_wayland = std::env::var("WAYLAND_DISPLAY")
|
||||||
|
.ok()
|
||||||
|
.filter(|var| !var.is_empty())
|
||||||
|
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||||
|
.filter(|var| !var.is_empty())
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
let has_x11 = std::env::var("DISPLAY").ok().filter(|var| !var.is_empty()).is_some();
|
||||||
|
|
||||||
|
if !has_wayland && !has_x11 {
|
||||||
|
tracing::warn!("No display server detected, disabling hardware acceleration");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for NVIDIA proprietary driver (known to have issues)
|
||||||
|
if let Ok(driver_info) = std::fs::read_to_string("/proc/driver/nvidia/version") {
|
||||||
|
if driver_info.contains("NVIDIA") {
|
||||||
|
tracing::warn!("NVIDIA proprietary driver detected, hardware acceleration may be unstable");
|
||||||
|
// Still return true but with warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for basic GPU capabilities
|
||||||
|
if has_wayland {
|
||||||
|
tracing::info!("Wayland detected, enabling hardware acceleration");
|
||||||
|
true
|
||||||
|
} else if has_x11 {
|
||||||
|
tracing::info!("X11 detected, enabling hardware acceleration");
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Windows generally has good D3D11 support
|
||||||
|
tracing::info!("Windows detected, enabling hardware acceleration");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// macOS has good Metal/IOSurface support
|
||||||
|
tracing::info!("macOS detected, enabling hardware acceleration");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
tracing::warn!("Unsupported platform for hardware acceleration");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,7 @@ use cef::{
|
|||||||
};
|
};
|
||||||
use include_dir::{Dir, include_dir};
|
use include_dir::{Dir, include_dir};
|
||||||
|
|
||||||
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
|
use super::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||||
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
|
|
||||||
|
|
||||||
pub(crate) struct GraphiteSchemeHandlerFactory {
|
pub(crate) struct GraphiteSchemeHandlerFactory {
|
||||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//! Common utilities and traits for texture import across platforms
|
||||||
|
|
||||||
|
use crate::cef::texture_import::*;
|
||||||
|
use ash::vk;
|
||||||
|
use cef::sys::cef_color_type_t;
|
||||||
|
use wgpu::Device;
|
||||||
|
|
||||||
|
/// Common format conversion utilities
|
||||||
|
pub mod format {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Convert CEF color type to wgpu texture format
|
||||||
|
pub fn cef_to_wgpu(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
|
||||||
|
match format {
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8UnormSrgb),
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8UnormSrgb),
|
||||||
|
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
/// Convert CEF color type to Vulkan format
|
||||||
|
pub fn cef_to_vulkan(format: cef_color_type_t) -> Result<vk::Format, TextureImportError> {
|
||||||
|
match format {
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM),
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM),
|
||||||
|
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common texture creation utilities
|
||||||
|
pub mod texture {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Create a fallback CPU texture with the given dimensions and format
|
||||||
|
pub fn create_fallback(device: &Device, width: u32, height: u32, format: cef_color_type_t, label: &str) -> TextureImportResult {
|
||||||
|
let wgpu_format = format::cef_to_wgpu(format)?;
|
||||||
|
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu_format,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::warn!(
|
||||||
|
"Using fallback CPU texture for CEF rendering ({}x{}, {:?}) - hardware acceleration failed or unavailable. Consider checking GPU driver support.",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
format
|
||||||
|
);
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common Vulkan utilities
|
||||||
|
pub mod vulkan {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Find a suitable memory type index for Vulkan allocation
|
||||||
|
pub fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option<u32> {
|
||||||
|
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the wgpu device is using Vulkan backend
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn is_vulkan_backend(device: &Device) -> bool {
|
||||||
|
use wgpu::hal::api;
|
||||||
|
let mut is_vulkan = false;
|
||||||
|
unsafe {
|
||||||
|
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||||
|
is_vulkan = device.is_some();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
is_vulkan
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the wgpu device is using D3D12 backend
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn is_d3d12_backend(device: &Device) -> bool {
|
||||||
|
use wgpu::hal::api;
|
||||||
|
let mut is_d3d12 = false;
|
||||||
|
unsafe {
|
||||||
|
device.as_hal::<api::Dx12, _, _>(|device| {
|
||||||
|
is_d3d12 = device.is_some();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
is_d3d12
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
//! Windows D3D11 shared texture import implementation
|
||||||
|
|
||||||
|
use super::common::{format, texture, vulkan};
|
||||||
|
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||||
|
use ash::vk;
|
||||||
|
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
use wgpu::hal::api;
|
||||||
|
|
||||||
|
pub struct D3D11Importer {
|
||||||
|
pub handle: *mut c_void,
|
||||||
|
pub format: cef_color_type_t,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureImporter for D3D11Importer {
|
||||||
|
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
handle: info.shared_texture_handle,
|
||||||
|
format: *info.format.as_ref(),
|
||||||
|
width: info.extra.coded_size.width as u32,
|
||||||
|
height: info.extra.coded_size.height as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Try hardware acceleration first
|
||||||
|
if self.supports_hardware_acceleration(device) {
|
||||||
|
// Try D3D12 first (most efficient on Windows)
|
||||||
|
if vulkan::is_d3d12_backend(device) {
|
||||||
|
match self.import_via_d3d12(device) {
|
||||||
|
Ok(texture) => {
|
||||||
|
tracing::info!("Successfully imported D3D11 shared texture via D3D12");
|
||||||
|
return Ok(texture);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to import D3D11 via D3D12: {}, trying Vulkan fallback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Vulkan as fallback
|
||||||
|
if vulkan::is_vulkan_backend(device) {
|
||||||
|
match self.import_via_vulkan(device) {
|
||||||
|
Ok(texture) => {
|
||||||
|
tracing::info!("Successfully imported D3D11 shared texture via Vulkan");
|
||||||
|
return Ok(texture);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to import D3D11 via Vulkan: {}, falling back to CPU texture", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to CPU texture
|
||||||
|
texture::create_fallback(device, self.width, self.height, self.format, "CEF D3D11 Texture (fallback)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||||
|
// Check if handle is valid
|
||||||
|
if self.handle.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if wgpu is using D3D12 or Vulkan backend
|
||||||
|
vulkan::is_d3d12_backend(device) || vulkan::is_vulkan_backend(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl D3D11Importer {
|
||||||
|
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Get wgpu's D3D12 device
|
||||||
|
use wgpu::hal::api;
|
||||||
|
let hal_texture = unsafe {
|
||||||
|
device.as_hal::<api::Dx12, _, _>(|device| {
|
||||||
|
let Some(device) = device else {
|
||||||
|
return Err(TextureImportError::HardwareUnavailable {
|
||||||
|
reason: "Device is not using D3D12 backend".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import D3D11 shared handle directly into D3D12 resource
|
||||||
|
let d3d12_resource = self.import_d3d11_handle_to_d3d12(device)?;
|
||||||
|
|
||||||
|
// Wrap D3D12 resource in wgpu-hal texture
|
||||||
|
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
|
||||||
|
d3d12_resource,
|
||||||
|
format::cef_to_wgpu(self.format)?,
|
||||||
|
wgpu::TextureDimension::D2,
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
1, // mip_level_count
|
||||||
|
1, // sample_count
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(hal_texture)
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Import hal texture into wgpu
|
||||||
|
let texture = unsafe {
|
||||||
|
device.create_texture_from_hal::<api::Dx12>(
|
||||||
|
hal_texture,
|
||||||
|
&wgpu::TextureDescriptor {
|
||||||
|
label: Some("CEF D3D11→D3D12 Shared Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Get wgpu's Vulkan instance and device
|
||||||
|
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||||
|
let hal_texture = unsafe {
|
||||||
|
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||||
|
let Some(device) = device else {
|
||||||
|
return Err(TextureImportError::HardwareUnavailable {
|
||||||
|
reason: "Device is not using Vulkan backend".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import D3D11 shared handle into Vulkan
|
||||||
|
let vk_image = self.import_d3d11_handle_to_vulkan(device)?;
|
||||||
|
|
||||||
|
// Wrap VkImage in wgpu-hal texture
|
||||||
|
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||||
|
vk_image,
|
||||||
|
&wgpu::hal::TextureDescriptor {
|
||||||
|
label: Some("CEF D3D11 Shared Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
|
||||||
|
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||||
|
view_formats: vec![],
|
||||||
|
},
|
||||||
|
None, // drop_callback
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(hal_texture)
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Import hal texture into wgpu
|
||||||
|
let texture = unsafe {
|
||||||
|
device.create_texture_from_hal::<Vulkan>(
|
||||||
|
hal_texture,
|
||||||
|
&wgpu::TextureDescriptor {
|
||||||
|
label: Some("CEF D3D11 Shared Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_d3d11_handle_to_vulkan(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
|
||||||
|
// Get raw Vulkan handles
|
||||||
|
let device = hal_device.raw_device();
|
||||||
|
let _instance = hal_device.shared_instance().raw_instance();
|
||||||
|
|
||||||
|
// Validate dimensions
|
||||||
|
if self.width == 0 || self.height == 0 {
|
||||||
|
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create external memory image info
|
||||||
|
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE);
|
||||||
|
|
||||||
|
// Create image create info
|
||||||
|
let image_create_info = vk::ImageCreateInfo::default()
|
||||||
|
.image_type(vk::ImageType::TYPE_2D)
|
||||||
|
.format(format::cef_to_vulkan(self.format)?)
|
||||||
|
.extent(vk::Extent3D {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth: 1,
|
||||||
|
})
|
||||||
|
.mip_levels(1)
|
||||||
|
.array_layers(1)
|
||||||
|
.samples(vk::SampleCountFlags::TYPE_1)
|
||||||
|
.tiling(vk::ImageTiling::OPTIMAL)
|
||||||
|
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||||
|
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||||
|
.push_next(&mut external_memory_info);
|
||||||
|
|
||||||
|
// Create the image
|
||||||
|
let image = unsafe {
|
||||||
|
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to create Vulkan image: {:?}", e),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get memory requirements
|
||||||
|
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||||
|
|
||||||
|
// Import D3D11 handle
|
||||||
|
let mut import_memory_win32 = vk::ImportMemoryWin32HandleInfoKHR::default()
|
||||||
|
.handle_type(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE)
|
||||||
|
.handle(self.handle as isize);
|
||||||
|
|
||||||
|
// Find a suitable memory type
|
||||||
|
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||||
|
|
||||||
|
let memory_type_index =
|
||||||
|
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| TextureImportError::VulkanError {
|
||||||
|
operation: "Failed to find suitable memory type for D3D11 texture".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||||
|
.allocation_size(memory_requirements.size)
|
||||||
|
.memory_type_index(memory_type_index)
|
||||||
|
.push_next(&mut import_memory_win32);
|
||||||
|
|
||||||
|
let device_memory = unsafe {
|
||||||
|
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to allocate memory for D3D11 texture: {:?}", e),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind memory to image
|
||||||
|
unsafe {
|
||||||
|
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to bind memory to image: {:?}", e),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_d3d11_handle_to_d3d12(&self, hal_device: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||||
|
use windows::Win32::Graphics::Direct3D12::*;
|
||||||
|
use windows::core::*;
|
||||||
|
|
||||||
|
// Get D3D12 device from wgpu-hal
|
||||||
|
let d3d12_device = hal_device.raw_device();
|
||||||
|
|
||||||
|
// Validate dimensions
|
||||||
|
if self.width == 0 || self.height == 0 {
|
||||||
|
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open D3D11 shared handle on D3D12 device
|
||||||
|
unsafe {
|
||||||
|
let mut shared_resource: Option<ID3D12Resource> = None;
|
||||||
|
d3d12_device
|
||||||
|
.OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource)
|
||||||
|
.map_err(|e| TextureImportError::PlatformError {
|
||||||
|
message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
//! Linux DMA-BUF texture import implementation
|
||||||
|
|
||||||
|
use super::common::{format, texture, vulkan};
|
||||||
|
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||||
|
use ash::vk;
|
||||||
|
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||||
|
use wgpu::hal::api;
|
||||||
|
|
||||||
|
pub(crate) struct DmaBufImporter {
|
||||||
|
fds: Vec<std::os::fd::RawFd>,
|
||||||
|
format: cef_color_type_t,
|
||||||
|
modifier: u64,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
strides: Vec<u32>,
|
||||||
|
offsets: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureImporter for DmaBufImporter {
|
||||||
|
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
fds: extract_fds_from_info(info),
|
||||||
|
format: *info.format.as_ref(),
|
||||||
|
modifier: info.modifier,
|
||||||
|
width: info.extra.coded_size.width as u32,
|
||||||
|
height: info.extra.coded_size.height as u32,
|
||||||
|
strides: extract_strides_from_info(info),
|
||||||
|
offsets: extract_offsets_from_info(info),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Try hardware acceleration first
|
||||||
|
if self.supports_hardware_acceleration(device) {
|
||||||
|
match self.import_via_vulkan(device) {
|
||||||
|
Ok(texture) => {
|
||||||
|
tracing::info!("Successfully imported DMA-BUF texture via Vulkan");
|
||||||
|
return Ok(texture);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to import DMA-BUF via Vulkan: {}, falling back to CPU texture", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to CPU texture
|
||||||
|
texture::create_fallback(device, self.width, self.height, self.format, "CEF DMA-BUF Texture (fallback)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||||
|
// Check if we have valid file descriptors
|
||||||
|
if self.fds.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for &fd in &self.fds {
|
||||||
|
if fd < 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if file descriptor is valid
|
||||||
|
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
||||||
|
if flags == -1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if wgpu is using Vulkan backend
|
||||||
|
vulkan::is_vulkan_backend(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DmaBufImporter {
|
||||||
|
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Get wgpu's Vulkan instance and device
|
||||||
|
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||||
|
let hal_texture = unsafe {
|
||||||
|
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||||
|
let Some(device) = device else {
|
||||||
|
return Err(TextureImportError::HardwareUnavailable {
|
||||||
|
reason: "Device is not using Vulkan backend".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create VkImage from DMA-BUF using external memory
|
||||||
|
let vk_image = self.create_vulkan_image_from_dmabuf(device)?;
|
||||||
|
|
||||||
|
// Wrap VkImage in wgpu-hal texture
|
||||||
|
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||||
|
vk_image,
|
||||||
|
&wgpu::hal::TextureDescriptor {
|
||||||
|
label: Some("CEF DMA-BUF Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
|
||||||
|
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||||
|
view_formats: vec![],
|
||||||
|
},
|
||||||
|
None, // drop_callback
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(hal_texture)
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Import hal texture into wgpu
|
||||||
|
let texture = unsafe {
|
||||||
|
device.create_texture_from_hal::<Vulkan>(
|
||||||
|
hal_texture,
|
||||||
|
&wgpu::TextureDescriptor {
|
||||||
|
label: Some("CEF DMA-BUF Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
|
||||||
|
// Get raw Vulkan handles
|
||||||
|
let device = hal_device.raw_device();
|
||||||
|
let _instance = hal_device.shared_instance().raw_instance();
|
||||||
|
|
||||||
|
// Validate dimensions
|
||||||
|
if self.width == 0 || self.height == 0 {
|
||||||
|
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create external memory image
|
||||||
|
let image_create_info = vk::ImageCreateInfo::default()
|
||||||
|
.image_type(vk::ImageType::TYPE_2D)
|
||||||
|
.format(format::cef_to_vulkan(self.format)?)
|
||||||
|
.extent(vk::Extent3D {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth: 1,
|
||||||
|
})
|
||||||
|
.mip_levels(1)
|
||||||
|
.array_layers(1)
|
||||||
|
.samples(vk::SampleCountFlags::TYPE_1)
|
||||||
|
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||||
|
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||||
|
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||||
|
|
||||||
|
// Set up DRM format modifier
|
||||||
|
let plane_layouts = self.create_subresource_layouts()?;
|
||||||
|
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||||
|
.drm_format_modifier(self.modifier)
|
||||||
|
.plane_layouts(&plane_layouts);
|
||||||
|
|
||||||
|
let image_create_info = image_create_info.push_next(&mut drm_format_modifier);
|
||||||
|
|
||||||
|
// Create the image
|
||||||
|
let image = unsafe {
|
||||||
|
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to create Vulkan image: {e:?}"),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import memory from DMA-BUF
|
||||||
|
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||||
|
|
||||||
|
// Duplicate the file descriptor to avoid ownership issues
|
||||||
|
let dup_fd = unsafe { libc::dup(self.fds[0]) };
|
||||||
|
if dup_fd == -1 {
|
||||||
|
return Err(TextureImportError::PlatformError {
|
||||||
|
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
|
||||||
|
|
||||||
|
// Find a suitable memory type
|
||||||
|
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||||
|
|
||||||
|
let memory_type_index =
|
||||||
|
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| TextureImportError::VulkanError {
|
||||||
|
operation: "Failed to find suitable memory type for DMA-BUF".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||||
|
.allocation_size(memory_requirements.size)
|
||||||
|
.memory_type_index(memory_type_index)
|
||||||
|
.push_next(&mut import_memory_fd);
|
||||||
|
|
||||||
|
let device_memory = unsafe {
|
||||||
|
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind memory to image
|
||||||
|
unsafe {
|
||||||
|
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
|
||||||
|
operation: format!("Failed to bind memory to image: {e:?}"),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_subresource_layouts(&self) -> Result<Vec<vk::SubresourceLayout>, TextureImportError> {
|
||||||
|
let mut layouts = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..self.fds.len() {
|
||||||
|
layouts.push(vk::SubresourceLayout {
|
||||||
|
offset: self.offsets.get(i).copied().unwrap_or(0) as u64,
|
||||||
|
size: 0, // Will be calculated by driver
|
||||||
|
row_pitch: self.strides.get(i).copied().unwrap_or(0) as u64,
|
||||||
|
array_pitch: 0,
|
||||||
|
depth_pitch: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(layouts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_fds_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<std::os::fd::RawFd> {
|
||||||
|
let plane_count = info.plane_count as usize;
|
||||||
|
let mut fds = Vec::with_capacity(plane_count);
|
||||||
|
|
||||||
|
for i in 0..plane_count {
|
||||||
|
if let Some(plane) = info.planes.get(i) {
|
||||||
|
fds.push(plane.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_strides_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
|
||||||
|
let plane_count = info.plane_count as usize;
|
||||||
|
let mut strides = Vec::with_capacity(plane_count);
|
||||||
|
|
||||||
|
for i in 0..plane_count {
|
||||||
|
if let Some(plane) = info.planes.get(i) {
|
||||||
|
strides.push(plane.stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strides
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_offsets_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
|
||||||
|
let plane_count = info.plane_count as usize;
|
||||||
|
let mut offsets = Vec::with_capacity(plane_count);
|
||||||
|
|
||||||
|
for i in 0..plane_count {
|
||||||
|
if let Some(plane) = info.planes.get(i) {
|
||||||
|
offsets.push(plane.offset as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offsets
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
//! macOS IOSurface texture import implementation
|
||||||
|
|
||||||
|
use super::common::{format, texture};
|
||||||
|
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||||
|
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||||
|
use core_foundation::base::{CFType, TCFType};
|
||||||
|
use objc2_io_surface::{IOSurface, IOSurfaceRef};
|
||||||
|
use objc2_metal::{MTLDevice, MTLPixelFormat, MTLTexture, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage};
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
use wgpu::hal::api;
|
||||||
|
|
||||||
|
pub struct IOSurfaceImporter {
|
||||||
|
pub handle: *mut c_void,
|
||||||
|
pub format: cef_color_type_t,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureImporter for IOSurfaceImporter {
|
||||||
|
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
handle: info.shared_texture_handle,
|
||||||
|
format: *info.format.as_ref(),
|
||||||
|
width: info.extra.coded_size.width as u32,
|
||||||
|
height: info.extra.coded_size.height as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Try hardware acceleration first
|
||||||
|
if self.supports_hardware_acceleration(device) {
|
||||||
|
match self.import_via_metal(device) {
|
||||||
|
Ok(texture) => {
|
||||||
|
tracing::trace!("Successfully imported IOSurface texture via Metal");
|
||||||
|
return Ok(texture);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to import IOSurface via Metal: {}, falling back to CPU texture", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to CPU texture
|
||||||
|
texture::create_fallback(device, self.width, self.height, self.format, "CEF IOSurface Texture (fallback)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||||
|
// Check if handle is valid
|
||||||
|
if self.handle.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if wgpu is using Metal backend
|
||||||
|
self.is_metal_backend(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IOSurfaceImporter {
|
||||||
|
fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
// Get wgpu's Metal device
|
||||||
|
use wgpu::{hal::Api, wgc::api::Metal};
|
||||||
|
let hal_texture = unsafe {
|
||||||
|
device.as_hal::<api::Metal, _, _>(|device| {
|
||||||
|
let Some(device) = device else {
|
||||||
|
return Err(TextureImportError::HardwareUnavailable {
|
||||||
|
reason: "Device is not using Metal backend".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import IOSurface handle into Metal texture
|
||||||
|
let metal_texture = self.import_iosurface_to_metal(device)?;
|
||||||
|
|
||||||
|
// Wrap Metal texture in wgpu-hal texture
|
||||||
|
let hal_texture = <api::Metal as wgpu::hal::Api>::Device::texture_from_raw(
|
||||||
|
metal_texture,
|
||||||
|
&wgpu::hal::TextureDescriptor {
|
||||||
|
label: Some("CEF IOSurface Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: wgpu::hal::TextureUses::RESOURCE,
|
||||||
|
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||||
|
view_formats: vec![],
|
||||||
|
},
|
||||||
|
None, // drop_callback
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(hal_texture)
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Import hal texture into wgpu
|
||||||
|
let texture = unsafe {
|
||||||
|
device.create_texture_from_hal::<Metal>(
|
||||||
|
hal_texture,
|
||||||
|
&wgpu::TextureDescriptor {
|
||||||
|
label: Some("CEF IOSurface Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: format::cef_to_wgpu(self.format)?,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_iosurface_to_metal(&self, hal_device: &<api::Metal as wgpu::hal::Api>::Device) -> Result<<api::Metal as wgpu::hal::Api>::Texture, TextureImportError> {
|
||||||
|
// Validate dimensions
|
||||||
|
if self.width == 0 || self.height == 0 {
|
||||||
|
return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert handle to IOSurface
|
||||||
|
let iosurface = unsafe {
|
||||||
|
let cf_type = CFType::wrap_under_get_rule(self.handle as IOSurfaceRef);
|
||||||
|
IOSurface::from(cf_type)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the Metal device from wgpu-hal
|
||||||
|
let metal_device = hal_device.raw_device();
|
||||||
|
|
||||||
|
// Convert CEF format to Metal pixel format
|
||||||
|
let metal_format = self.cef_to_metal_format(self.format)?;
|
||||||
|
|
||||||
|
// Create Metal texture descriptor
|
||||||
|
let texture_descriptor = MTLTextureDescriptor::new();
|
||||||
|
texture_descriptor.setTextureType(MTLTextureType::Type2D);
|
||||||
|
texture_descriptor.setPixelFormat(metal_format);
|
||||||
|
texture_descriptor.setWidth(self.width as usize);
|
||||||
|
texture_descriptor.setHeight(self.height as usize);
|
||||||
|
texture_descriptor.setDepth(1);
|
||||||
|
texture_descriptor.setMipmapLevelCount(1);
|
||||||
|
texture_descriptor.setSampleCount(1);
|
||||||
|
texture_descriptor.setUsage(MTLTextureUsage::ShaderRead);
|
||||||
|
|
||||||
|
// Create Metal texture from IOSurface
|
||||||
|
let metal_texture = unsafe { metal_device.newTextureWithDescriptor_iosurface_plane(&texture_descriptor, &iosurface, 0) };
|
||||||
|
|
||||||
|
let Some(metal_texture) = metal_texture else {
|
||||||
|
return Err(TextureImportError::PlatformError {
|
||||||
|
message: "Failed to create Metal texture from IOSurface".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::trace!("Successfully created Metal texture from IOSurface");
|
||||||
|
Ok(metal_texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cef_to_metal_format(&self, format: cef_color_type_t) -> Result<MTLPixelFormat, TextureImportError> {
|
||||||
|
match format {
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(MTLPixelFormat::BGRA8Unorm_sRGB),
|
||||||
|
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(MTLPixelFormat::RGBA8Unorm_sRGB),
|
||||||
|
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_metal_backend(&self, device: &wgpu::Device) -> bool {
|
||||||
|
use wgpu::hal::api;
|
||||||
|
let mut is_metal = false;
|
||||||
|
unsafe {
|
||||||
|
device.as_hal::<api::Metal, _, _>(|device| {
|
||||||
|
is_metal = device.is_some();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
is_metal
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! Unified texture import system for CEF hardware acceleration
|
||||||
|
//!
|
||||||
|
//! This module provides a platform-agnostic interface for importing shared textures
|
||||||
|
//! from CEF into wgpu, with automatic fallback to CPU textures when hardware
|
||||||
|
//! acceleration is not available.
|
||||||
|
//!
|
||||||
|
//! # Supported Platforms
|
||||||
|
//!
|
||||||
|
//! - **Linux**: DMA-BUF via Vulkan external memory
|
||||||
|
//! - **Windows**: D3D11 shared textures via Vulkan interop
|
||||||
|
//! - **macOS**: IOSurface via Metal native API
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! // Import texture with automatic platform detection
|
||||||
|
//! let texture = shared_handle.import_texture(&device)?;
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Features
|
||||||
|
//!
|
||||||
|
//! - `accelerated_paint` - Base feature for texture import
|
||||||
|
//! - `accelerated_paint_dmabuf` - Linux DMA-BUF support
|
||||||
|
//! - `accelerated_paint_d3d11` - Windows D3D11 support
|
||||||
|
//! - `accelerated_paint_iosurface` - macOS IOSurface support
|
||||||
|
|
||||||
|
pub(crate) mod common;
|
||||||
|
|
||||||
|
pub(crate) mod shared_texture_handle;
|
||||||
|
pub(crate) use shared_texture_handle::SharedTextureHandle;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) mod dmabuf;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub(crate) mod d3d11;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) mod iosurface;
|
||||||
|
|
||||||
|
/// Result type for texture import operations
|
||||||
|
pub type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
|
||||||
|
|
||||||
|
/// Errors that can occur during texture import
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum TextureImportError {
|
||||||
|
#[error("Invalid texture handle: {0}")]
|
||||||
|
InvalidHandle(String),
|
||||||
|
|
||||||
|
#[error("Unsupported texture format: {format:?}")]
|
||||||
|
UnsupportedFormat { format: cef::sys::cef_color_type_t },
|
||||||
|
|
||||||
|
#[error("Hardware acceleration not available: {reason}")]
|
||||||
|
HardwareUnavailable { reason: String },
|
||||||
|
|
||||||
|
#[error("Vulkan operation failed: {operation}")]
|
||||||
|
VulkanError { operation: String },
|
||||||
|
|
||||||
|
#[error("Platform-specific error: {message}")]
|
||||||
|
PlatformError { message: String },
|
||||||
|
|
||||||
|
#[error("Unsupported platform for texture import")]
|
||||||
|
UnsupportedPlatform,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for platform-specific texture importers
|
||||||
|
pub trait TextureImporter {
|
||||||
|
fn new(info: &cef::AcceleratedPaintInfo) -> Self;
|
||||||
|
|
||||||
|
/// Import the texture into wgpu, with automatic fallback to CPU texture
|
||||||
|
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
|
||||||
|
|
||||||
|
/// Check if hardware acceleration is available for this texture
|
||||||
|
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use cef::AcceleratedPaintInfo;
|
||||||
|
|
||||||
|
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||||
|
|
||||||
|
pub(crate) enum SharedTextureHandle {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
DmaBuf(super::dmabuf::DmaBufImporter),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
D3D11(super::d3d11::D3D11Importer),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
IOSurface(super::iosurface::IOSurfaceImporter),
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedTextureHandle {
|
||||||
|
pub(crate) fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||||
|
// Extract DMA-BUF information
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
return Self::DmaBuf(super::dmabuf::DmaBufImporter::new(info));
|
||||||
|
|
||||||
|
// Extract D3D11 shared handle with texture metadata
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return Self::D3D11(super::d3d11::D3D11Importer::new(info));
|
||||||
|
|
||||||
|
// Extract IOSurface handle with texture metadata
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
return Self::IOSurface(super::iosurface::IOSurfaceImporter::new(info));
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Self::Unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a texture using the appropriate platform-specific importer
|
||||||
|
pub(crate) fn import_texture(self, device: &wgpu::Device) -> TextureImportResult {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
SharedTextureHandle::DmaBuf(importer) => importer.import_to_wgpu(device),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
SharedTextureHandle::D3D11(importer) => importer.import_to_wgpu(device),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
SharedTextureHandle::IOSurface(importer) => importer.import_to_wgpu(device),
|
||||||
|
SharedTextureHandle::Unsupported => Err(TextureImportError::UnsupportedPlatform),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
pub(crate) static APP_NAME: &str = "Graphite";
|
pub(crate) static APP_NAME: &str = "Graphite";
|
||||||
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
|
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
|
||||||
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
|
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
|
||||||
|
|
||||||
|
// CEF configuration constants
|
||||||
|
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;
|
||||||
|
pub(crate) const CEF_MESSAGE_LOOP_MAX_ITERATIONS: usize = 10;
|
||||||
|
|||||||
+22
-30
@@ -1,12 +1,12 @@
|
|||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Instant;
|
||||||
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use winit::event_loop::EventLoop;
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
pub(crate) mod consts;
|
pub(crate) mod consts;
|
||||||
|
|
||||||
mod cef;
|
mod cef;
|
||||||
use cef::Setup;
|
|
||||||
|
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ use app::WinitApp;
|
|||||||
mod dirs;
|
mod dirs;
|
||||||
|
|
||||||
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
|
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
|
||||||
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext};
|
use graphite_desktop_wrapper::{NodeGraphExecutionResult, WgpuContext};
|
||||||
|
|
||||||
pub(crate) enum CustomEvent {
|
pub(crate) enum CustomEvent {
|
||||||
UiUpdate(wgpu::Texture),
|
UiUpdate(wgpu::Texture),
|
||||||
@@ -28,21 +28,24 @@ pub(crate) enum CustomEvent {
|
|||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||||
|
|
||||||
let cef_context = match cef::Context::<Setup>::new() {
|
let cef_context_builder = cef::CefContextBuilder::new();
|
||||||
Ok(c) => c,
|
|
||||||
Err(cef::SetupError::Subprocess) => exit(0),
|
if cef_context_builder.is_sub_process() {
|
||||||
Err(cef::SetupError::SubprocessFailed(t)) => {
|
// We are in a CEF subprocess
|
||||||
tracing::error!("Subprocess of type {t} failed");
|
// This will block until the CEF subprocess quits
|
||||||
exit(1);
|
let error = cef_context_builder.execute_sub_process();
|
||||||
}
|
tracing::error!("Cef subprocess failed with error: {error}");
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
|
||||||
|
|
||||||
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
|
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
|
||||||
|
|
||||||
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
|
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
|
let cef_handler = cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone());
|
||||||
let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) {
|
let cef_context = match cef_context_builder.initialize(cef_handler) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(cef::InitError::AlreadyRunning) => {
|
Err(cef::InitError::AlreadyRunning) => {
|
||||||
tracing::error!("Another instance is already running, Exiting.");
|
tracing::error!("Another instance is already running, Exiting.");
|
||||||
@@ -52,26 +55,15 @@ fn main() {
|
|||||||
tracing::error!("Cef initialization failed with code: {code}");
|
tracing::error!("Cef initialization failed with code: {code}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
Err(cef::InitError::BrowserCreationFailed) => {
|
||||||
|
tracing::error!("Failed to create CEF browser");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Cef initialized successfully");
|
tracing::info!("CEF initialized successfully");
|
||||||
|
|
||||||
let rendering_loop_proxy = event_loop.create_proxy();
|
let mut winit_app = WinitApp::new(Box::new(cef_context), window_size_sender, wgpu_context, event_loop.create_proxy());
|
||||||
let target_fps = 60;
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
let last_render = Instant::now();
|
|
||||||
|
|
||||||
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
|
||||||
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
|
|
||||||
|
|
||||||
let frame_time = Duration::from_secs_f32((target_fps as f32).recip());
|
|
||||||
let sleep = last_render + frame_time - Instant::now();
|
|
||||||
std::thread::sleep(sleep);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut winit_app = WinitApp::new(cef_context, window_size_sender, wgpu_context, event_loop.create_proxy());
|
|
||||||
|
|
||||||
event_loop.run_app(&mut winit_app).unwrap();
|
event_loop.run_app(&mut winit_app).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ impl GraphicsState {
|
|||||||
self.bind_overlays_texture(texture);
|
self.bind_overlays_texture(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
pub(crate) fn render(&mut self, window: &Window) -> Result<(), wgpu::SurfaceError> {
|
||||||
if let Some(scene) = self.overlays_scene.take() {
|
if let Some(scene) = self.overlays_scene.take() {
|
||||||
self.render_overlays(scene);
|
self.render_overlays(scene);
|
||||||
}
|
}
|
||||||
@@ -275,6 +275,7 @@ impl GraphicsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.context.queue.submit(std::iter::once(encoder.finish()));
|
self.context.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
window.pre_present_notify();
|
||||||
output.present();
|
output.present();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ pub enum DialogMessage {
|
|||||||
RequestLicensesDialogWithLocalizedCommitDate {
|
RequestLicensesDialogWithLocalizedCommitDate {
|
||||||
localized_commit_year: String,
|
localized_commit_year: String,
|
||||||
},
|
},
|
||||||
|
RequestLicensesThirdPartyDialogWithLicenseText {
|
||||||
|
license_text: String,
|
||||||
|
},
|
||||||
RequestNewDocumentDialog,
|
RequestNewDocumentDialog,
|
||||||
RequestPreferencesDialog,
|
RequestPreferencesDialog,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::new_document_dialog::NewDocumentDialogMessageContext;
|
use super::new_document_dialog::NewDocumentDialogMessageContext;
|
||||||
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
|
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
|
||||||
|
use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog;
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
|
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
@@ -103,6 +104,10 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
|||||||
|
|
||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
|
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
||||||
|
let dialog = LicensesThirdPartyDialog { license_text };
|
||||||
|
dialog.send_dialog_to_frontend(responses);
|
||||||
|
}
|
||||||
DialogMessage::RequestNewDocumentDialog => {
|
DialogMessage::RequestNewDocumentDialog => {
|
||||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||||
name: portfolio.generate_new_document_name(),
|
name: portfolio.generate_new_document_name(),
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let export_type = vec![
|
let export_type = vec![
|
||||||
TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(),
|
TextLabel::new("File Type").table_align(true).min_width("100px").widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
|
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let resolution = vec![
|
let resolution = vec![
|
||||||
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(),
|
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
NumberInput::new(Some(self.scale_factor))
|
NumberInput::new(Some(self.scale_factor))
|
||||||
.unit("")
|
.unit("")
|
||||||
@@ -144,14 +144,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let export_area = vec![
|
let export_area = vec![
|
||||||
TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(),
|
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let checkbox_id = CheckboxId::new();
|
let checkbox_id = CheckboxId::new();
|
||||||
let transparent_background = vec![
|
let transparent_background = vec![
|
||||||
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_holder(),
|
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
CheckboxInput::new(self.transparent_background)
|
CheckboxInput::new(self.transparent_background)
|
||||||
.disabled(self.file_type == FileType::Jpg)
|
.disabled(self.file_type == FileType::Jpg)
|
||||||
|
|||||||
+3
-3
@@ -79,7 +79,7 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let name = vec![
|
let name = vec![
|
||||||
TextLabel::new("Name").table_align(true).min_width(90).widget_holder(),
|
TextLabel::new("Name").table_align(true).min_width("90px").widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
TextInput::new(&self.name)
|
TextInput::new(&self.name)
|
||||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
||||||
@@ -89,7 +89,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
|
|
||||||
let checkbox_id = CheckboxId::new();
|
let checkbox_id = CheckboxId::new();
|
||||||
let infinite = vec![
|
let infinite = vec![
|
||||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_holder(),
|
TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
CheckboxInput::new(self.infinite)
|
CheckboxInput::new(self.infinite)
|
||||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
||||||
@@ -98,7 +98,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let scale = vec![
|
let scale = vec![
|
||||||
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_holder(),
|
TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_holder(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
NumberInput::new(Some(self.dimensions.x as f64))
|
NumberInput::new(Some(self.dimensions.x as f64))
|
||||||
.label("W")
|
.label("W")
|
||||||
|
|||||||
@@ -16,22 +16,31 @@ impl DialogLayoutHolder for LicensesDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn layout_column_2(&self) -> Layout {
|
fn layout_column_2(&self) -> Layout {
|
||||||
let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md";
|
#[allow(clippy::type_complexity)]
|
||||||
let links = [
|
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
||||||
("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"),
|
("GraphiteLogo", "Graphite Logo", || {
|
||||||
("IconsGrid", "Graphite Icons", icons_license_link),
|
FrontendMessage::TriggerVisitLink {
|
||||||
("License", "Graphite License", "https://graphite.rs/license/"),
|
url: "https://graphite.rs/logo/".into(),
|
||||||
("License", "Other Licenses", "/third-party-licenses.txt"),
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
("IconsGrid", "Graphite Icons", || {
|
||||||
|
FrontendMessage::TriggerVisitLink {
|
||||||
|
url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
("License", "Graphite License", || {
|
||||||
|
FrontendMessage::TriggerVisitLink {
|
||||||
|
url: "https://graphite.rs/license/".into(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
||||||
];
|
];
|
||||||
let widgets = links
|
let widgets = button_definitions
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|(icon, label, url)| {
|
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_holder())
|
||||||
TextButton::new(label)
|
|
||||||
.icon(Some(icon.into()))
|
|
||||||
.flush(true)
|
|
||||||
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
|
|
||||||
.widget_holder()
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
|
pub struct LicensesThirdPartyDialog {
|
||||||
|
pub license_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DialogLayoutHolder for LicensesThirdPartyDialog {
|
||||||
|
const ICON: &'static str = "License12px";
|
||||||
|
const TITLE: &'static str = "Third-Party Software License Notices";
|
||||||
|
|
||||||
|
fn layout_buttons(&self) -> Layout {
|
||||||
|
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||||
|
|
||||||
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutHolder for LicensesThirdPartyDialog {
|
||||||
|
fn layout(&self) -> Layout {
|
||||||
|
// Remove the header and begin with the line containing the first license section (we otherwise keep the title for standalone viewing of the licenses text file)
|
||||||
|
let license_text = if let Some(first_underscore_line) = self.license_text.lines().position(|line| line.contains('_')) {
|
||||||
|
// Find the byte position where the line with underscore starts
|
||||||
|
let char_position = self.license_text.split('\n').take(first_underscore_line).map(|line| line.len() + '\n'.len_utf8()).sum();
|
||||||
|
self.license_text[char_position..].to_string()
|
||||||
|
} else {
|
||||||
|
// This shouldn't be encountered, but if no underscore line is found, we use the full text as a safety fallback
|
||||||
|
self.license_text.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar
|
||||||
|
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count()).max().unwrap_or(0) + 2 + 1;
|
||||||
|
|
||||||
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
TextLabel::new(license_text)
|
||||||
|
.monospace(true)
|
||||||
|
.multiline(true)
|
||||||
|
.min_width(format!("{non_wrapping_column_width}ch"))
|
||||||
|
.widget_holder(),
|
||||||
|
],
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ mod coming_soon_dialog;
|
|||||||
mod demo_artwork_dialog;
|
mod demo_artwork_dialog;
|
||||||
mod error_dialog;
|
mod error_dialog;
|
||||||
mod licenses_dialog;
|
mod licenses_dialog;
|
||||||
|
mod licenses_third_party_dialog;
|
||||||
|
|
||||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
||||||
@@ -14,3 +15,4 @@ pub use demo_artwork_dialog::ARTWORK;
|
|||||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||||
pub use error_dialog::ErrorDialog;
|
pub use error_dialog::ErrorDialog;
|
||||||
pub use licenses_dialog::LicensesDialog;
|
pub use licenses_dialog::LicensesDialog;
|
||||||
|
pub use licenses_third_party_dialog::LicensesThirdPartyDialog;
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "commitDate")]
|
#[serde(rename = "commitDate")]
|
||||||
commit_date: String,
|
commit_date: String,
|
||||||
},
|
},
|
||||||
|
TriggerDisplayThirdPartyLicensesDialog,
|
||||||
TriggerSaveDocument {
|
TriggerSaveDocument {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@@ -44,16 +44,18 @@ pub struct TextLabel {
|
|||||||
|
|
||||||
pub italic: bool,
|
pub italic: bool,
|
||||||
|
|
||||||
|
pub monospace: bool,
|
||||||
|
|
||||||
|
pub multiline: bool,
|
||||||
|
|
||||||
#[serde(rename = "centerAlign")]
|
#[serde(rename = "centerAlign")]
|
||||||
pub center_align: bool,
|
pub center_align: bool,
|
||||||
|
|
||||||
#[serde(rename = "tableAlign")]
|
#[serde(rename = "tableAlign")]
|
||||||
pub table_align: bool,
|
pub table_align: bool,
|
||||||
|
|
||||||
pub multiline: bool,
|
|
||||||
|
|
||||||
#[serde(rename = "minWidth")]
|
#[serde(rename = "minWidth")]
|
||||||
pub min_width: u32,
|
pub min_width: String,
|
||||||
|
|
||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
|
|
||||||
|
|||||||
@@ -1021,6 +1021,8 @@ impl OverlayContextInternal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Load Source Sans Pro font data
|
// Load Source Sans Pro font data
|
||||||
|
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||||
|
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||||
let font_blob = Some(load_font(FONT_DATA));
|
let font_blob = Some(load_font(FONT_DATA));
|
||||||
|
|
||||||
@@ -1046,6 +1048,8 @@ impl OverlayContextInternal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Load Source Sans Pro font data
|
// Load Source Sans Pro font data
|
||||||
|
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||||
|
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||||
let font_blob = Some(load_font(FONT_DATA));
|
let font_blob = Some(load_font(FONT_DATA));
|
||||||
|
|
||||||
|
|||||||
Generated
+28
-29
@@ -7,11 +7,11 @@
|
|||||||
"name": "graphite-web-frontend",
|
"name": "graphite-web-frontend",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inconsolata": "^5.2.5",
|
|
||||||
"@fontsource/source-sans-pro": "^5.2.5",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"reflect-metadata": "^0.2.2"
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"source-code-pro": "github:adobe-fonts/source-code-pro#2.042R-u/1.062R-i/1.026R-vf",
|
||||||
|
"source-sans": "github:adobe-fonts/source-sans#2.045R-ro%2F1.095R-it"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"rollup-plugin-license": "^3.5.3",
|
"rollup-plugin-license": "^3.6.0",
|
||||||
"sass": "1.78.0",
|
"sass": "1.78.0",
|
||||||
"svelte": "^4.2.19",
|
"svelte": "^4.2.19",
|
||||||
"svelte-preprocess": "^6.0.2",
|
"svelte-preprocess": "^6.0.2",
|
||||||
@@ -556,21 +556,6 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource/inconsolata": {
|
|
||||||
"version": "5.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.5.tgz",
|
|
||||||
"integrity": "sha512-OvzkZY5qYghv/jEV6cfGZzFhdFTvSnU+ExPC7WcZ7w8PdRhtiu/SpcBWOBt+3LXgS0n9qyepgq4zZmxlDTlGGQ==",
|
|
||||||
"license": "OFL-1.1",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/source-sans-pro": {
|
|
||||||
"version": "5.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.2.5.tgz",
|
|
||||||
"integrity": "sha512-ypendqc4pYUc+EgF7qqPY9iVYEz1t/Qr03VojKxG/2g3dnpHa1B6DOlDxWQjQXDj5QrG6inEqGT0g+edjALZyg==",
|
|
||||||
"license": "OFL-1.1"
|
|
||||||
},
|
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@@ -4622,14 +4607,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup-plugin-license": {
|
"node_modules/rollup-plugin-license": {
|
||||||
"version": "3.5.3",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.6.0.tgz",
|
||||||
"integrity": "sha512-r3wImZSo2d6sEk9BRJtlzeI/upjyjnpthy06Fdl0EzqRrlg3ULb9KQR7xHJI0zuayW/8bchEXSF5dO6dha4OyA==",
|
"integrity": "sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commenting": "~1.1.0",
|
"commenting": "~1.1.0",
|
||||||
"fdir": "6.3.0",
|
"fdir": "^6.4.3",
|
||||||
"lodash": "~4.17.21",
|
"lodash": "~4.17.21",
|
||||||
"magic-string": "~0.30.0",
|
"magic-string": "~0.30.0",
|
||||||
"moment": "~2.30.1",
|
"moment": "~2.30.1",
|
||||||
@@ -4645,11 +4630,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup-plugin-license/node_modules/fdir": {
|
"node_modules/rollup-plugin-license/node_modules/fdir": {
|
||||||
"version": "6.3.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
@@ -4660,9 +4648,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup-plugin-license/node_modules/picomatch": {
|
"node_modules/rollup-plugin-license/node_modules/picomatch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -4864,6 +4852,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-code-pro": {
|
||||||
|
"version": "2.38.0",
|
||||||
|
"resolved": "git+ssh://git@github.com/adobe-fonts/source-code-pro.git#d3f1a5962cde503f9409c21e58527611d4a19ef1",
|
||||||
|
"license": "OFL-1.1"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -4874,6 +4867,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-sans": {
|
||||||
|
"name": "source-sans-pro",
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "git+ssh://git@github.com/adobe-fonts/source-sans.git#ce77773581f4d454f0fa985c073bb25c721bfcf5",
|
||||||
|
"license": "SIL Open Font License 1.1"
|
||||||
|
},
|
||||||
"node_modules/spdx-compare": {
|
"node_modules/spdx-compare": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
||||||
|
|||||||
@@ -31,11 +31,11 @@
|
|||||||
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
|
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inconsolata": "^5.2.5",
|
|
||||||
"@fontsource/source-sans-pro": "^5.2.5",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"reflect-metadata": "^0.2.2"
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"source-code-pro": "github:adobe-fonts/source-code-pro#2.042R-u/1.062R-i/1.026R-vf",
|
||||||
|
"source-sans": "github:adobe-fonts/source-sans#2.045R-ro%2F1.095R-it"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"rollup-plugin-license": "^3.5.3",
|
"rollup-plugin-license": "^3.6.0",
|
||||||
"sass": "1.78.0",
|
"sass": "1.78.0",
|
||||||
"svelte": "^4.2.19",
|
"svelte": "^4.2.19",
|
||||||
"svelte-preprocess": "^6.0.2",
|
"svelte-preprocess": "^6.0.2",
|
||||||
|
|||||||
@@ -352,4 +352,44 @@
|
|||||||
:not(.optional-input) > .checkbox-input input:focus-visible + label.checked {
|
:not(.optional-input) > .checkbox-input input:focus-visible + label.checked {
|
||||||
outline: 1px dashed var(--color-2-mildblack);
|
outline: 1px dashed var(--color-2-mildblack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-stretch: normal;
|
||||||
|
src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-It.ttf.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
font-stretch: normal;
|
||||||
|
src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Code Pro";
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
src: url("@graphite/../node_modules/source-code-pro/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2") format("woff2");
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
.header-area,
|
.header-area,
|
||||||
.footer-area {
|
.footer-area {
|
||||||
background: var(--color-1-nearblack);
|
background: var(--color-1-nearblack);
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-area,
|
.header-area,
|
||||||
@@ -113,6 +114,8 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: -4px 0;
|
margin: -4px 0;
|
||||||
|
padding-right: calc(24px + 1px * var(--even-integer-subpixel-expansion-x));
|
||||||
|
padding-bottom: calc(16px + 1px * var(--even-integer-subpixel-expansion-y));
|
||||||
|
|
||||||
&.center .row {
|
&.center .row {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -126,18 +129,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.details.text-label {
|
|
||||||
-webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
|
||||||
user-select: text;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
max-width: 400px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-input button {
|
.radio-input button {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-label.multiline {
|
||||||
|
-webkit-user-select: text; // Still required by Safari as of 2025
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by the "Third-Party Software License Notices" dialog
|
||||||
|
.details:has(.text-label.multiline.monospace) {
|
||||||
|
max-height: 60vh;
|
||||||
|
max-width: 80vw;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
// Used by the "Open Demo Artwork" dialog
|
// Used by the "Open Demo Artwork" dialog
|
||||||
.image-label {
|
.image-label {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|||||||
@@ -137,20 +137,23 @@
|
|||||||
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
|
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
|
||||||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
||||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
if (type === "Dialog" && floatingMenuContentDiv) {
|
||||||
// TODO: Also use https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver to detect any changes which may affect the size of the content.
|
|
||||||
// TODO: The current method only notices when the dialog size increases but can't detect when it decreases.
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
let { width, height } = entry.contentRect;
|
const existingWidth = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-x"));
|
||||||
|
const existingHeight = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-y"));
|
||||||
|
|
||||||
width = Math.ceil(width);
|
let { width, height } = entry.contentRect;
|
||||||
if (width % 2 === 1) width += 1;
|
width -= existingWidth;
|
||||||
height = Math.ceil(height);
|
height -= existingHeight;
|
||||||
if (height % 2 === 1) height += 1;
|
|
||||||
|
let targetWidth = Math.ceil(width);
|
||||||
|
if (targetWidth % 2 === 1) targetWidth += 1;
|
||||||
|
let targetHeight = Math.ceil(height);
|
||||||
|
if (targetHeight % 2 === 1) targetHeight += 1;
|
||||||
|
|
||||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
||||||
floatingMenuContentDiv.style.setProperty("min-width", width === 0 ? "unset" : `${width}px`);
|
floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-x", `${targetWidth - width}`);
|
||||||
floatingMenuContentDiv.style.setProperty("min-height", height === 0 ? "unset" : `${height}px`);
|
floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-y", `${targetHeight - height}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
resizeObserver.observe(floatingMenuContentDiv);
|
resizeObserver.observe(floatingMenuContentDiv);
|
||||||
@@ -158,14 +161,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
|
|
||||||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
|
||||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
|
||||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
|
||||||
floatingMenuContentDiv.style.setProperty("min-width", "unset");
|
|
||||||
floatingMenuContentDiv.style.setProperty("min-height", "unset");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the client bounds of the elements and apply relevant styles to them.
|
// Gets the client bounds of the elements and apply relevant styles to them.
|
||||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
|
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
|
||||||
// TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop).
|
// TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop).
|
||||||
|
|||||||
@@ -770,7 +770,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
-webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
-webkit-user-select: none; // Still required by Safari as of 2025
|
||||||
user-select: none;
|
user-select: none;
|
||||||
// Workaround for `user-select: none` not working on <input> elements
|
// Workaround for `user-select: none` not working on <input> elements
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let bold = false;
|
export let bold = false;
|
||||||
export let italic = false;
|
export let italic = false;
|
||||||
|
export let monospace = false;
|
||||||
export let centerAlign = false;
|
export let centerAlign = false;
|
||||||
export let tableAlign = false;
|
export let tableAlign = false;
|
||||||
export let minWidth = 0;
|
export let minWidth = "";
|
||||||
export let multiline = false;
|
export let multiline = false;
|
||||||
export let tooltip: string | undefined = undefined;
|
export let tooltip: string | undefined = undefined;
|
||||||
export let forCheckbox: bigint | undefined = undefined;
|
export let forCheckbox: bigint | undefined = undefined;
|
||||||
@@ -28,10 +29,11 @@
|
|||||||
class:disabled
|
class:disabled
|
||||||
class:bold
|
class:bold
|
||||||
class:italic
|
class:italic
|
||||||
|
class:monospace
|
||||||
class:multiline
|
class:multiline
|
||||||
class:center-align={centerAlign}
|
class:center-align={centerAlign}
|
||||||
class:table-align={tableAlign}
|
class:table-align={tableAlign}
|
||||||
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
|
style:min-width={minWidth || undefined}
|
||||||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined}
|
for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined}
|
||||||
@@ -58,6 +60,11 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.monospace {
|
||||||
|
font-family: "Source Code Pro", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
&.multiline {
|
&.multiline {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
|
|||||||
@@ -183,7 +183,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: "Inconsolata", monospace;
|
font-family: "Source Code Pro", monospace;
|
||||||
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
// This file is the browser's entry point for the JS bundle
|
// This file is the browser's entry point for the JS bundle
|
||||||
|
|
||||||
// Fonts
|
|
||||||
import "@fontsource/inconsolata";
|
|
||||||
import "@fontsource/source-sans-pro/400-italic.css";
|
|
||||||
import "@fontsource/source-sans-pro/400.css";
|
|
||||||
import "@fontsource/source-sans-pro/700-italic.css";
|
|
||||||
import "@fontsource/source-sans-pro/700.css";
|
|
||||||
|
|
||||||
// `reflect-metadata` allows for runtime reflection of types in JavaScript.
|
// `reflect-metadata` allows for runtime reflection of types in JavaScript.
|
||||||
// It is needed for class-transformer to work and is imported as a side effect.
|
// It is needed for class-transformer to work and is imported as a side effect.
|
||||||
// The library replaces the Reflect API on the window to support more features.
|
// The library replaces the Reflect API on the window to support more features.
|
||||||
|
|||||||
@@ -946,6 +946,8 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
|
|||||||
readonly commitDate!: string;
|
readonly commitDate!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TriggerDisplayThirdPartyLicensesDialog extends JsMessage {}
|
||||||
|
|
||||||
// WIDGET PROPS
|
// WIDGET PROPS
|
||||||
|
|
||||||
export abstract class WidgetProps {
|
export abstract class WidgetProps {
|
||||||
@@ -1370,13 +1372,15 @@ export class TextLabel extends WidgetProps {
|
|||||||
|
|
||||||
italic!: boolean;
|
italic!: boolean;
|
||||||
|
|
||||||
|
monospace!: boolean;
|
||||||
|
|
||||||
|
multiline!: boolean;
|
||||||
|
|
||||||
centerAlign!: boolean;
|
centerAlign!: boolean;
|
||||||
|
|
||||||
tableAlign!: boolean;
|
tableAlign!: boolean;
|
||||||
|
|
||||||
minWidth!: number;
|
minWidth!: string;
|
||||||
|
|
||||||
multiline!: boolean;
|
|
||||||
|
|
||||||
@Transform(({ value }: { value: string }) => value || undefined)
|
@Transform(({ value }: { value: string }) => value || undefined)
|
||||||
tooltip!: string | undefined;
|
tooltip!: string | undefined;
|
||||||
@@ -1692,6 +1696,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
SendUIMetadata,
|
SendUIMetadata,
|
||||||
TriggerAboutGraphiteLocalizedCommitDate,
|
TriggerAboutGraphiteLocalizedCommitDate,
|
||||||
|
TriggerDisplayThirdPartyLicensesDialog,
|
||||||
TriggerSaveDocument,
|
TriggerSaveDocument,
|
||||||
TriggerSaveFile,
|
TriggerSaveFile,
|
||||||
TriggerExportImage,
|
TriggerExportImage,
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { type Editor } from "@graphite/editor";
|
import { type Editor } from "@graphite/editor";
|
||||||
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/messages";
|
import {
|
||||||
|
defaultWidgetLayout,
|
||||||
|
DisplayDialog,
|
||||||
|
DisplayDialogDismiss,
|
||||||
|
UpdateDialogButtons,
|
||||||
|
UpdateDialogColumn1,
|
||||||
|
UpdateDialogColumn2,
|
||||||
|
patchWidgetLayout,
|
||||||
|
TriggerDisplayThirdPartyLicensesDialog,
|
||||||
|
} from "@graphite/messages";
|
||||||
import { type IconName } from "@graphite/utility-functions/icons";
|
import { type IconName } from "@graphite/utility-functions/icons";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
@@ -78,6 +87,17 @@ export function createDialogState(editor: Editor) {
|
|||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);
|
editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);
|
||||||
|
|
||||||
|
editor.subscriptions.subscribeJsMessage(TriggerDisplayThirdPartyLicensesDialog, async () => {
|
||||||
|
const BACKUP_URL = "https://editor.graphite.rs/third-party-licenses.txt";
|
||||||
|
let licenseText = `Content was not able to load. Please check your network connection and try again.\n\nOr visit ${BACKUP_URL} for the license notices.`;
|
||||||
|
if (editor.handle.inDevelopmentMode()) licenseText = `Third-party licenses are not available in development builds.\n\nVisit ${BACKUP_URL} for the license notices.`;
|
||||||
|
|
||||||
|
const response = await fetch("/third-party-licenses.txt");
|
||||||
|
if (response.ok && response.headers.get("Content-Type")?.includes("text/plain")) licenseText = await response.text();
|
||||||
|
|
||||||
|
editor.handle.requestLicensesThirdPartyDialogWithLicenseText(licenseText);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
dismissDialog,
|
dismissDialog,
|
||||||
|
|||||||
+83
-38
@@ -13,34 +13,19 @@ import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-asse
|
|||||||
|
|
||||||
const projectRootDir = path.resolve(__dirname);
|
const projectRootDir = path.resolve(__dirname);
|
||||||
|
|
||||||
// Keep this list in sync with those in `/about.toml` and `/deny.toml`.
|
|
||||||
const ALLOWED_LICENSES = [
|
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
|
||||||
"Apache-2.0",
|
|
||||||
"BSD-2-Clause",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"BSL-1.0",
|
|
||||||
"CC0-1.0",
|
|
||||||
"CDLA-Permissive-2.0",
|
|
||||||
"ISC",
|
|
||||||
"MIT-0",
|
|
||||||
"MIT",
|
|
||||||
"MPL-2.0",
|
|
||||||
"OpenSSL",
|
|
||||||
"Unicode-3.0",
|
|
||||||
"Unicode-DFS-2016",
|
|
||||||
"Zlib",
|
|
||||||
"NCSA",
|
|
||||||
];
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
svelte({
|
svelte({
|
||||||
preprocess: [sveltePreprocess()],
|
preprocess: [sveltePreprocess()],
|
||||||
onwarn(warning, defaultHandler) {
|
onwarn(warning, defaultHandler) {
|
||||||
// NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
const suppressed = [
|
||||||
const suppressed = ["css-unused-selector", "vite-plugin-svelte-css-no-scopable-elements", "a11y-no-static-element-interactions", "a11y-no-noninteractive-element-interactions"];
|
"css-unused-selector", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||||
|
"vite-plugin-svelte-css-no-scopable-elements", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||||
|
"a11y-no-static-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||||
|
"a11y-no-noninteractive-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||||
|
"a11y-click-events-have-key-events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||||
|
];
|
||||||
if (suppressed.includes(warning.code)) return;
|
if (suppressed.includes(warning.code)) return;
|
||||||
|
|
||||||
defaultHandler?.(warning);
|
defaultHandler?.(warning);
|
||||||
@@ -66,8 +51,10 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
rollupPluginLicense({
|
rollupPluginLicense({
|
||||||
thirdParty: {
|
thirdParty: {
|
||||||
|
includePrivate: false,
|
||||||
|
multipleVersions: true,
|
||||||
allow: {
|
allow: {
|
||||||
test: `(${ALLOWED_LICENSES.join(" OR ")})`,
|
test: `(${getAcceptedLicenses()})`,
|
||||||
failOnUnlicensed: true,
|
failOnUnlicensed: true,
|
||||||
failOnViolation: true,
|
failOnViolation: true,
|
||||||
},
|
},
|
||||||
@@ -103,10 +90,11 @@ type PackageInfo = {
|
|||||||
|
|
||||||
function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
||||||
// Generate the Rust license information.
|
// Generate the Rust license information.
|
||||||
let licenses = generateRustLicenses() || [];
|
const rustLicenses = generateRustLicenses();
|
||||||
|
const additionalLicenses = generateAdditionalLicenses();
|
||||||
|
|
||||||
// Ensure we have license information to work with before proceeding.
|
// Ensure we have the required license information to work with before proceeding.
|
||||||
if (licenses.length === 0) {
|
if (rustLicenses.length === 0) {
|
||||||
// This is probably caused by `cargo about` not being installed.
|
// This is probably caused by `cargo about` not being installed.
|
||||||
console.error("Could not run `cargo about`, which is required to generate license information.");
|
console.error("Could not run `cargo about`, which is required to generate license information.");
|
||||||
console.error("To install cargo-about on your system, you can run `cargo install cargo-about`.");
|
console.error("To install cargo-about on your system, you can run `cargo install cargo-about`.");
|
||||||
@@ -121,9 +109,11 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find then duplicate this license if one of its packages is `path-bool`, adding its notice text.
|
let licenses = rustLicenses.concat(additionalLicenses);
|
||||||
let foundLicensesIndex;
|
|
||||||
let foundPackagesIndex;
|
// SPECIAL CASE: Find then duplicate this license if one of its packages is `path-bool`, adding its notice text.
|
||||||
|
let foundLicensesIndex: number | undefined = undefined;
|
||||||
|
let foundPackagesIndex: number | undefined = undefined;
|
||||||
licenses.forEach((license, licenseIndex) => {
|
licenses.forEach((license, licenseIndex) => {
|
||||||
license.packages.forEach((pkg, pkgIndex) => {
|
license.packages.forEach((pkg, pkgIndex) => {
|
||||||
if (pkg.name === "path-bool") {
|
if (pkg.name === "path-bool") {
|
||||||
@@ -147,7 +137,7 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Augment the imported Rust license list with the provided JS license list.
|
// Extend the license list with the provided JS licenses.
|
||||||
jsLicenses.forEach((jsLicense) => {
|
jsLicenses.forEach((jsLicense) => {
|
||||||
const name = jsLicense.name || "";
|
const name = jsLicense.name || "";
|
||||||
const version = jsLicense.version || "";
|
const version = jsLicense.version || "";
|
||||||
@@ -158,14 +148,12 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||||||
|
|
||||||
let repository = jsLicense.repository || "";
|
let repository = jsLicense.repository || "";
|
||||||
if (repository && typeof repository === "object") repository = repository.url;
|
if (repository && typeof repository === "object") repository = repository.url;
|
||||||
// Remove the `git+` or `git://` prefix and `.git` suffix.
|
|
||||||
const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository;
|
|
||||||
|
|
||||||
const matchedLicense = licenses.find(
|
const matchedLicense = licenses.find(
|
||||||
(license) => license.licenseName === licenseName && trimBlankLines(license.licenseText || "") === licenseText && trimBlankLines(license.noticeText || "") === noticeText,
|
(license) => license.licenseName === licenseName && trimBlankLines(license.licenseText || "") === licenseText && trimBlankLines(license.noticeText || "") === noticeText,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pkg: PackageInfo = { name, version, author, repository: repo };
|
const pkg: PackageInfo = { name, version, author, repository };
|
||||||
if (matchedLicense) matchedLicense.packages.push(pkg);
|
if (matchedLicense) matchedLicense.packages.push(pkg);
|
||||||
else licenses.push({ licenseName, licenseText, noticeText, packages: [pkg] });
|
else licenses.push({ licenseName, licenseText, noticeText, packages: [pkg] });
|
||||||
});
|
});
|
||||||
@@ -232,7 +220,15 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||||||
licenses.forEach((license) => {
|
licenses.forEach((license) => {
|
||||||
let packagesWithSameLicense = license.packages.map((packageInfo) => {
|
let packagesWithSameLicense = license.packages.map((packageInfo) => {
|
||||||
const { name, version, author, repository } = packageInfo;
|
const { name, version, author, repository } = packageInfo;
|
||||||
return `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}`;
|
|
||||||
|
// Remove the `git+` or `git://` prefix and `.git` suffix.
|
||||||
|
let repo = repository;
|
||||||
|
if (repo.startsWith("git+")) repo = repo.slice("git+".length);
|
||||||
|
if (repo.startsWith("git://")) repo = repo.slice("git://".length);
|
||||||
|
if (repo.endsWith(".git")) repo = repo.slice(0, -".git".length);
|
||||||
|
if (repo.endsWith(".git#release")) repo = repo.slice(0, -".git#release".length);
|
||||||
|
|
||||||
|
return `${name} ${version}${author ? ` - ${author}` : ""}${repo ? ` - ${repo}` : ""}`;
|
||||||
});
|
});
|
||||||
const multi = packagesWithSameLicense.length !== 1;
|
const multi = packagesWithSameLicense.length !== 1;
|
||||||
const saysLicense = license.licenseName.toLowerCase().includes("license");
|
const saysLicense = license.licenseName.toLowerCase().includes("license");
|
||||||
@@ -249,10 +245,44 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||||||
formattedLicenseNotice += ` ${"‾".repeat(packagesLineLength + 2)}\n`;
|
formattedLicenseNotice += ` ${"‾".repeat(packagesLineLength + 2)}\n`;
|
||||||
formattedLicenseNotice += `${license.licenseText}\n`;
|
formattedLicenseNotice += `${license.licenseText}\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
formattedLicenseNotice += "\n";
|
||||||
return formattedLicenseNotice;
|
return formattedLicenseNotice;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRustLicenses(): LicenseInfo[] | undefined {
|
// Include additional licenses that aren't automatically generated by `cargo about` or `rollup-plugin-license`.
|
||||||
|
function generateAdditionalLicenses(): LicenseInfo[] {
|
||||||
|
const ADDITIONAL_LICENSES = [
|
||||||
|
{
|
||||||
|
licenseName: "SIL Open Font License 1.1",
|
||||||
|
licenseTextPath: "node_modules/source-sans/LICENSE.txt",
|
||||||
|
manifestPath: "node_modules/source-sans/package.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
licenseName: "SIL Open Font License 1.1",
|
||||||
|
licenseTextPath: "node_modules/source-code-pro/LICENSE.md",
|
||||||
|
manifestPath: "node_modules/source-code-pro/package.json",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return ADDITIONAL_LICENSES.map(({ licenseName, licenseTextPath, manifestPath }) => {
|
||||||
|
const licenseText = (fs.existsSync(licenseTextPath) && fs.readFileSync(licenseTextPath, "utf8")) || "";
|
||||||
|
|
||||||
|
const manifestJSON = (fs.existsSync(manifestPath) && JSON.parse(fs.readFileSync(manifestPath, "utf8"))) || {};
|
||||||
|
const name = manifestJSON.name || "";
|
||||||
|
const version = manifestJSON.version || "";
|
||||||
|
const author = manifestJSON.author.name || manifestJSON.author || "";
|
||||||
|
const repository = manifestJSON.repository?.url || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
licenseName,
|
||||||
|
licenseText: trimBlankLines(licenseText),
|
||||||
|
packages: [{ name, version, author, repository }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRustLicenses(): LicenseInfo[] {
|
||||||
// Log the starting status to the build output.
|
// Log the starting status to the build output.
|
||||||
console.info("\n\nGenerating license information for Rust code\n");
|
console.info("\n\nGenerating license information for Rust code\n");
|
||||||
|
|
||||||
@@ -272,14 +302,14 @@ function generateRustLicenses(): LicenseInfo[] | undefined {
|
|||||||
if (status !== 101) {
|
if (status !== 101) {
|
||||||
console.error("cargo-about failed", status, stderr);
|
console.error("cargo-about failed", status, stderr);
|
||||||
}
|
}
|
||||||
return undefined;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the output starts with this expected label, which lets us know the file generated with expected output.
|
// Make sure the output starts with this expected label, which lets us know the file generated with expected output.
|
||||||
// We don't want to eval an error message or something else, so we fail early if that happens.
|
// We don't want to eval an error message or something else, so we fail early if that happens.
|
||||||
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
|
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
|
||||||
console.error("Unexpected output from cargo-about", stdout);
|
console.error("Unexpected output from cargo-about", stdout);
|
||||||
return undefined;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the array JS syntax string into an actual JS array in memory.
|
// Convert the array JS syntax string into an actual JS array in memory.
|
||||||
@@ -308,7 +338,7 @@ function generateRustLicenses(): LicenseInfo[] | undefined {
|
|||||||
|
|
||||||
return rustLicenses;
|
return rustLicenses;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return undefined;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,3 +385,18 @@ function trimBlankLines(input: string): string {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAcceptedLicenses() {
|
||||||
|
const tomlContent = fs.readFileSync(path.resolve(__dirname, "../about.toml"), "utf8");
|
||||||
|
|
||||||
|
const licensesBlock = tomlContent?.match(/accepted\s*=\s*\[([^\]]*)\]/)?.[1] || "";
|
||||||
|
|
||||||
|
return licensesBlock
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.replace(/#.*$/, "")) // Remove comments
|
||||||
|
.join("\n")
|
||||||
|
.split(",")
|
||||||
|
.map((license) => license.trim().replace(/"/g, ""))
|
||||||
|
.filter((license) => license.length > 0)
|
||||||
|
.join(" OR ");
|
||||||
|
}
|
||||||
|
|||||||
@@ -466,6 +466,12 @@ impl EditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = requestLicensesThirdPartyDialogWithLicenseText)]
|
||||||
|
pub fn request_licenses_third_party_dialog_with_license_text(&self, license_text: String) {
|
||||||
|
let message = DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Send new bounds when document panel viewports get resized or moved within the editor
|
/// Send new bounds when document panel viewports get resized or moved within the editor
|
||||||
/// [left, top, right, bottom]...
|
/// [left, top, right, bottom]...
|
||||||
#[wasm_bindgen(js_name = boundsOfViewports)]
|
#[wasm_bindgen(js_name = boundsOfViewports)]
|
||||||
|
|||||||
Reference in New Issue
Block a user