mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 17:58:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
@@ -47,6 +47,11 @@ jobs:
|
||||
echo "Latest updated version:"
|
||||
rustc --version
|
||||
|
||||
- name: 🦀 Fetch Rust dependencies
|
||||
run: |
|
||||
echo "If it fails here, the committed Cargo.lock may be out of date"
|
||||
cargo fetch --locked
|
||||
|
||||
- name: ✂ Replace template in <head> of index.html
|
||||
run: |
|
||||
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
|
||||
|
||||
Vendored
+5
-6
@@ -39,12 +39,11 @@
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
// Svelte config
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
// 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",
|
||||
"a11y-no-static-element-interactions": "ignore",
|
||||
"a11y-no-noninteractive-element-interactions": "ignore",
|
||||
"a11y-click-events-have-key-events": "ignore"
|
||||
"css-unused-selector": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
},
|
||||
// VS Code config
|
||||
"html.format.wrapLineLength": 200,
|
||||
|
||||
Generated
+78
-7
@@ -1777,6 +1777,7 @@ version = "0.29.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"libm",
|
||||
"serde",
|
||||
]
|
||||
@@ -1948,6 +1949,7 @@ dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
"log",
|
||||
"lyon_geom",
|
||||
"node-macro",
|
||||
"num-traits",
|
||||
"parley",
|
||||
@@ -2022,6 +2024,7 @@ dependencies = [
|
||||
"kurbo",
|
||||
"ndarray",
|
||||
"node-macro",
|
||||
"num-traits",
|
||||
"rand 0.9.1",
|
||||
"rand_chacha 0.9.0",
|
||||
"serde",
|
||||
@@ -2076,17 +2079,19 @@ dependencies = [
|
||||
name = "graphite-desktop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"bytemuck",
|
||||
"cef",
|
||||
"core-foundation",
|
||||
"derivative",
|
||||
"dirs",
|
||||
"futures",
|
||||
"glam",
|
||||
"graph-craft",
|
||||
"graphene-std",
|
||||
"graphite-editor",
|
||||
"image",
|
||||
"graphite-desktop-wrapper",
|
||||
"include_dir",
|
||||
"libc",
|
||||
"objc2-io-surface",
|
||||
"objc2-metal 0.3.1",
|
||||
"open",
|
||||
"rfd",
|
||||
"ron",
|
||||
@@ -2095,10 +2100,28 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"vello",
|
||||
"wgpu",
|
||||
"wgpu-executor",
|
||||
"windows",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphite-desktop-wrapper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"futures",
|
||||
"graph-craft",
|
||||
"graphene-std",
|
||||
"graphite-editor",
|
||||
"image",
|
||||
"ron",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"vello",
|
||||
"wgpu",
|
||||
"wgpu-executor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphite-editor"
|
||||
version = "0.0.0"
|
||||
@@ -2978,6 +3001,17 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lyon_geom"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"euclid",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -3463,7 +3497,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
"objc2-metal 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3518,6 +3552,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-link-presentation"
|
||||
version = "0.2.2"
|
||||
@@ -3542,6 +3589,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
@@ -3552,7 +3613,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
"objc2-metal 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3797,9 +3858,13 @@ dependencies = [
|
||||
"glam",
|
||||
"glob",
|
||||
"image",
|
||||
"lyon_geom",
|
||||
"regex",
|
||||
"resvg",
|
||||
"roots",
|
||||
"rustc-hash 2.1.1",
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"svg",
|
||||
]
|
||||
|
||||
@@ -4627,6 +4692,12 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roots"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "082f11ffa03bbef6c2c6ea6bea1acafaade2fd9050ae0234ab44a2153742b058"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
|
||||
+5
-3
@@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"editor",
|
||||
"desktop",
|
||||
"desktop/wrapper",
|
||||
"proc-macros",
|
||||
"frontend/wasm",
|
||||
"node-graph/gapplication-io",
|
||||
@@ -125,9 +126,9 @@ usvg = "0.44"
|
||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||
rand_chacha = "0.9"
|
||||
glam = { version = "0.29", default-features = false, features = [
|
||||
"serde",
|
||||
"nostd-libm",
|
||||
"scalar-math",
|
||||
"debug-glam-assert",
|
||||
"bytemuck",
|
||||
] }
|
||||
base64 = "0.22"
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
@@ -141,7 +142,7 @@ pretty_assertions = "1.4.1"
|
||||
fern = { version = "0.7", features = ["colored"] }
|
||||
num_enum = "0.7"
|
||||
num-derive = "0.4"
|
||||
num-traits = { version = "0.2", default-features = false, features = ["i128"] }
|
||||
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
|
||||
specta = { version = "2.0.0-rc.22", features = [
|
||||
"glam",
|
||||
"derive",
|
||||
@@ -159,6 +160,7 @@ syn = { version = "2.0", default-features = false, features = [
|
||||
"proc-macro",
|
||||
] }
|
||||
kurbo = { version = "0.11.0", features = ["serde"] }
|
||||
lyon_geom = "1.0"
|
||||
petgraph = { version = "0.7.1", default-features = false, features = [
|
||||
"graphmap",
|
||||
] }
|
||||
|
||||
+18
-18
@@ -1,22 +1,22 @@
|
||||
# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`.
|
||||
accepted = [
|
||||
"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",
|
||||
"bzip2-1.0.6",
|
||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
||||
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSD-3-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSL-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"CC0-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"ISC", # Keep this list in sync with those in `/deny.toml`
|
||||
"MIT-0", # Keep this list in sync with those in `/deny.toml`
|
||||
"MIT", # Keep this list in sync with those in `/deny.toml`
|
||||
"MPL-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
|
||||
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
|
||||
"Zlib", # Keep this list in sync with those in `/deny.toml`
|
||||
"NCSA", # Keep this list in sync with those in `/deny.toml`
|
||||
"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"]
|
||||
ignore-build-dependencies = true
|
||||
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -63,25 +63,25 @@ ignore = [
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [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 = [
|
||||
"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",
|
||||
"bzip2-1.0.6",
|
||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
||||
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
||||
"BSD-3-Clause", # Keep this list in sync with those in `/about.toml`
|
||||
"BSL-1.0", # Keep this list in sync with those in `/about.toml`
|
||||
"CC0-1.0", # Keep this list in sync with those in `/about.toml`
|
||||
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"ISC", # Keep this list in sync with those in `/about.toml`
|
||||
"MIT-0", # Keep this list in sync with those in `/about.toml`
|
||||
"MIT", # Keep this list in sync with those in `/about.toml`
|
||||
"MPL-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"OpenSSL", # Keep this list in sync with those in `/about.toml`
|
||||
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
|
||||
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
|
||||
"Zlib", # Keep this list in sync with those in `/about.toml`
|
||||
"NCSA", # Keep this list in sync with those in `/about.toml`
|
||||
"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 higher the value, the more closely the license text must be to the
|
||||
|
||||
+32
-11
@@ -9,19 +9,18 @@ edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
default = ["gpu"]
|
||||
gpu = ["graphite-editor/gpu"]
|
||||
default = ["gpu", "accelerated_paint"]
|
||||
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]
|
||||
# # Local dependencies
|
||||
graphite-editor = { path = "../editor", features = [
|
||||
"gpu",
|
||||
"ron",
|
||||
"vello",
|
||||
] }
|
||||
graphene-std = { workspace = true }
|
||||
graph-craft = { workspace = true }
|
||||
wgpu-executor = { workspace = true }
|
||||
graphite-desktop-wrapper = { path = "wrapper" }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
@@ -39,4 +38,26 @@ vello = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
open = { workspace = true }
|
||||
image = { 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 }
|
||||
|
||||
+207
-233
@@ -1,26 +1,19 @@
|
||||
use crate::CustomEvent;
|
||||
use crate::WindowSize;
|
||||
use crate::consts::APP_NAME;
|
||||
use crate::dialogs::dialog_open_graphite_file;
|
||||
use crate::dialogs::dialog_save_file;
|
||||
use crate::dialogs::dialog_save_graphite_file;
|
||||
use crate::cef::WindowSize;
|
||||
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
|
||||
use crate::render::GraphicsState;
|
||||
use crate::render::WgpuContext;
|
||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
use std::fs;
|
||||
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform};
|
||||
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::StartCause;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::event_loop::ControlFlow;
|
||||
@@ -31,18 +24,33 @@ use winit::window::WindowId;
|
||||
use crate::cef;
|
||||
|
||||
pub(crate) struct WinitApp {
|
||||
cef_context: cef::Context<cef::Initialized>,
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
window: Option<Arc<Window>>,
|
||||
cef_schedule: Option<Instant>,
|
||||
window_size_sender: Sender<WindowSize>,
|
||||
graphics_state: Option<GraphicsState>,
|
||||
wgpu_context: WgpuContext,
|
||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
||||
editor: Editor,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
last_ui_update: Instant,
|
||||
avg_frame_time: f32,
|
||||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
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 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 {
|
||||
cef_context,
|
||||
window: None,
|
||||
@@ -51,96 +59,128 @@ impl WinitApp {
|
||||
window_size_sender,
|
||||
wgpu_context,
|
||||
event_loop_proxy,
|
||||
editor: Editor::new(),
|
||||
desktop_wrapper: DesktopWrapper::new(),
|
||||
last_ui_update: Instant::now(),
|
||||
avg_frame_time: 0.,
|
||||
start_render_sender,
|
||||
web_communication_initialized: false,
|
||||
web_communication_startup_buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_message(&mut self, message: Message) {
|
||||
let responses = self.editor.handle_message(message);
|
||||
self.send_messages_to_editor(responses);
|
||||
}
|
||||
|
||||
fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
|
||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) {
|
||||
let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() };
|
||||
if let Some(graphics_state) = &mut self.graphics_state {
|
||||
let scene = overlay_context.take_scene();
|
||||
graphics_state.set_overlays_scene(scene);
|
||||
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) {
|
||||
match message {
|
||||
DesktopFrontendMessage::ToWeb(messages) => {
|
||||
let Some(bytes) = serialize_frontend_messages(messages) else {
|
||||
tracing::error!("Failed to serialize frontend messages");
|
||||
return;
|
||||
};
|
||||
self.send_or_queue_web_message(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
for _ in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerOpenDocument)) {
|
||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let path = futures::executor::block_on(dialog_open_graphite_file());
|
||||
if let Some(path) = path {
|
||||
let content = std::fs::read_to_string(&path).unwrap_or_else(|_| {
|
||||
tracing::error!("Failed to read file: {}", path.display());
|
||||
String::new()
|
||||
});
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) {
|
||||
let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Some(path) = path {
|
||||
let _ = std::fs::write(&path, content);
|
||||
} else {
|
||||
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
|
||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let path = futures::executor::block_on(dialog_save_graphite_file(name));
|
||||
if let Some(path) = path {
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
tracing::error!("Failed to save file: {}: {}", path.display(), e);
|
||||
} else {
|
||||
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
|
||||
document_id,
|
||||
message: DocumentMessage::SavedDocument { path: Some(path) },
|
||||
});
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message));
|
||||
}
|
||||
let mut dialog = AsyncFileDialog::new().set_title(title);
|
||||
for filter in filters {
|
||||
dialog = dialog.add_filter(filter.name, &filter.extensions);
|
||||
}
|
||||
|
||||
let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) };
|
||||
|
||||
if let Some(path) = futures::executor::block_on(show_dialog)
|
||||
&& let Ok(content) = std::fs::read(&path)
|
||||
{
|
||||
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) {
|
||||
let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() };
|
||||
let _ = thread::spawn(move || {
|
||||
let path = futures::executor::block_on(dialog_save_file(name));
|
||||
if let Some(path) = path {
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
tracing::error!("Failed to save file: {}: {}", path.display(), e);
|
||||
DesktopFrontendMessage::SaveFileDialog {
|
||||
title,
|
||||
default_filename,
|
||||
default_folder,
|
||||
filters,
|
||||
context,
|
||||
} => {
|
||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
|
||||
if let Some(folder) = default_folder {
|
||||
dialog = dialog.set_directory(folder);
|
||||
}
|
||||
for filter in filters {
|
||||
dialog = dialog.add_filter(filter.name, &filter.extensions);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
|
||||
let _ = thread::spawn(move || {
|
||||
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };
|
||||
if let Err(e) = open::that(&url) {
|
||||
tracing::error!("Failed to open URL: {}: {}", url, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };
|
||||
|
||||
if responses.is_empty() {
|
||||
return;
|
||||
if let Some(path) = futures::executor::block_on(show_dialog) {
|
||||
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::WriteFile { path, content } => {
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
tracing::error!("Failed to write file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::OpenUrl(url) => {
|
||||
let _ = thread::spawn(move || {
|
||||
if let Err(e) = open::that(&url) {
|
||||
tracing::error!("Failed to open URL: {}: {}", url, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::UpdateViewportBounds { x, y, width, height } => {
|
||||
if let Some(graphics_state) = &mut self.graphics_state
|
||||
&& let Some(window) = &self.window
|
||||
{
|
||||
let window_size = window.inner_size();
|
||||
|
||||
let viewport_offset_x = x / window_size.width as f32;
|
||||
let viewport_offset_y = y / window_size.height as f32;
|
||||
graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]);
|
||||
|
||||
let viewport_scale_x = if width != 0.0 { window_size.width as f32 / width } else { 1.0 };
|
||||
let viewport_scale_y = if height != 0.0 { window_size.height as f32 / height } else { 1.0 };
|
||||
graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::UpdateOverlays(scene) => {
|
||||
if let Some(graphics_state) = &mut self.graphics_state {
|
||||
graphics_state.set_overlays_scene(scene);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::UpdateWindowState { maximized, minimized } => {
|
||||
if let Some(window) = &self.window {
|
||||
window.set_maximized(maximized);
|
||||
window.set_minimized(minimized);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::CloseWindow => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
|
||||
for message in messages {
|
||||
self.handle_desktop_frontend_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
|
||||
let responses = self.desktop_wrapper.dispatch(message);
|
||||
self.handle_desktop_frontend_messages(responses);
|
||||
}
|
||||
|
||||
fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
|
||||
if self.web_communication_initialized {
|
||||
self.cef_context.send_web_message(message);
|
||||
} else {
|
||||
self.web_communication_startup_buffer.push(message);
|
||||
}
|
||||
let Ok(message) = ron::to_string(&responses) else {
|
||||
tracing::error!("Failed to serialize Messages");
|
||||
return;
|
||||
};
|
||||
self.cef_context.send_web_message(message.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,23 +189,20 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
// Set a timeout in case we miss any cef schedule requests
|
||||
let timeout = Instant::now() + Duration::from_millis(10);
|
||||
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
|
||||
&& schedule < Instant::now()
|
||||
{
|
||||
self.cef_schedule = None;
|
||||
self.cef_context.work();
|
||||
}
|
||||
if let StartCause::ResumeTimeReached { .. } = cause {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
// Poll cef message loop multiple times to avoid message loop starvation
|
||||
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
|
||||
self.cef_context.work();
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -174,7 +211,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
.with_min_inner_size(winit::dpi::LogicalSize::new(400, 300))
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1200, 800));
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use crate::consts::APP_ID;
|
||||
use winit::platform::wayland::ActiveEventLoopExtWayland;
|
||||
@@ -194,17 +231,48 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
|
||||
tracing::info!("Winit window created and ready");
|
||||
|
||||
let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone());
|
||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||
|
||||
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
|
||||
#[cfg(target_os = "windows")]
|
||||
let platform = Platform::Windows;
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform = Platform::Mac;
|
||||
#[cfg(target_os = "linux")]
|
||||
let platform = Platform::Linux;
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform));
|
||||
}
|
||||
|
||||
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: CustomEvent) {
|
||||
match event {
|
||||
CustomEvent::WebCommunicationInitialized => {
|
||||
self.web_communication_initialized = true;
|
||||
for message in self.web_communication_startup_buffer.drain(..) {
|
||||
self.cef_context.send_web_message(message);
|
||||
}
|
||||
}
|
||||
CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
|
||||
CustomEvent::NodeGraphExecutionResult(result) => match result {
|
||||
NodeGraphExecutionResult::HasRun(texture) => {
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
|
||||
if let Some(texture) = texture
|
||||
&& let Some(graphics_state) = self.graphics_state.as_mut()
|
||||
&& let Some(window) = self.window.as_ref()
|
||||
{
|
||||
graphics_state.bind_viewport_texture(texture);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
NodeGraphExecutionResult::NotRun => {}
|
||||
},
|
||||
CustomEvent::UiUpdate(texture) => {
|
||||
if let Some(graphics_state) = self.graphics_state.as_mut() {
|
||||
graphics_state.resize(texture.width(), texture.height());
|
||||
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 {
|
||||
window.request_redraw();
|
||||
@@ -217,150 +285,56 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
self.cef_schedule = Some(instant);
|
||||
}
|
||||
}
|
||||
CustomEvent::DispatchMessage(message) => {
|
||||
self.dispatch_message(message);
|
||||
}
|
||||
CustomEvent::MessageReceived(message) => {
|
||||
if let Message::InputPreprocessor(_) = &message {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message {
|
||||
if let Some(graphic_state) = &mut self.graphics_state {
|
||||
let window_size = self.window.as_ref().unwrap().inner_size();
|
||||
let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32);
|
||||
let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size;
|
||||
let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size;
|
||||
let offset = top_left.to_array();
|
||||
let scale = (bottom_right - top_left).recip();
|
||||
graphic_state.set_viewport_offset(offset);
|
||||
graphic_state.set_viewport_scale(scale.to_array());
|
||||
} else {
|
||||
panic!("graphics state not intialized, viewport offset might be lost");
|
||||
}
|
||||
}
|
||||
CustomEvent::CloseWindow => {
|
||||
// TODO: Implement graceful shutdown
|
||||
|
||||
self.dispatch_message(message);
|
||||
}
|
||||
CustomEvent::NodeGraphRan(texture) => {
|
||||
if let Some(texture) = texture
|
||||
&& let Some(graphics_state) = &mut self.graphics_state
|
||||
{
|
||||
graphics_state.bind_viewport_texture(texture);
|
||||
}
|
||||
let mut responses = VecDeque::new();
|
||||
let err = self.editor.poll_node_graph_evaluation(&mut responses);
|
||||
if let Err(e) = err {
|
||||
if e != "No active document" {
|
||||
tracing::error!("Error poling node graph: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
for message in responses {
|
||||
self.dispatch_message(message);
|
||||
}
|
||||
tracing::info!("Exiting main event loop");
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
|
||||
WindowEvent::DroppedFile(path) => {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
|
||||
let Some(extension) = path.extension().and_then(|s| s.to_str()) else {
|
||||
tracing::warn!("Unsupported file dropped: {}", path.display());
|
||||
// Fine to early return since we don't need to do cef work in this case
|
||||
return;
|
||||
};
|
||||
let load_string = |path: &std::path::PathBuf| {
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
tracing::error!("Failed to read file: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
if content.is_empty() {
|
||||
tracing::warn!("Dropped file is empty: {}", path.display());
|
||||
return None;
|
||||
}
|
||||
Some(content)
|
||||
};
|
||||
// TODO: Consider moving this logic to the editor so we have one message to load data which is then demultiplexed in the portfolio message handler
|
||||
match extension {
|
||||
"graphite" => {
|
||||
let Some(content) = load_string(&path) else { return };
|
||||
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
self.dispatch_message(message.into());
|
||||
}
|
||||
"svg" => {
|
||||
let Some(content) = load_string(&path) else { return };
|
||||
|
||||
let message = PortfolioMessage::PasteSvg {
|
||||
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
|
||||
svg: content,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
self.dispatch_message(message.into());
|
||||
}
|
||||
_ => match image::ImageReader::open(&path) {
|
||||
Ok(reader) => match reader.decode() {
|
||||
Ok(image) => {
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
// TODO: support loading images with more than 8 bits per channel
|
||||
let image_data = image.to_rgba8();
|
||||
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
|
||||
|
||||
let message = PortfolioMessage::PasteImage {
|
||||
name,
|
||||
image,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
self.dispatch_message(message.into());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to decode image: {}: {}", path.display(), e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to open image file: {}: {}", path.display(), e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
tracing::info!("The close button was pressed; stopping");
|
||||
event_loop.exit();
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
|
||||
}
|
||||
WindowEvent::Resized(PhysicalSize { width, height }) => {
|
||||
let _ = self.window_size_sender.send(WindowSize::new(width as usize, height as usize));
|
||||
self.cef_context.notify_of_resize();
|
||||
}
|
||||
|
||||
WindowEvent::RedrawRequested => {
|
||||
let Some(ref mut graphics_state) = self.graphics_state else { return };
|
||||
// Only rerender once we have a new ui texture to display
|
||||
|
||||
match graphics_state.render() {
|
||||
Ok(_) => {}
|
||||
Err(wgpu::SurfaceError::Lost) => {
|
||||
tracing::warn!("lost surface");
|
||||
if let Some(window) = &self.window {
|
||||
match graphics_state.render(window.as_ref()) {
|
||||
Ok(_) => {}
|
||||
Err(wgpu::SurfaceError::Lost) => {
|
||||
tracing::warn!("lost surface");
|
||||
}
|
||||
Err(wgpu::SurfaceError::OutOfMemory) => {
|
||||
event_loop.exit();
|
||||
}
|
||||
Err(e) => tracing::error!("{:?}", e),
|
||||
}
|
||||
Err(wgpu::SurfaceError::OutOfMemory) => {
|
||||
event_loop.exit();
|
||||
}
|
||||
Err(e) => tracing::error!("{:?}", e),
|
||||
let _ = self.start_render_sender.try_send(());
|
||||
}
|
||||
}
|
||||
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
|
||||
WindowEvent::DroppedFile(path) => {
|
||||
match std::fs::read(&path) {
|
||||
Ok(content) => {
|
||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
+49
-11
@@ -1,26 +1,52 @@
|
||||
use crate::{CustomEvent, WgpuContext, render::FrameBufferRef};
|
||||
use std::{
|
||||
sync::{Arc, Mutex, mpsc::Receiver},
|
||||
time::Instant,
|
||||
};
|
||||
//! 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::render::FrameBufferRef;
|
||||
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
mod consts;
|
||||
mod context;
|
||||
mod dirs;
|
||||
mod input;
|
||||
mod internal;
|
||||
mod ipc;
|
||||
mod platform;
|
||||
mod scheme_handler;
|
||||
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;
|
||||
|
||||
pub(crate) trait CefEventHandler: Clone {
|
||||
fn window_size(&self) -> WindowSize;
|
||||
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
|
||||
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
||||
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
|
||||
fn initialized_web_communication(&self);
|
||||
fn receive_web_message(&self, message: &[u8]);
|
||||
}
|
||||
|
||||
@@ -120,14 +146,26 @@ impl CefEventHandler for CefHandler {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
|
||||
}
|
||||
|
||||
fn initialized_web_communication(&self) {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::WebCommunicationInitialized);
|
||||
}
|
||||
|
||||
fn receive_web_message(&self, message: &[u8]) {
|
||||
let str = std::str::from_utf8(message).unwrap();
|
||||
match ron::from_str(str) {
|
||||
Ok(message) => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived(message));
|
||||
let Some(desktop_wrapper_message) = deserialize_editor_message(message) else {
|
||||
tracing::error!("Failed to deserialize web message");
|
||||
return;
|
||||
};
|
||||
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 deserialize message {:?}", 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};
|
||||
use cef::{App, BrowserSettings, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, RenderHandler, RequestContext, WindowInfo, browser_host_create_browser_sync, initialize};
|
||||
use cef::{Browser, CefString, Settings, api_hash, args::Args, execute_process};
|
||||
use thiserror::Error;
|
||||
use winit::event::WindowEvent;
|
||||
mod multithreaded;
|
||||
mod singlethreaded;
|
||||
|
||||
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
||||
mod builder;
|
||||
pub(crate) use builder::{CefContextBuilder, InitError};
|
||||
|
||||
use super::input::InputState;
|
||||
use super::ipc::{MessageType, SendMessage};
|
||||
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||
use super::{CefEventHandler, input};
|
||||
pub(crate) trait CefContext {
|
||||
fn work(&mut self);
|
||||
|
||||
use super::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
|
||||
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
|
||||
|
||||
pub(crate) struct Setup {}
|
||||
pub(crate) struct Initialized {}
|
||||
pub(crate) trait ContextState {}
|
||||
impl ContextState for Setup {}
|
||||
impl ContextState for Initialized {}
|
||||
fn notify_of_resize(&self);
|
||||
|
||||
pub(crate) struct Context<S: ContextState> {
|
||||
args: Args,
|
||||
pub(crate) browser: Option<Browser>,
|
||||
pub(crate) input_state: InputState,
|
||||
marker: std::marker::PhantomData<S>,
|
||||
}
|
||||
|
||||
impl Context<Setup> {
|
||||
pub(crate) fn new() -> Result<Context<Setup>, SetupError> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _loader = {
|
||||
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
|
||||
assert!(loader.load());
|
||||
loader
|
||||
};
|
||||
let _ = api_hash(CEF_API_VERSION_LAST, 0);
|
||||
|
||||
let args = Args::new();
|
||||
let cmd = args.as_cmd_line().unwrap();
|
||||
let switch = CefString::from("type");
|
||||
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
|
||||
|
||||
if !is_browser_process {
|
||||
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
|
||||
let mut app = 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,
|
||||
fn send_web_message(&self, message: Vec<u8>);
|
||||
}
|
||||
|
||||
@@ -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::{ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
|
||||
use super::context::{Context, Initialized};
|
||||
|
||||
mod keymap;
|
||||
use keymap::{ToDomBits, ToVKBits};
|
||||
|
||||
pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: WindowEvent) -> Option<WindowEvent> {
|
||||
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
if let Some(browser) = &context.browser {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
}
|
||||
|
||||
context.input_state.update_mouse_position(&position);
|
||||
let mouse_event: MouseEvent = (&context.input_state).into();
|
||||
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
}
|
||||
|
||||
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, .. } => {
|
||||
if let Some(browser) = &context.browser {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
|
||||
let mouse_up = match state {
|
||||
ElementState::Pressed => 0,
|
||||
ElementState::Released => 1,
|
||||
};
|
||||
let mouse_up = match state {
|
||||
ElementState::Pressed => 0,
|
||||
ElementState::Released => 1,
|
||||
};
|
||||
|
||||
let cef_button = match button {
|
||||
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
|
||||
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
|
||||
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
|
||||
MouseButton::Forward => None, //TODO: Handle Forward button
|
||||
MouseButton::Back => None, //TODO: Handle Back button
|
||||
_ => None,
|
||||
};
|
||||
let cef_button = match button {
|
||||
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
|
||||
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
|
||||
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
|
||||
MouseButton::Forward => None, //TODO: Handle Forward button
|
||||
MouseButton::Back => None, //TODO: Handle Back button
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut mouse_state = context.input_state.mouse_state.clone();
|
||||
match button {
|
||||
MouseButton::Left => {
|
||||
mouse_state.left = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
let mut mouse_state = input_state.mouse_state.clone();
|
||||
match button {
|
||||
MouseButton::Left => {
|
||||
mouse_state.left = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
MouseButton::Right => {
|
||||
mouse_state.right = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
}
|
||||
MouseButton::Middle => {
|
||||
mouse_state.middle = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
context.input_state.update_mouse_state(mouse_state);
|
||||
|
||||
let mouse_event: MouseEvent = (&context.input_state).into();
|
||||
|
||||
if let Some(button) = cef_button {
|
||||
host.send_mouse_click_event(
|
||||
Some(&mouse_event),
|
||||
button,
|
||||
mouse_up,
|
||||
1, // click count
|
||||
);
|
||||
}
|
||||
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: _, .. } => {
|
||||
if let Some(browser) = &context.browser {
|
||||
if let Some(host) = browser.host() {
|
||||
let mouse_event = (&context.input_state).into();
|
||||
let line_width = 40; //feels about right, TODO: replace with correct value
|
||||
let line_height = 30; //feels about right, TODO: replace with correct value
|
||||
let (delta_x, delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||
}
|
||||
if let Some(host) = browser.host() {
|
||||
let mouse_event = input_state.into();
|
||||
let line_width = 40; //feels about right, TODO: replace with correct value
|
||||
let line_height = 30; //feels about right, TODO: replace with correct value
|
||||
let (delta_x, delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||
}
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
context.input_state.update_modifiers(&modifiers.state());
|
||||
input_state.update_modifiers(&modifiers.state());
|
||||
}
|
||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||
if let Some(browser) = &context.browser {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
|
||||
let (named_key, character) = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named_key) => (
|
||||
Some(named_key),
|
||||
match named_key {
|
||||
winit::keyboard::NamedKey::Space => Some(' '),
|
||||
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
||||
_ => None,
|
||||
},
|
||||
),
|
||||
winit::keyboard::Key::Character(str) => {
|
||||
let char = str.chars().next().unwrap_or('\0');
|
||||
(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();
|
||||
let (named_key, character) = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named_key) => (
|
||||
Some(named_key),
|
||||
match named_key {
|
||||
winit::keyboard::NamedKey::Space => Some(' '),
|
||||
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
||||
_ => None,
|
||||
},
|
||||
),
|
||||
winit::keyboard::Key::Character(str) => {
|
||||
let char = str.chars().next().unwrap_or('\0');
|
||||
(None, Some(char))
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match event.state {
|
||||
ElementState::Pressed => {
|
||||
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
|
||||
host.send_key_event(Some(&key_event));
|
||||
let mut key_event = KeyEvent {
|
||||
size: size_of::<KeyEvent>(),
|
||||
focus_on_editable_field: 1,
|
||||
modifiers: input_state.cef_modifiers(&event.location, event.repeat).raw(),
|
||||
is_system_key: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
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();
|
||||
}
|
||||
|
||||
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)]
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ mod browser_process_app;
|
||||
mod browser_process_client;
|
||||
mod browser_process_handler;
|
||||
mod browser_process_life_span_handler;
|
||||
mod render_handler;
|
||||
mod render_process_app;
|
||||
mod render_process_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_client::BrowserProcessClientImpl;
|
||||
pub(super) use render_handler::RenderHandlerImpl;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::env;
|
||||
|
||||
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>) {
|
||||
if let Some(cmd) = command_line {
|
||||
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
|
||||
cmd.append_switch(Some(&CefString::from("disable-gpu")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
{
|
||||
// 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
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let use_wayland = env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
|
||||
@@ -32,6 +32,10 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
) -> ::std::os::raw::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::Initialized,
|
||||
data: _,
|
||||
}) => self.event_handler.initialized_web_communication(),
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToNative,
|
||||
data,
|
||||
|
||||
@@ -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 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> {
|
||||
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>,
|
||||
event_handler: H,
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
@@ -47,6 +48,23 @@ impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
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 {
|
||||
self.object.cast()
|
||||
}
|
||||
|
||||
@@ -76,24 +76,30 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
}
|
||||
|
||||
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
|
||||
let function_name = "sendNativeMessage";
|
||||
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
|
||||
let mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
|
||||
let Some(mut function) = v8_value_create_function(Some(&CefString::from(name)), Some(&mut v8_handler)) else {
|
||||
tracing::error!("Failed to create V8 function {name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return;
|
||||
};
|
||||
global.set_value_bykey(Some(&CefString::from(name)), Some(&mut function), V8Propertyattribute::default());
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
|
||||
let Some(mut function) = v8_value_create_function(Some(&CefString::from(function_name)), Some(&mut v8_handler)) else {
|
||||
tracing::error!("Failed to create V8 function {function_name}");
|
||||
return;
|
||||
};
|
||||
let initialized_function_name = "initializeNativeCommunication";
|
||||
let send_function_name = "sendNativeMessage";
|
||||
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return;
|
||||
};
|
||||
global.set_value_bykey(Some(&CefString::from(function_name)), Some(&mut function), V8Propertyattribute::default());
|
||||
register_js_function(context, initialized_function_name);
|
||||
register_js_function(context, send_function_name);
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
|
||||
|
||||
@@ -21,8 +21,11 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
_retval: Option<&mut Option<V8Value>>,
|
||||
_exception: Option<&mut cef::CefString>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
if let Some(name) = name {
|
||||
if name.to_string() == "sendNativeMessage" {
|
||||
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
|
||||
"initializeNativeCommunication" => {
|
||||
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
|
||||
}
|
||||
"sendNativeMessage" => {
|
||||
let Some(args) = arguments else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
@@ -48,6 +51,9 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
|
||||
return 1;
|
||||
}
|
||||
name => {
|
||||
tracing::error!("Unknown V8 function called: {}", name);
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
-18
@@ -1,14 +1,17 @@
|
||||
use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||
|
||||
use super::{Context, Initialized};
|
||||
use cef::{CefString, Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||
|
||||
pub(crate) enum MessageType {
|
||||
Initialized,
|
||||
SendToJS,
|
||||
SendToNative,
|
||||
}
|
||||
impl From<MessageType> for MessageInfo {
|
||||
fn from(val: MessageType) -> Self {
|
||||
match val {
|
||||
MessageType::Initialized => MessageInfo {
|
||||
name: "initialized".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
MessageType::SendToJS => MessageInfo {
|
||||
name: "send_to_js".to_string(),
|
||||
target: cef_process_id_t::PID_RENDERER.into(),
|
||||
@@ -24,6 +27,7 @@ impl TryFrom<String> for MessageType {
|
||||
type Error = ();
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"initialized" => Ok(MessageType::Initialized),
|
||||
"send_to_js" => Ok(MessageType::SendToJS),
|
||||
"send_to_native" => Ok(MessageType::SendToNative),
|
||||
_ => Err(()),
|
||||
@@ -39,21 +43,6 @@ pub(crate) struct MessageInfo {
|
||||
pub(crate) trait SendMessage {
|
||||
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> {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
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};
|
||||
|
||||
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
|
||||
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
|
||||
use super::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||
|
||||
pub(crate) struct GraphiteSchemeHandlerFactory {
|
||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||
@@ -88,6 +87,7 @@ impl<'a> GraphiteFrontendResourceHandler<'a> {
|
||||
match ext {
|
||||
"html" => Some("text/html".to_string()),
|
||||
"css" => Some("text/css".to_string()),
|
||||
"txt" => Some("text/plain".to_string()),
|
||||
"wasm" => Some("application/wasm".to_string()),
|
||||
"js" => Some("application/javascript".to_string()),
|
||||
"png" => Some("image/png".to_string()),
|
||||
|
||||
@@ -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_ID: &str = "rs.graphite.GraphiteEditor";
|
||||
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;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rfd::AsyncFileDialog;
|
||||
|
||||
pub(crate) async fn dialog_open_graphite_file() -> Option<PathBuf> {
|
||||
AsyncFileDialog::new()
|
||||
.add_filter("Graphite", &["graphite"])
|
||||
.set_title("Open Graphite Document")
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub(crate) async fn dialog_save_graphite_file(name: String) -> Option<PathBuf> {
|
||||
AsyncFileDialog::new()
|
||||
.add_filter("Graphite", &["graphite"])
|
||||
.set_title("Save Graphite Document")
|
||||
.set_file_name(name)
|
||||
.save_file()
|
||||
.await
|
||||
.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub(crate) async fn dialog_save_file(name: String) -> Option<PathBuf> {
|
||||
AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
+25
-36
@@ -1,53 +1,53 @@
|
||||
use std::process::exit;
|
||||
use std::time::Instant;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
use graphite_editor::messages::prelude::Message;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
pub(crate) mod consts;
|
||||
|
||||
mod cef;
|
||||
use cef::{Setup, WindowSize};
|
||||
|
||||
mod render;
|
||||
use render::WgpuContext;
|
||||
|
||||
mod app;
|
||||
use app::WinitApp;
|
||||
|
||||
mod dirs;
|
||||
|
||||
mod dialogs;
|
||||
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
|
||||
use graphite_desktop_wrapper::{NodeGraphExecutionResult, WgpuContext};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CustomEvent {
|
||||
UiUpdate(wgpu::Texture),
|
||||
ScheduleBrowserWork(Instant),
|
||||
DispatchMessage(Message),
|
||||
MessageReceived(Message),
|
||||
NodeGraphRan(Option<wgpu::Texture>),
|
||||
WebCommunicationInitialized,
|
||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||
CloseWindow,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||
|
||||
let cef_context = match cef::Context::<Setup>::new() {
|
||||
Ok(c) => c,
|
||||
Err(cef::SetupError::Subprocess) => exit(0),
|
||||
Err(cef::SetupError::SubprocessFailed(t)) => {
|
||||
tracing::error!("Subprocess of type {t} failed");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
let cef_context_builder = cef::CefContextBuilder::new();
|
||||
|
||||
if cef_context_builder.is_sub_process() {
|
||||
// We are in a CEF subprocess
|
||||
// This will block until the CEF subprocess quits
|
||||
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 (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
|
||||
let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) {
|
||||
let cef_handler = 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,
|
||||
Err(cef::InitError::AlreadyRunning) => {
|
||||
tracing::error!("Another instance is already running, Exiting.");
|
||||
@@ -57,26 +57,15 @@ fn main() {
|
||||
tracing::error!("Cef initialization failed with code: {code}");
|
||||
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 target_fps = 60;
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
let last_render = Instant::now();
|
||||
let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph());
|
||||
if has_run {
|
||||
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan(texture.map(|t| (*t.texture).clone())));
|
||||
}
|
||||
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());
|
||||
let mut winit_app = WinitApp::new(Box::new(cef_context), window_size_sender, wgpu_context, event_loop.create_proxy());
|
||||
|
||||
event_loop.run_app(&mut winit_app).unwrap();
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ mod frame_buffer_ref;
|
||||
pub(crate) use frame_buffer_ref::FrameBufferRef;
|
||||
|
||||
mod graphics_state;
|
||||
pub(crate) use graphics_state::{GraphicsState, WgpuContext};
|
||||
pub(crate) use graphics_state::GraphicsState;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use graphene_std::Color;
|
||||
use std::sync::Arc;
|
||||
use wgpu_executor::WgpuExecutor;
|
||||
use winit::window::Window;
|
||||
|
||||
pub(crate) use wgpu_executor::Context as WgpuContext;
|
||||
use graphite_desktop_wrapper::{Color, WgpuContext, WgpuExecutor};
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
@@ -234,7 +232,7 @@ impl GraphicsState {
|
||||
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() {
|
||||
self.render_overlays(scene);
|
||||
}
|
||||
@@ -277,6 +275,7 @@ impl GraphicsState {
|
||||
}
|
||||
}
|
||||
self.context.queue.submit(std::iter::once(encoder.finish()));
|
||||
window.pre_present_notify();
|
||||
output.present();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "graphite-desktop-wrapper"
|
||||
version = "0.1.0"
|
||||
description = "Graphite Desktop Wrapper"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
default = ["gpu"]
|
||||
gpu = ["graphite-editor/gpu"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-editor = { path = "../../editor", features = [
|
||||
"gpu",
|
||||
"ron",
|
||||
"vello",
|
||||
] }
|
||||
graphene-std = { workspace = true }
|
||||
graph-craft = { workspace = true }
|
||||
wgpu-executor = { workspace = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
ron = { workspace = true}
|
||||
vello = { workspace = true }
|
||||
image = { workspace = true }
|
||||
@@ -0,0 +1,122 @@
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use graphite_editor::messages::prelude::{AppWindowMessage, DocumentMessage, PortfolioMessage};
|
||||
|
||||
use crate::messages::Platform;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext};
|
||||
|
||||
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
|
||||
match message {
|
||||
DesktopWrapperMessage::FromWeb(message) => {
|
||||
dispatcher.queue_editor_message(*message);
|
||||
}
|
||||
DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context {
|
||||
OpenFileDialogContext::Document => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
||||
}
|
||||
OpenFileDialogContext::Import => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
||||
}
|
||||
},
|
||||
DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context {
|
||||
SaveFileDialogContext::Document { document_id, content } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WriteFile { path: path.clone(), content });
|
||||
dispatcher.queue_editor_message(EditorMessage::Portfolio(PortfolioMessage::DocumentPassMessage {
|
||||
document_id,
|
||||
message: DocumentMessage::SavedDocument { path: Some(path) },
|
||||
}));
|
||||
}
|
||||
SaveFileDialogContext::File { content } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
|
||||
}
|
||||
},
|
||||
DesktopWrapperMessage::OpenFile { path, content } => {
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
match extension.as_str() {
|
||||
"graphite" => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
||||
}
|
||||
_ => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
DesktopWrapperMessage::OpenDocument { path, content } => {
|
||||
let Ok(content) = String::from_utf8(content) else {
|
||||
tracing::warn!("Document file is invalid: {}", path.display());
|
||||
return;
|
||||
};
|
||||
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: None,
|
||||
document_path: Some(path),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
dispatcher.queue_editor_message(message.into());
|
||||
}
|
||||
DesktopWrapperMessage::ImportFile { path, content } => {
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
match extension.as_str() {
|
||||
"svg" => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content });
|
||||
}
|
||||
_ => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportImage { path, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
DesktopWrapperMessage::ImportSvg { path, content } => {
|
||||
let Ok(content) = String::from_utf8(content) else {
|
||||
tracing::warn!("Svg file is invalid: {}", path.display());
|
||||
return;
|
||||
};
|
||||
|
||||
let message = PortfolioMessage::PasteSvg {
|
||||
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
|
||||
svg: content,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
dispatcher.queue_editor_message(message.into());
|
||||
}
|
||||
DesktopWrapperMessage::ImportImage { path, content } => {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
let Some(image_format) = image::ImageFormat::from_extension(&extension) else {
|
||||
tracing::warn!("Unsupported file type: {}", path.display());
|
||||
return;
|
||||
};
|
||||
let reader = image::ImageReader::with_format(std::io::Cursor::new(content), image_format);
|
||||
let Ok(image) = reader.decode() else {
|
||||
tracing::error!("Failed to decode image: {}", path.display());
|
||||
return;
|
||||
};
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
|
||||
// TODO: Handle Image formats with more than 8 bits per channel
|
||||
let image_data = image.to_rgba8();
|
||||
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
|
||||
let message = PortfolioMessage::PasteImage {
|
||||
name,
|
||||
image,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
dispatcher.queue_editor_message(message.into());
|
||||
}
|
||||
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
|
||||
DesktopWrapperMessage::UpdatePlatform(platform) => {
|
||||
let platform = match platform {
|
||||
Platform::Windows => AppWindowPlatform::Windows,
|
||||
Platform::Mac => AppWindowPlatform::Mac,
|
||||
Platform::Linux => AppWindowPlatform::Linux,
|
||||
};
|
||||
let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
|
||||
dispatcher.queue_editor_message(message.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use graphite_editor::messages::prelude::InputPreprocessorMessage;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, EditorMessage};
|
||||
|
||||
pub(super) fn intercept_editor_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: EditorMessage) -> Option<EditorMessage> {
|
||||
match message {
|
||||
EditorMessage::InputPreprocessor(message) => {
|
||||
if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message {
|
||||
let top_left = bounds_of_viewports[0].top_left;
|
||||
let bottom_right = bounds_of_viewports[0].bottom_right;
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateViewportBounds {
|
||||
x: top_left.x as f32,
|
||||
y: top_left.y as f32,
|
||||
width: (bottom_right.x - top_left.x) as f32,
|
||||
height: (bottom_right.y - top_left.y) as f32,
|
||||
});
|
||||
}
|
||||
Some(EditorMessage::InputPreprocessor(message))
|
||||
}
|
||||
m => Some(m),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use graphite_editor::messages::prelude::FrontendMessage;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
||||
|
||||
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
|
||||
match message {
|
||||
FrontendMessage::RenderOverlays { context } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
||||
}
|
||||
FrontendMessage::TriggerOpenDocument => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||
title: "Open Document".to_string(),
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite".to_string(),
|
||||
extensions: vec!["graphite".to_string()],
|
||||
}],
|
||||
context: OpenFileDialogContext::Document,
|
||||
});
|
||||
}
|
||||
FrontendMessage::TriggerImport => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||
title: "Import File".to_string(),
|
||||
filters: vec![
|
||||
FileFilter {
|
||||
name: "Svg".to_string(),
|
||||
extensions: vec!["svg".to_string()],
|
||||
},
|
||||
FileFilter {
|
||||
name: "Image".to_string(),
|
||||
extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()],
|
||||
},
|
||||
],
|
||||
context: OpenFileDialogContext::Import,
|
||||
});
|
||||
}
|
||||
FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => {
|
||||
if let Some(path) = path {
|
||||
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
|
||||
} else {
|
||||
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
||||
title: "Save Document".to_string(),
|
||||
default_filename: name,
|
||||
default_folder: path.and_then(|p| p.parent().map(PathBuf::from)),
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite".to_string(),
|
||||
extensions: vec!["graphite".to_string()],
|
||||
}],
|
||||
context: SaveFileDialogContext::Document { document_id, content },
|
||||
});
|
||||
}
|
||||
}
|
||||
FrontendMessage::TriggerSaveFile { name, content } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
||||
title: "Save File".to_string(),
|
||||
default_filename: name,
|
||||
default_folder: None,
|
||||
filters: Vec::new(),
|
||||
context: SaveFileDialogContext::File { content },
|
||||
});
|
||||
}
|
||||
FrontendMessage::TriggerVisitLink { url } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenUrl(url));
|
||||
}
|
||||
FrontendMessage::UpdateWindowState { maximized, minimized } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateWindowState { maximized, minimized });
|
||||
|
||||
// Forward this to update the ui
|
||||
return Some(message);
|
||||
}
|
||||
FrontendMessage::CloseWindow => {
|
||||
dispatcher.respond(DesktopFrontendMessage::CloseWindow);
|
||||
}
|
||||
m => return Some(m),
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::messages::prelude::{FrontendMessage, Message};
|
||||
|
||||
// TODO: Remove usage of this reexport in desktop create and remove this line
|
||||
pub use graphene_std::Color;
|
||||
|
||||
pub use wgpu_executor::Context as WgpuContext;
|
||||
pub use wgpu_executor::WgpuExecutor;
|
||||
|
||||
pub mod messages;
|
||||
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
||||
|
||||
mod message_dispatcher;
|
||||
use message_dispatcher::DesktopWrapperMessageDispatcher;
|
||||
|
||||
mod handle_desktop_wrapper_message;
|
||||
mod intercept_editor_message;
|
||||
mod intercept_frontend_message;
|
||||
|
||||
pub struct DesktopWrapper {
|
||||
editor: Editor,
|
||||
}
|
||||
|
||||
impl DesktopWrapper {
|
||||
pub fn new() -> Self {
|
||||
Self { editor: Editor::new() }
|
||||
}
|
||||
|
||||
pub fn init(&self, wgpu_context: WgpuContext) {
|
||||
let application_io = WasmApplicationIo::new_with_context(wgpu_context);
|
||||
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec<DesktopFrontendMessage> {
|
||||
let mut executor = DesktopWrapperMessageDispatcher::new(&mut self.editor);
|
||||
executor.queue_desktop_wrapper_message(message);
|
||||
executor.execute()
|
||||
}
|
||||
|
||||
pub async fn execute_node_graph() -> NodeGraphExecutionResult {
|
||||
let result = graphite_editor::node_graph_executor::run_node_graph().await;
|
||||
match result {
|
||||
(true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)),
|
||||
(false, _) => NodeGraphExecutionResult::NotRun,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DesktopWrapper {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum NodeGraphExecutionResult {
|
||||
HasRun(Option<wgpu::Texture>),
|
||||
NotRun,
|
||||
}
|
||||
|
||||
pub fn deserialize_editor_message(data: &[u8]) -> Option<DesktopWrapperMessage> {
|
||||
if let Ok(string) = std::str::from_utf8(data) {
|
||||
if let Ok(message) = ron::de::from_str::<Message>(string) {
|
||||
Some(DesktopWrapperMessage::FromWeb(message.into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_frontend_messages(messages: Vec<FrontendMessage>) -> Option<Vec<u8>> {
|
||||
if let Ok(serialized) = ron::ser::to_string(&messages) {
|
||||
Some(serialized.into_bytes())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use graphite_editor::application::Editor;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::handle_desktop_wrapper_message::handle_desktop_wrapper_message;
|
||||
use super::intercept_editor_message::intercept_editor_message;
|
||||
use super::intercept_frontend_message::intercept_frontend_message;
|
||||
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage};
|
||||
|
||||
pub(crate) struct DesktopWrapperMessageDispatcher<'a> {
|
||||
editor: &'a mut Editor,
|
||||
desktop_wrapper_message_queue: VecDeque<DesktopWrapperMessage>,
|
||||
editor_message_queue: Vec<EditorMessage>,
|
||||
responses: Vec<DesktopFrontendMessage>,
|
||||
}
|
||||
|
||||
impl<'a> DesktopWrapperMessageDispatcher<'a> {
|
||||
pub(crate) fn new(editor: &'a mut Editor) -> Self {
|
||||
Self {
|
||||
editor,
|
||||
desktop_wrapper_message_queue: VecDeque::new(),
|
||||
editor_message_queue: Vec::new(),
|
||||
responses: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execute(mut self) -> Vec<DesktopFrontendMessage> {
|
||||
self.process_queue();
|
||||
self.responses
|
||||
}
|
||||
|
||||
pub(crate) fn queue_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
|
||||
self.desktop_wrapper_message_queue.push_back(message);
|
||||
}
|
||||
|
||||
pub(super) fn queue_editor_message(&mut self, message: EditorMessage) {
|
||||
if let Some(message) = intercept_editor_message(self, message) {
|
||||
self.editor_message_queue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn respond(&mut self, response: DesktopFrontendMessage) {
|
||||
self.responses.push(response);
|
||||
}
|
||||
|
||||
pub(super) fn poll_node_graph_evaluation(&mut self) {
|
||||
let mut responses = VecDeque::new();
|
||||
if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) {
|
||||
if e != "No active document" {
|
||||
tracing::error!("Error poling node graph: {}", e);
|
||||
}
|
||||
}
|
||||
while let Some(message) = responses.pop_front() {
|
||||
self.queue_editor_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_queue(&mut self) {
|
||||
let mut frontend_messages = Vec::new();
|
||||
|
||||
while !self.desktop_wrapper_message_queue.is_empty() || !self.editor_message_queue.is_empty() {
|
||||
while let Some(message) = self.desktop_wrapper_message_queue.pop_front() {
|
||||
handle_desktop_wrapper_message(self, message);
|
||||
}
|
||||
let current_frontend_messages = self
|
||||
.editor
|
||||
.handle_message(EditorMessage::Batched {
|
||||
messages: std::mem::take(&mut self.editor_message_queue).into_boxed_slice(),
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|m| intercept_frontend_message(self, m));
|
||||
frontend_messages.extend(current_frontend_messages);
|
||||
}
|
||||
|
||||
self.respond(DesktopFrontendMessage::ToWeb(frontend_messages));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use graphite_editor::messages::prelude::{DocumentId, FrontendMessage};
|
||||
|
||||
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
|
||||
|
||||
pub enum DesktopFrontendMessage {
|
||||
ToWeb(Vec<FrontendMessage>),
|
||||
OpenFileDialog {
|
||||
title: String,
|
||||
filters: Vec<FileFilter>,
|
||||
context: OpenFileDialogContext,
|
||||
},
|
||||
SaveFileDialog {
|
||||
title: String,
|
||||
default_filename: String,
|
||||
default_folder: Option<PathBuf>,
|
||||
filters: Vec<FileFilter>,
|
||||
context: SaveFileDialogContext,
|
||||
},
|
||||
WriteFile {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
OpenUrl(String),
|
||||
UpdateViewportBounds {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
},
|
||||
UpdateOverlays(vello::Scene),
|
||||
UpdateWindowState {
|
||||
maximized: bool,
|
||||
minimized: bool,
|
||||
},
|
||||
CloseWindow,
|
||||
}
|
||||
|
||||
pub struct FileFilter {
|
||||
pub name: String,
|
||||
pub extensions: Vec<String>,
|
||||
}
|
||||
|
||||
pub enum DesktopWrapperMessage {
|
||||
FromWeb(Box<EditorMessage>),
|
||||
OpenFileDialogResult { path: PathBuf, content: Vec<u8>, context: OpenFileDialogContext },
|
||||
SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext },
|
||||
OpenDocument { path: PathBuf, content: Vec<u8> },
|
||||
OpenFile { path: PathBuf, content: Vec<u8> },
|
||||
ImportFile { path: PathBuf, content: Vec<u8> },
|
||||
ImportSvg { path: PathBuf, content: Vec<u8> },
|
||||
ImportImage { path: PathBuf, content: Vec<u8> },
|
||||
PollNodeGraphEvaluation,
|
||||
UpdatePlatform(Platform),
|
||||
}
|
||||
|
||||
pub enum OpenFileDialogContext {
|
||||
Document,
|
||||
Import,
|
||||
}
|
||||
|
||||
pub enum SaveFileDialogContext {
|
||||
Document { document_id: DocumentId, content: Vec<u8> },
|
||||
File { content: Vec<u8> },
|
||||
}
|
||||
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
@@ -151,8 +151,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||
|
||||
// DOCUMENT
|
||||
pub const FILE_EXTENSION: &str = "graphite";
|
||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
||||
];
|
||||
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
|
||||
@@ -442,7 +442,7 @@ mod test {
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 6);
|
||||
|
||||
println!("{:?} {:?}", layers_after_copy, layers_before_copy);
|
||||
println!("{layers_after_copy:?} {layers_before_copy:?}");
|
||||
|
||||
assert_eq!(layers_after_copy[5], shape_id);
|
||||
}
|
||||
@@ -497,7 +497,8 @@ mod test {
|
||||
);
|
||||
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: document_name.into(),
|
||||
document_name: Some(document_name.to_string()),
|
||||
document_path: None,
|
||||
document_serialized_content,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use super::animation_message_handler::AnimationTimeMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Animation)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use super::app_window_message_handler::AppWindowPlatform;
|
||||
|
||||
#[impl_message(Message, AppWindow)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppWindowMessage {
|
||||
AppWindowMinimize,
|
||||
AppWindowMaximize,
|
||||
AppWindowUpdatePlatform { platform: AppWindowPlatform },
|
||||
AppWindowClose,
|
||||
}
|
||||
|
||||
@@ -6,34 +6,34 @@ use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
pub struct AppWindowMessageHandler {
|
||||
platform: AppWindowPlatform,
|
||||
maximized: bool,
|
||||
viewport_hole_punch_active: bool,
|
||||
minimized: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
AppWindowMessage::AppWindowMinimize => {
|
||||
self.platform = if self.platform == AppWindowPlatform::Mac {
|
||||
AppWindowPlatform::Windows
|
||||
} else {
|
||||
AppWindowPlatform::Mac
|
||||
};
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
AppWindowMessage::AppWindowMaximize => {
|
||||
self.maximized = !self.maximized;
|
||||
responses.add(FrontendMessage::UpdateMaximized { maximized: self.maximized });
|
||||
|
||||
self.viewport_hole_punch_active = !self.viewport_hole_punch_active;
|
||||
responses.add(FrontendMessage::UpdateViewportHolePunch {
|
||||
active: self.viewport_hole_punch_active,
|
||||
responses.add(FrontendMessage::UpdateWindowState {
|
||||
maximized: self.maximized,
|
||||
minimized: self.minimized,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
self.platform = AppWindowPlatform::Web;
|
||||
AppWindowMessage::AppWindowMinimize => {
|
||||
self.minimized = !self.minimized;
|
||||
responses.add(FrontendMessage::UpdateWindowState {
|
||||
maximized: self.maximized,
|
||||
minimized: self.minimized,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowUpdatePlatform { platform } => {
|
||||
self.platform = platform;
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
responses.add(FrontendMessage::CloseWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ use crate::messages::prelude::*;
|
||||
pub enum BroadcastMessage {
|
||||
// Sub-messages
|
||||
#[child]
|
||||
TriggerEvent(BroadcastEvent),
|
||||
TriggerEvent(EventMessage),
|
||||
|
||||
// Messages
|
||||
SubscribeEvent {
|
||||
on: BroadcastEvent,
|
||||
on: EventMessage,
|
||||
send: Box<Message>,
|
||||
},
|
||||
UnsubscribeEvent {
|
||||
on: BroadcastEvent,
|
||||
message: Box<Message>,
|
||||
on: EventMessage,
|
||||
send: Box<Message>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct BroadcastMessageHandler {
|
||||
listeners: HashMap<BroadcastEvent, Vec<Message>>,
|
||||
event: EventMessageHandler,
|
||||
listeners: HashMap<EventMessage, Vec<Message>>,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
@@ -10,19 +11,15 @@ impl MessageHandler<BroadcastMessage, ()> for BroadcastMessageHandler {
|
||||
fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
// Sub-messages
|
||||
BroadcastMessage::TriggerEvent(event) => {
|
||||
for message in self.listeners.entry(event).or_default() {
|
||||
responses.add_front(message.clone())
|
||||
}
|
||||
}
|
||||
BroadcastMessage::TriggerEvent(message) => self.event.process_message(message, responses, EventMessageContext { listeners: &mut self.listeners }),
|
||||
|
||||
// Messages
|
||||
BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send),
|
||||
BroadcastMessage::UnsubscribeEvent { on, message } => self.listeners.entry(on).or_default().retain(|msg| *msg != *message),
|
||||
BroadcastMessage::UnsubscribeEvent { on, send } => self.listeners.entry(on).or_default().retain(|msg| *msg != *send),
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(BroadcastEventDiscriminant;)
|
||||
actions!(EventMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
|
||||
#[impl_message(Message, BroadcastMessage, TriggerEvent)]
|
||||
pub enum BroadcastEvent {
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
|
||||
pub enum EventMessage {
|
||||
/// Triggered by requestAnimationFrame in JS
|
||||
AnimationFrame,
|
||||
CanvasTransformed,
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct EventMessageContext<'a> {
|
||||
pub listeners: &'a mut HashMap<EventMessage, Vec<Message>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct EventMessageHandler {}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<EventMessage, EventMessageContext<'_>> for EventMessageHandler {
|
||||
fn process_message(&mut self, message: EventMessage, responses: &mut VecDeque<Message>, context: EventMessageContext) {
|
||||
for message in context.listeners.entry(message).or_default() {
|
||||
responses.add_front(message.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(EventMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod event_message;
|
||||
mod event_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use event_message::{EventMessage, EventMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use event_message_handler::{EventMessageContext, EventMessageHandler};
|
||||
@@ -1,7 +1,7 @@
|
||||
mod broadcast_message;
|
||||
mod broadcast_message_handler;
|
||||
|
||||
pub mod broadcast_event;
|
||||
pub mod event;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant};
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, Defer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DeferMessage {
|
||||
SetGraphSubmissionIndex(u64),
|
||||
TriggerGraphRun(u64, DocumentId),
|
||||
SetGraphSubmissionIndex { execution_id: u64 },
|
||||
TriggerGraphRun { execution_id: u64, document_id: DocumentId },
|
||||
AfterGraphRun { messages: Vec<Message> },
|
||||
TriggerNavigationReady,
|
||||
AfterNavigationReady { messages: Vec<Message> },
|
||||
|
||||
@@ -24,10 +24,10 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
|
||||
DeferMessage::AfterNavigationReady { messages } => {
|
||||
self.after_viewport_resize.extend_from_slice(&messages);
|
||||
}
|
||||
DeferMessage::SetGraphSubmissionIndex(execution_id) => {
|
||||
DeferMessage::SetGraphSubmissionIndex { execution_id } => {
|
||||
self.current_graph_submission_id = execution_id + 1;
|
||||
}
|
||||
DeferMessage::TriggerGraphRun(execution_id, document_id) => {
|
||||
DeferMessage::TriggerGraphRun { execution_id, document_id } => {
|
||||
let after_graph_run = self.after_graph_run.entry(document_id).or_default();
|
||||
if after_graph_run.is_empty() {
|
||||
return;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub enum DialogMessage {
|
||||
RequestLicensesDialogWithLocalizedCommitDate {
|
||||
localized_commit_year: String,
|
||||
},
|
||||
RequestLicensesThirdPartyDialogWithLicenseText {
|
||||
license_text: String,
|
||||
},
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::new_document_dialog::NewDocumentDialogMessageContext;
|
||||
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::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
@@ -103,6 +104,10 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
||||
let dialog = LicensesThirdPartyDialog { license_text };
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
|
||||
@@ -4,10 +4,10 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, DialogMessage, ExportDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ExportDialogMessage {
|
||||
FileType(FileType),
|
||||
ScaleFactor(f64),
|
||||
TransparentBackground(bool),
|
||||
ExportBounds(ExportBounds),
|
||||
FileType { file_type: FileType },
|
||||
ScaleFactor { factor: f64 },
|
||||
TransparentBackground { transparent: bool },
|
||||
ExportBounds { bounds: ExportBounds },
|
||||
|
||||
Submit,
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||
let ExportDialogMessageContext { portfolio } = context;
|
||||
|
||||
match message {
|
||||
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
|
||||
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
||||
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
||||
ExportDialogMessage::FileType { file_type } => self.file_type = file_type,
|
||||
ExportDialogMessage::ScaleFactor { factor } => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||
|
||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
@@ -84,24 +84,28 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
|
||||
.into_iter()
|
||||
.map(|(val, name)| RadioEntryData::new(format!("{val:?}")).label(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
|
||||
.map(|(file_type, name)| {
|
||||
RadioEntryData::new(format!("{file_type:?}"))
|
||||
.label(name)
|
||||
.on_update(move |_| ExportDialogMessage::FileType { file_type }.into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
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(),
|
||||
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
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(),
|
||||
NumberInput::new(Some(self.scale_factor))
|
||||
.unit("")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.disabled(self.file_type == FileType::Svg)
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
|
||||
.min_width(200)
|
||||
.widget_holder(),
|
||||
];
|
||||
@@ -125,10 +129,10 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
.map(|choice| {
|
||||
choice
|
||||
.into_iter()
|
||||
.map(|(val, name, disabled)| {
|
||||
MenuListEntry::new(format!("{val:?}"))
|
||||
.map(|(bounds, name, disabled)| {
|
||||
MenuListEntry::new(format!("{bounds:?}"))
|
||||
.label(name)
|
||||
.on_commit(move |_| ExportDialogMessage::ExportBounds(val).into())
|
||||
.on_commit(move |_| ExportDialogMessage::ExportBounds { bounds }.into())
|
||||
.disabled(disabled)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -140,18 +144,18 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
}
|
||||
|
||||
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(),
|
||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
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(),
|
||||
CheckboxInput::new(self.transparent_background)
|
||||
.disabled(self.file_type == FileType::Jpg)
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into())
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, DialogMessage, NewDocumentDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NewDocumentDialogMessage {
|
||||
Name(String),
|
||||
Infinite(bool),
|
||||
DimensionsX(f64),
|
||||
DimensionsY(f64),
|
||||
Name { name: String },
|
||||
Infinite { infinite: bool },
|
||||
DimensionsX { width: f64 },
|
||||
DimensionsY { height: f64 },
|
||||
|
||||
Submit,
|
||||
}
|
||||
|
||||
+11
-11
@@ -20,10 +20,10 @@ pub struct NewDocumentDialogMessageHandler {
|
||||
impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContext<'a>> for NewDocumentDialogMessageHandler {
|
||||
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, context: NewDocumentDialogMessageContext<'a>) {
|
||||
match message {
|
||||
NewDocumentDialogMessage::Name(name) => self.name = name,
|
||||
NewDocumentDialogMessage::Infinite(infinite) => self.infinite = infinite,
|
||||
NewDocumentDialogMessage::DimensionsX(x) => self.dimensions.x = x as u32,
|
||||
NewDocumentDialogMessage::DimensionsY(y) => self.dimensions.y = y as u32,
|
||||
NewDocumentDialogMessage::Name { name } => self.name = name,
|
||||
NewDocumentDialogMessage::Infinite { infinite } => self.infinite = infinite,
|
||||
NewDocumentDialogMessage::DimensionsX { width } => self.dimensions.x = width as u32,
|
||||
NewDocumentDialogMessage::DimensionsY { height } => self.dimensions.y = height as u32,
|
||||
NewDocumentDialogMessage::Submit => {
|
||||
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
|
||||
|
||||
@@ -79,26 +79,26 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
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(),
|
||||
TextInput::new(&self.name)
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into())
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
||||
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
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(),
|
||||
CheckboxInput::new(self.infinite)
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
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(),
|
||||
NumberInput::new(Some(self.dimensions.x as f64))
|
||||
.label("W")
|
||||
@@ -108,7 +108,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.is_integer(true)
|
||||
.disabled(self.infinite)
|
||||
.min_width(100)
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.dimensions.y as f64))
|
||||
@@ -119,7 +119,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.is_integer(true)
|
||||
.disabled(self.infinite)
|
||||
.min_width(100)
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY { height: number_input.value.unwrap() }.into())
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
|
||||
use crate::messages::broadcast::event::EventMessage;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -27,7 +27,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
||||
TextButton::new("Discard")
|
||||
.on_update(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -16,22 +16,31 @@ impl DialogLayoutHolder for LicensesDialog {
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md";
|
||||
let links = [
|
||||
("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"),
|
||||
("IconsGrid", "Graphite Icons", icons_license_link),
|
||||
("License", "Graphite License", "https://graphite.rs/license/"),
|
||||
("License", "Other Licenses", "/third-party-licenses.txt"),
|
||||
#[allow(clippy::type_complexity)]
|
||||
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
||||
("GraphiteLogo", "Graphite Logo", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://graphite.rs/logo/".into(),
|
||||
}
|
||||
.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
|
||||
.into_iter()
|
||||
.map(|(icon, label, url)| {
|
||||
TextButton::new(label)
|
||||
.icon(Some(icon.into()))
|
||||
.flush(true)
|
||||
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
|
||||
.widget_holder()
|
||||
})
|
||||
let widgets = button_definitions
|
||||
.iter()
|
||||
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_holder())
|
||||
.collect();
|
||||
|
||||
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 error_dialog;
|
||||
mod licenses_dialog;
|
||||
mod licenses_third_party_dialog;
|
||||
|
||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||
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 error_dialog::ErrorDialog;
|
||||
pub use licenses_dialog::LicensesDialog;
|
||||
pub use licenses_third_party_dialog::LicensesThirdPartyDialog;
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "commitDate")]
|
||||
commit_date: String,
|
||||
},
|
||||
TriggerDisplayThirdPartyLicensesDialog,
|
||||
TriggerSaveDocument {
|
||||
document_id: DocumentId,
|
||||
name: String,
|
||||
@@ -328,16 +329,18 @@ pub enum FrontendMessage {
|
||||
UpdatePlatform {
|
||||
platform: AppWindowPlatform,
|
||||
},
|
||||
UpdateMaximized {
|
||||
UpdateWindowState {
|
||||
maximized: bool,
|
||||
minimized: bool,
|
||||
},
|
||||
CloseWindow,
|
||||
UpdateViewportHolePunch {
|
||||
active: bool,
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
RenderOverlays(
|
||||
RenderOverlays {
|
||||
#[serde(skip, default = "OverlayContext::default")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayContext,
|
||||
),
|
||||
context: OverlayContext,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }),
|
||||
//
|
||||
// SelectToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt })),
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove { modifier_keys: SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt } }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control, skew: Control }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),
|
||||
@@ -178,7 +178,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control])),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove { modifier: [Alt, Shift, Control] }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
@@ -223,9 +223,9 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
||||
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }),
|
||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAll),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllSelected),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllSelected),
|
||||
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
|
||||
@@ -297,8 +297,8 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=BrushToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=BrushToolMessage::DragStop),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD) }),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD) }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=BrushToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=BrushToolMessage::Abort),
|
||||
//
|
||||
@@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
|
||||
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
||||
|
||||
@@ -3,13 +3,16 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, KeyMapping)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KeyMappingMessage {
|
||||
// Sub-messages
|
||||
#[child]
|
||||
Lookup(InputMapperMessage),
|
||||
#[child]
|
||||
ModifyMapping(MappingVariant),
|
||||
|
||||
// Messages
|
||||
ModifyMapping {
|
||||
mapping: MappingVariant,
|
||||
},
|
||||
}
|
||||
|
||||
#[impl_message(Message, KeyMappingMessage, ModifyMapping)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum MappingVariant {
|
||||
#[default]
|
||||
|
||||
@@ -19,8 +19,11 @@ impl MessageHandler<KeyMappingMessage, KeyMappingMessageContext<'_>> for KeyMapp
|
||||
let KeyMappingMessageContext { input, actions } = context;
|
||||
|
||||
match message {
|
||||
// Sub-messages
|
||||
KeyMappingMessage::Lookup(input_message) => self.mapping_handler.process_message(input_message, responses, InputMapperMessageContext { input, actions }),
|
||||
KeyMappingMessage::ModifyMapping(new_layout) => self.mapping_handler.set_mapping(new_layout.into()),
|
||||
|
||||
// Messages
|
||||
KeyMappingMessage::ModifyMapping { mapping } => self.mapping_handler.set_mapping(mapping.into()),
|
||||
}
|
||||
}
|
||||
advertise_actions!();
|
||||
|
||||
@@ -2,6 +2,6 @@ mod key_mapping_message;
|
||||
mod key_mapping_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant, MappingVariantDiscriminant};
|
||||
pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant};
|
||||
#[doc(inline)]
|
||||
pub use key_mapping_message_handler::{KeyMappingMessageContext, KeyMappingMessageHandler};
|
||||
|
||||
@@ -479,7 +479,7 @@ impl<const LENGTH: usize> Iterator for BitVectorIter<'_, LENGTH> {
|
||||
impl<const LENGTH: usize> Display for BitVector<LENGTH> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
for storage in self.0.iter().rev() {
|
||||
write!(f, "{:0width$b}", storage, width = STORAGE_SIZE_BITS)?;
|
||||
write!(f, "{storage:0STORAGE_SIZE_BITS$b}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -44,16 +44,18 @@ pub struct TextLabel {
|
||||
|
||||
pub italic: bool,
|
||||
|
||||
pub monospace: bool,
|
||||
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "centerAlign")]
|
||||
pub center_align: bool,
|
||||
|
||||
#[serde(rename = "tableAlign")]
|
||||
pub table_align: bool,
|
||||
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
pub min_width: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ pub enum Message {
|
||||
Tool(ToolMessage),
|
||||
|
||||
// Messages
|
||||
NoOp,
|
||||
Batched {
|
||||
messages: Box<[Message]>,
|
||||
},
|
||||
NoOp,
|
||||
}
|
||||
|
||||
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
|
||||
@@ -70,13 +70,11 @@ mod test {
|
||||
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
||||
// Print the current node
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
("├── ", format!("{prefix}│ "))
|
||||
} else if is_last {
|
||||
("└── ", format!("{prefix} "))
|
||||
} else {
|
||||
if is_last {
|
||||
("└── ", format!("{} ", prefix))
|
||||
} else {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
}
|
||||
("├── ", format!("{prefix}│ "))
|
||||
};
|
||||
|
||||
if tree.path().is_empty() {
|
||||
@@ -94,24 +92,38 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
// Print message field if any
|
||||
if let Some(fields) = tree.fields() {
|
||||
let len = fields.len();
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Print handler field if any
|
||||
if let Some(data) = tree.message_handler_fields() {
|
||||
let len = data.fields().len();
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
("├── ", format!("{prefix}│ "))
|
||||
} else {
|
||||
("└── ", format!("{} ", prefix))
|
||||
("└── ", format!("{prefix} "))
|
||||
};
|
||||
if data.path().is_empty() {
|
||||
file.write_all(format!("{}{}{}\n", prefix, branch, data.name()).as_bytes()).unwrap();
|
||||
} else {
|
||||
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
|
||||
}
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
|
||||
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
|
||||
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
|
||||
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
|
||||
} else if tree.name() != FRONTEND_MESSAGE_STR {
|
||||
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
|
||||
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::{Affine2, Vec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Context;
|
||||
@@ -594,6 +595,19 @@ impl TableRowLayout for DVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Vec2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Vec2"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Vec2".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for DAffine2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Transform"
|
||||
@@ -607,6 +621,20 @@ impl TableRowLayout for DAffine2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Affine2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Transform"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Transform".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
fn format_transform_matrix(transform: &DAffine2) -> String {
|
||||
let (scale, angle, translation) = transform.to_scale_angle_translation();
|
||||
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
|
||||
|
||||
@@ -53,7 +53,9 @@ pub enum DocumentMessage {
|
||||
DocumentHistoryBackward,
|
||||
DocumentHistoryForward,
|
||||
DocumentStructureChanged,
|
||||
DrawArtboardOverlays(OverlayContext),
|
||||
DrawArtboardOverlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
DuplicateSelectedLayers,
|
||||
EnterNestedNetwork {
|
||||
node_id: NodeId,
|
||||
@@ -72,9 +74,15 @@ pub enum DocumentMessage {
|
||||
open: bool,
|
||||
},
|
||||
GraphViewOverlayToggle,
|
||||
GridOptions(GridSnapping),
|
||||
GridOverlays(OverlayContext),
|
||||
GridVisibility(bool),
|
||||
GridOptions {
|
||||
options: GridSnapping,
|
||||
},
|
||||
GridOverlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
GridVisibility {
|
||||
visible: bool,
|
||||
},
|
||||
GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType,
|
||||
},
|
||||
@@ -110,6 +118,7 @@ pub enum DocumentMessage {
|
||||
RenderRulers,
|
||||
RenderScrollbars,
|
||||
SaveDocument,
|
||||
SaveDocumentAs,
|
||||
SavedDocument {
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO
|
||||
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||
@@ -85,8 +85,6 @@ pub struct DocumentMessageHandler {
|
||||
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
||||
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
||||
pub collapsed: CollapsedLayers,
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
pub name: String,
|
||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||
pub commit_hash: String,
|
||||
@@ -113,6 +111,12 @@ pub struct DocumentMessageHandler {
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
//
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
#[serde(skip)]
|
||||
pub name: String,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||
#[serde(skip)]
|
||||
breadcrumb_network_path: Vec<NodeId>,
|
||||
@@ -125,9 +129,6 @@ pub struct DocumentMessageHandler {
|
||||
/// Stack of document network snapshots for future history states.
|
||||
#[serde(skip)]
|
||||
document_redo_history: VecDeque<NodeNetworkInterface>,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
path: Option<PathBuf>,
|
||||
/// Hash of the document snapshot that was most recently saved to disk by the user.
|
||||
#[serde(skip)]
|
||||
saved_hash: Option<u64>,
|
||||
@@ -159,7 +160,6 @@ impl Default for DocumentMessageHandler {
|
||||
// ============================================
|
||||
network_interface: default_document_network_interface(),
|
||||
collapsed: CollapsedLayers::default(),
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
|
||||
document_ptz: PTZ::default(),
|
||||
document_mode: DocumentMode::DesignMode,
|
||||
@@ -172,11 +172,12 @@ impl Default for DocumentMessageHandler {
|
||||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
path: None,
|
||||
breadcrumb_network_path: Vec::new(),
|
||||
selection_network_path: Vec::new(),
|
||||
document_undo_history: VecDeque::new(),
|
||||
document_redo_history: VecDeque::new(),
|
||||
path: None,
|
||||
saved_hash: None,
|
||||
auto_saved_hash: None,
|
||||
layer_range_selection_reference: None,
|
||||
@@ -391,7 +392,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
}
|
||||
}
|
||||
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
|
||||
DocumentMessage::DrawArtboardOverlays { context: overlay_context } => {
|
||||
if !overlay_context.visibility_settings.artboard_name() {
|
||||
return;
|
||||
}
|
||||
@@ -588,19 +589,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
DocumentMessage::GraphViewOverlayToggle => {
|
||||
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
|
||||
}
|
||||
DocumentMessage::GridOptions(grid) => {
|
||||
self.snapping_state.grid = grid;
|
||||
DocumentMessage::GridOptions { options } => {
|
||||
self.snapping_state.grid = options;
|
||||
self.snapping_state.grid_snapping = true;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
DocumentMessage::GridOverlays(mut overlay_context) => {
|
||||
DocumentMessage::GridOverlays { context: mut overlay_context } => {
|
||||
if self.snapping_state.grid_snapping {
|
||||
grid_overlay(self, &mut overlay_context)
|
||||
}
|
||||
}
|
||||
DocumentMessage::GridVisibility(enabled) => {
|
||||
self.snapping_state.grid_snapping = enabled;
|
||||
DocumentMessage::GridVisibility { visible } => {
|
||||
self.snapping_state.grid_snapping = visible;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
|
||||
@@ -947,7 +948,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::RenameDocument { new_name } => {
|
||||
self.name = new_name;
|
||||
self.name = new_name.clone();
|
||||
|
||||
self.path = None;
|
||||
self.set_save_state(false);
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
@@ -1020,25 +1025,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
multiplier: scrollbar_multiplier.into(),
|
||||
});
|
||||
}
|
||||
DocumentMessage::SaveDocument => {
|
||||
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
|
||||
if let DocumentMessage::SaveDocumentAs = message {
|
||||
self.path = None;
|
||||
}
|
||||
|
||||
self.set_save_state(true);
|
||||
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
||||
// Update the save status of the just saved document
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
|
||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => self.name.clone(),
|
||||
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
||||
};
|
||||
responses.add(FrontendMessage::TriggerSaveDocument {
|
||||
document_id,
|
||||
name,
|
||||
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION),
|
||||
path: self.path.clone(),
|
||||
content: self.serialize_document().into_bytes(),
|
||||
})
|
||||
}
|
||||
DocumentMessage::SavedDocument { path } => {
|
||||
self.path = path;
|
||||
|
||||
// Update the name to match the file stem
|
||||
let document_name_from_path = self.path.as_ref().and_then(|path| {
|
||||
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(name) = document_name_from_path {
|
||||
self.name = name;
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectParentLayer => {
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
@@ -1062,7 +1082,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
if !parent_layers.is_empty() {
|
||||
let nodes = parent_layers.into_iter().collect();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectAllLayers => {
|
||||
@@ -1137,7 +1157,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
} else {
|
||||
responses.add_front(NodeGraphMessage::SelectedNodesAdd { nodes: vec![id] });
|
||||
}
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
} else {
|
||||
nodes.push(id);
|
||||
}
|
||||
@@ -1206,7 +1226,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
Some(overlays_type) => overlays_type,
|
||||
None => {
|
||||
visibility_settings.all = visible;
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
return;
|
||||
}
|
||||
@@ -1229,7 +1249,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
OverlaysType::Handles => visibility_settings.handles = visible,
|
||||
}
|
||||
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::SetRangeSelectionLayer { new_layer } => {
|
||||
@@ -1443,12 +1463,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
let transform = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
self.network_interface.set_document_to_viewport_transform(transform);
|
||||
// Ensure selection box is kept in sync with the pointer when the PTZ changes
|
||||
responses.add(SelectToolMessage::PointerMove(SelectToolPointerKeys {
|
||||
axis_align: Key::Shift,
|
||||
snap_angle: Key::Shift,
|
||||
center: Key::Alt,
|
||||
duplicate: Key::Alt,
|
||||
}));
|
||||
responses.add(SelectToolMessage::PointerMove {
|
||||
modifier_keys: SelectToolPointerKeys {
|
||||
axis_align: Key::Shift,
|
||||
snap_angle: Key::Shift,
|
||||
center: Key::Alt,
|
||||
duplicate: Key::Alt,
|
||||
},
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
} else {
|
||||
let Some(network_metadata) = self.network_interface.network_metadata(&self.breadcrumb_network_path) else {
|
||||
@@ -1477,11 +1499,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
}
|
||||
DocumentMessage::SelectionStepBack => {
|
||||
self.network_interface.selection_step_back(&self.selection_network_path);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
DocumentMessage::SelectionStepForward => {
|
||||
self.network_interface.selection_step_forward(&self.selection_network_path);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
|
||||
// Get bounding box of all layers
|
||||
@@ -1569,6 +1591,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
ZoomCanvasToFitAll,
|
||||
);
|
||||
|
||||
// Additional actions available on desktop
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs));
|
||||
|
||||
// Additional actions if there are any selected layers
|
||||
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
|
||||
let mut select = actions!(DocumentMessageDiscriminant;
|
||||
@@ -1756,7 +1782,8 @@ impl DocumentMessageHandler {
|
||||
|
||||
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
|
||||
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
|
||||
.or_else(|_| {
|
||||
.or_else(|e| {
|
||||
log::warn!("failed to directly load document with the following error: {e}. Trying old DocumentMessageHandler");
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldDocumentMessageHandler {
|
||||
@@ -2484,7 +2511,7 @@ impl DocumentMessageHandler {
|
||||
.icon("Grid")
|
||||
.tooltip("Grid")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleGridVisibility))
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility(optional_input.checked).into())
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility { visible: optional_input.checked }.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(overlay_options(&self.snapping_state.grid))
|
||||
|
||||
@@ -91,6 +91,32 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
|
||||
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
|
||||
}
|
||||
|
||||
/// Expand a bounds to avoid div zero errors
|
||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
/// Returns corners of all subpaths
|
||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box())
|
||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
||||
clamp_bounds(bounds_min, bounds_max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -138,29 +164,3 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand a bounds to avoid div zero errors
|
||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
/// Returns corners of all subpaths
|
||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box())
|
||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
||||
clamp_bounds(bounds_min, bounds_max)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta);
|
||||
|
||||
ptz.pan += transformed_delta;
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
}
|
||||
NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => {
|
||||
@@ -286,7 +286,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
ptz.flip = !ptz.flip;
|
||||
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
@@ -325,7 +325,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
self.navigation_operation = NavigationOperation::None;
|
||||
|
||||
// Send the final messages to close out the operation
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(ToolMessage::UpdateCursor);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NavigateToolMessage::End);
|
||||
|
||||
@@ -46,7 +46,7 @@ impl NodePropertiesContext<'_> {
|
||||
return None;
|
||||
};
|
||||
widget_override_lambda(*node_id, index, self)
|
||||
.map_err(|error| log::error!("Error in widget override lambda: {}", error))
|
||||
.map_err(|error| log::error!("Error in widget override lambda: {error}"))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
@@ -111,7 +111,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::None, true)],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -151,19 +151,19 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -233,33 +233,33 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_graphic::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// Secondary (left) input type coercion
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 1)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// The monitor node is used to display a thumbnail in the UI
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -349,7 +349,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: [
|
||||
// Ensure this ID is kept in sync with the ID in set_alias so that the name input is kept in sync with the alias
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
|
||||
inputs: vec![
|
||||
NodeInput::network(concrete!(TaggedValue), 1),
|
||||
@@ -365,7 +365,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// The monitor node is used to display a thumbnail in the UI.
|
||||
@@ -373,12 +373,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![
|
||||
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
|
||||
NodeInput::node(NodeId(2), 0),
|
||||
@@ -495,13 +495,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::network(concrete!(String), 1)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::load_resource::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::decode_image::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -568,7 +568,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -630,20 +630,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::scope("editor-api")],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::create_surface::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0), NodeInput::network(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::rasterize::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -716,7 +716,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
category: "Raster: Pattern",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::std_nodes::noise_pattern::IDENTIFIER),
|
||||
inputs: vec![
|
||||
NodeInput::value(TaggedValue::None, false),
|
||||
@@ -783,7 +783,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -792,7 +792,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -801,7 +801,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -810,7 +810,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -888,13 +888,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -962,7 +962,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(Vec<brush::brush_stroke::BrushStroke>), 1),
|
||||
NodeInput::network(concrete!(BrushCache), 2),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER),
|
||||
..Default::default()
|
||||
}]
|
||||
@@ -1013,7 +1013,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
@@ -1032,7 +1032,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
@@ -1054,13 +1054,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::scope("editor-api")],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::create_gpu_surface::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -1126,12 +1126,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::node(NodeId(0), 0)],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -1247,7 +1247,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1257,7 +1257,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(graphene_std::vector::VectorModification), 1),
|
||||
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
|
||||
],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::path_modify::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1317,7 +1317,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(text::text::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![
|
||||
NodeInput::scope("editor-api"),
|
||||
NodeInput::value(TaggedValue::String("Lorem ipsum".to_string()), false),
|
||||
@@ -1427,7 +1427,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1439,7 +1439,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(DVec2), 3),
|
||||
NodeInput::network(concrete!(DVec2), 4),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1524,25 +1524,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(path_bool::boolean_operation::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1622,7 +1622,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::subpath_segment_lengths::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -1637,25 +1637,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::node(NodeId(0), 0),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::sample_polyline::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1791,26 +1791,26 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(f64), 1),
|
||||
NodeInput::network(concrete!(u32), 2),
|
||||
],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::poisson_disk_points::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1944,10 +1944,10 @@ fn static_input_properties() -> InputProperties {
|
||||
"string".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
let Some(value) = context.network_interface.input_data(&node_id, index, "string_properties", context.selection_network_path) else {
|
||||
return Err(format!("Could not get string properties for node {}", node_id));
|
||||
return Err(format!("Could not get string properties for node {node_id}"));
|
||||
};
|
||||
let Some(string) = value.as_str() else {
|
||||
return Err(format!("Could not downcast string properties for node {}", node_id));
|
||||
return Err(format!("Could not downcast string properties for node {node_id}"));
|
||||
};
|
||||
Ok(node_properties::string_properties(string))
|
||||
}),
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
|
||||
call_argument: (input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
NodeGraphMessage::AddPathNode => {
|
||||
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) {
|
||||
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::AddImport => {
|
||||
@@ -142,7 +142,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
NodeGraphMessage::Init => {
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
on: EventMessage::SelectionChanged,
|
||||
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
|
||||
});
|
||||
network_interface.load_structure();
|
||||
@@ -1472,7 +1472,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.add_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesRemove { nodes } => {
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
|
||||
@@ -1480,7 +1480,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.retain_selected_nodes(|node| !nodes.contains(node));
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesSet { nodes } => {
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
|
||||
@@ -1488,7 +1488,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.set_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
|
||||
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
|
||||
@@ -1516,7 +1516,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
let mut nodes = Vec::new();
|
||||
for node_id in &self.frontend_nodes {
|
||||
let Some(node_bbox) = network_interface.node_bounding_box(node_id, breadcrumb_network_path) else {
|
||||
log::error!("Could not get bbox for node: {:?}", node_id);
|
||||
log::error!("Could not get bbox for node: {node_id:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1721,7 +1721,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
NodeGraphMessage::ToggleLocked { node_id } => {
|
||||
let Some(node_metadata) = network_interface.document_network_metadata().persistent_metadata.node_metadata.get(&node_id) else {
|
||||
log::error!("Cannot get node {:?} in NodeGraphMessage::ToggleLocked", node_id);
|
||||
log::error!("Cannot get node {node_id:?} in NodeGraphMessage::ToggleLocked");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1888,7 +1888,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
@@ -2486,7 +2486,7 @@ impl NodeGraphMessageHandler {
|
||||
data_type: frontend_data_type,
|
||||
name: "Output 1".to_string(),
|
||||
description: String::new(),
|
||||
resolved_type: format!("{:?}", output_type),
|
||||
resolved_type: format!("{output_type:?}"),
|
||||
connected_to,
|
||||
})
|
||||
} else {
|
||||
@@ -2518,7 +2518,7 @@ impl NodeGraphMessageHandler {
|
||||
data_type,
|
||||
name: output_name,
|
||||
description: String::new(),
|
||||
resolved_type: format!("{:?}", output_type),
|
||||
resolved_type: format!("{output_type:?}"),
|
||||
connected_to,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,9 +147,9 @@ pub(crate) fn property_from_type(
|
||||
Type::Concrete(concrete_type) => {
|
||||
match concrete_type.alias.as_ref().map(|x| x.as_ref()) {
|
||||
// Aliased types (ambiguous values)
|
||||
Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
||||
Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
||||
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
|
||||
Some("Percentage") | Some("PercentageF32") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
||||
Some("SignedPercentage") | Some("SignedPercentageF32") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
||||
Some("Angle") | Some("AngleF32") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
|
||||
Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(),
|
||||
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
|
||||
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
|
||||
@@ -166,7 +166,7 @@ pub(crate) fn property_from_type(
|
||||
// ===============
|
||||
// PRIMITIVE TYPES
|
||||
// ===============
|
||||
Some(x) if x == TypeId::of::<f64>() => number_widget(default_info, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY))).into(),
|
||||
Some(x) if x == TypeId::of::<f64>() || x == TypeId::of::<f32>() => number_widget(default_info, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY))).into(),
|
||||
Some(x) if x == TypeId::of::<u32>() => number_widget(default_info, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX)))).into(),
|
||||
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
|
||||
@@ -805,6 +805,14 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props:
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::F32(x)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
.value(Some(x as f64))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F32(x.value.unwrap() as f32), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::U32(x)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
@@ -851,6 +859,15 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props:
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::FVec2(vec2)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
// We use an arbitrary `y` instead of an arbitrary `x` here because the "Grid" node's "Spacing" value's height should be used from rectangular mode when transferred to "Y Spacing" in isometric mode
|
||||
.value(Some(vec2.y as f64))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F32(x.value.unwrap() as f32), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ impl FrontendGraphDataType {
|
||||
match TaggedValue::from_type_or_none(input) {
|
||||
TaggedValue::U32(_)
|
||||
| TaggedValue::U64(_)
|
||||
| TaggedValue::F32(_)
|
||||
| TaggedValue::F64(_)
|
||||
| TaggedValue::DVec2(_)
|
||||
| TaggedValue::F64Array4(_)
|
||||
|
||||
@@ -200,7 +200,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||
move |input: &I| {
|
||||
let mut grid = grid.clone();
|
||||
update(&mut grid, input);
|
||||
DocumentMessage::GridOptions(grid).into()
|
||||
DocumentMessage::GridOptions { options: grid }.into()
|
||||
}
|
||||
}
|
||||
let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| {
|
||||
|
||||
@@ -7,14 +7,14 @@ use crate::messages::prelude::*;
|
||||
pub enum OverlaysMessage {
|
||||
Draw,
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
AddProvider(
|
||||
AddProvider {
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
RemoveProvider(
|
||||
provider: OverlayProvider,
|
||||
},
|
||||
RemoveProvider {
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
provider: OverlayProvider,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,12 +56,14 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
let _ = canvas_context.reset_transform();
|
||||
|
||||
if visibility_settings.all() {
|
||||
responses.add(DocumentMessage::GridOverlays(OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings: visibility_settings.clone(),
|
||||
}));
|
||||
responses.add(DocumentMessage::GridOverlays {
|
||||
context: OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings: visibility_settings.clone(),
|
||||
},
|
||||
});
|
||||
for provider in &self.overlay_providers {
|
||||
responses.add(provider(OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
@@ -81,22 +83,22 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
|
||||
|
||||
if visibility_settings.all() {
|
||||
responses.add(DocumentMessage::GridOverlays(overlay_context.clone()));
|
||||
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
|
||||
|
||||
for provider in &self.overlay_providers {
|
||||
responses.add(provider(overlay_context.clone()));
|
||||
}
|
||||
}
|
||||
responses.add(FrontendMessage::RenderOverlays(overlay_context));
|
||||
responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
|
||||
}
|
||||
#[cfg(all(not(target_family = "wasm"), test))]
|
||||
OverlaysMessage::Draw => {
|
||||
let _ = (responses, visibility_settings, ipp, device_pixel_ratio);
|
||||
}
|
||||
OverlaysMessage::AddProvider(message) => {
|
||||
OverlaysMessage::AddProvider { provider: message } => {
|
||||
self.overlay_providers.insert(message);
|
||||
}
|
||||
OverlaysMessage::RemoveProvider(message) => {
|
||||
OverlaysMessage::RemoveProvider { provider: message } => {
|
||||
self.overlay_providers.remove(&message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use super::utility_types::{DrawHandles, OverlayContext};
|
||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::{PointId, SegmentId};
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
|
||||
@@ -24,33 +26,40 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
|
||||
create_context().expect("Failed to get canvas context")
|
||||
}
|
||||
|
||||
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> Vec<SegmentId> {
|
||||
let selected_points = shape_editor.selected_points();
|
||||
let selected_anchors = selected_points
|
||||
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
|
||||
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> HashMap<LayerNodeIdentifier, Vec<SegmentId>> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for (layer, state) in &shape_editor.selected_shape_state {
|
||||
let Some(vector) = network_interface.compute_modified_vector(*layer) else { continue };
|
||||
let selected_segments = selected_segments_for_layer(&vector, state);
|
||||
|
||||
map.insert(*layer, selected_segments);
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState) -> Vec<SegmentId> {
|
||||
let selected_anchors = state
|
||||
.selected_points()
|
||||
.filter_map(|point| if let ManipulatorPointId::Anchor(p) = point { Some(p) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Collect the segments whose handles are selected
|
||||
let mut selected_segments = shape_editor
|
||||
let mut selected_segments = state
|
||||
.selected_points()
|
||||
.filter_map(|point_id| match point_id {
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(*segment_id),
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(segment_id),
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: Currently if there are two duplicate layers, both of their segments get overlays
|
||||
// Adding segments which are are connected to selected anchors
|
||||
for layer in network_interface.selected_nodes().selected_layers(network_interface.document_metadata()) {
|
||||
let Some(vector) = network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||
selected_segments.push(segment_id);
|
||||
}
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||
selected_segments.push(segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
selected_segments
|
||||
}
|
||||
|
||||
@@ -124,22 +133,21 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
overlay_context.outline_vector(&vector, transform);
|
||||
}
|
||||
|
||||
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the selected segments and then add a bold line overlay on them
|
||||
for (segment_id, bezier, _, _) in vector.segment_iter() {
|
||||
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if selected_shape_state.is_segment_selected(segment_id) {
|
||||
overlay_context.outline_select_bezier(bezier, transform);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
||||
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
||||
let is_selected = |point: ManipulatorPointId| selected_shape_state.is_point_selected(point);
|
||||
|
||||
if display_handles {
|
||||
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector.adjacent_segment(point_id)).collect();
|
||||
let opposite_handles_data = selected_shape_state.selected_points().filter_map(|point_id| vector.adjacent_segment(&point_id)).collect::<Vec<_>>();
|
||||
|
||||
match draw_handles {
|
||||
DrawHandles::All => {
|
||||
@@ -148,9 +156,11 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
});
|
||||
}
|
||||
DrawHandles::SelectedAnchors(ref selected_segments) => {
|
||||
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.filter(|(segment_id, ..)| selected_segments.contains(segment_id))
|
||||
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
|
||||
.for_each(|(segment_id, bezier, _start, _end)| {
|
||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
||||
});
|
||||
@@ -161,7 +171,9 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawHandles::FrontierHandles(ref segment_endpoints) => {
|
||||
DrawHandles::FrontierHandles(ref segment_endpoints_by_layer) => {
|
||||
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::consts::{
|
||||
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
@@ -1024,7 +1025,7 @@ pub enum Pivot {
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
|
||||
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::consts::{
|
||||
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
@@ -400,8 +401,8 @@ pub enum Pivot {
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
|
||||
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -1021,6 +1022,8 @@ impl OverlayContextInternal {
|
||||
};
|
||||
|
||||
// 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");
|
||||
let font_blob = Some(load_font(FONT_DATA));
|
||||
|
||||
@@ -1046,6 +1049,8 @@ impl OverlayContextInternal {
|
||||
};
|
||||
|
||||
// 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");
|
||||
let font_blob = Some(load_font(FONT_DATA));
|
||||
|
||||
@@ -1152,7 +1157,7 @@ impl OverlayContextInternal {
|
||||
let move_to = last_point != Some(start_id);
|
||||
last_point = Some(end_id);
|
||||
|
||||
self.bezier_to_path(bezier, row.transform.clone(), move_to, &mut path);
|
||||
self.bezier_to_path(bezier, *row.transform, move_to, &mut path);
|
||||
}
|
||||
|
||||
// Render the path
|
||||
|
||||
@@ -161,6 +161,17 @@ impl DocumentMetadata {
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Get the loose bounding box of the click target of the specified layer in the specified transform space
|
||||
pub fn loose_bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.click_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| match click_target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
|
||||
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Calculate the corners of the bounding box but with a nonzero size.
|
||||
///
|
||||
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
|
||||
|
||||
@@ -25,6 +25,7 @@ use kurbo::BezPath;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference.
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
@@ -73,10 +74,8 @@ impl NodeNetworkInterface {
|
||||
fix_network(network);
|
||||
}
|
||||
if let DocumentNodeImplementation::ProtoNode(protonode) = &node.implementation {
|
||||
if protonode.name.contains("PathModifyNode") {
|
||||
if node.inputs.len() < 3 {
|
||||
node.inputs.push(NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath));
|
||||
}
|
||||
if protonode.name.contains("PathModifyNode") && node.inputs.len() < 3 {
|
||||
node.inputs.push(NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,13 +459,9 @@ impl NodeNetworkInterface {
|
||||
/// If the node is not in the hashmap then a default input is found based on the compiled network, using the node_id passed as a parameter
|
||||
pub fn map_ids(&mut self, mut node_template: NodeTemplate, node_id: &NodeId, new_ids: &HashMap<NodeId, NodeId>, network_path: &[NodeId]) -> NodeTemplate {
|
||||
for (input_index, input) in node_template.document_node.inputs.iter_mut().enumerate() {
|
||||
if let &mut NodeInput::Node { node_id: id, output_index, lambda } = input {
|
||||
if let &mut NodeInput::Node { node_id: id, output_index } = input {
|
||||
if let Some(&new_id) = new_ids.get(&id) {
|
||||
*input = NodeInput::Node {
|
||||
node_id: new_id,
|
||||
output_index,
|
||||
lambda,
|
||||
};
|
||||
*input = NodeInput::Node { node_id: new_id, output_index };
|
||||
} else {
|
||||
// Disconnect node input if it is not connected to another node in new_ids
|
||||
let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0);
|
||||
@@ -547,12 +542,11 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(_) => {
|
||||
// If a node has manual composition, then offset the input index by 1 since the proto node also includes the type of the input passed through manual composition.
|
||||
let manual_composition_offset = if node.manual_composition.is_some() { 1 } else { 0 };
|
||||
// Offset the input index by 1 since the proto node also includes the type of the input passed as a call argument.
|
||||
self.resolved_types
|
||||
.types
|
||||
.get(node_id_path.as_slice())
|
||||
.and_then(|node_types| node_types.inputs.get(input_index + manual_composition_offset).cloned())
|
||||
.and_then(|node_types| node_types.inputs.get(input_index + 1).cloned())
|
||||
.map(|node_types| (node_types, TypeSource::Compiled))
|
||||
}
|
||||
DocumentNodeImplementation::Extract => None,
|
||||
@@ -581,7 +575,7 @@ impl NodeNetworkInterface {
|
||||
return (concrete!(()), TypeSource::Error("could not resolve protonode"));
|
||||
};
|
||||
|
||||
let skip_footprint = if node.manual_composition.is_some() { 1 } else { 0 };
|
||||
let skip_footprint = 1;
|
||||
|
||||
let Some(input_type) = std::iter::once(node_types.call_argument.clone()).chain(node_types.inputs.clone()).nth(input_index + skip_footprint) else {
|
||||
log::error!("Could not get type");
|
||||
@@ -821,7 +815,7 @@ impl NodeNetworkInterface {
|
||||
data_type,
|
||||
name,
|
||||
description,
|
||||
resolved_type: format!("{:?}", input_type),
|
||||
resolved_type: format!("{input_type:?}"),
|
||||
connected_to,
|
||||
},
|
||||
click_target,
|
||||
@@ -1069,7 +1063,7 @@ impl NodeNetworkInterface {
|
||||
|
||||
pub fn reference(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&Option<String>> {
|
||||
let Some(node_metadata) = self.node_metadata(node_id, network_path) else {
|
||||
log::error!("Could not get reference for node: {:?}", node_id);
|
||||
log::error!("Could not get reference for node: {node_id:?}");
|
||||
return None;
|
||||
};
|
||||
Some(&node_metadata.persistent_metadata.reference)
|
||||
@@ -1287,7 +1281,7 @@ impl NodeNetworkInterface {
|
||||
let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]);
|
||||
let clip_input = artboard.unwrap().inputs.get(5).unwrap();
|
||||
if let NodeInput::Value { tagged_value, .. } = clip_input {
|
||||
if tagged_value.clone().into_inner() == TaggedValue::Bool(true) {
|
||||
if tagged_value.clone().deref() == &TaggedValue::Bool(true) {
|
||||
return Some(Quad::clip(
|
||||
self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
|
||||
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
|
||||
@@ -1499,7 +1493,7 @@ impl NodeNetworkInterface {
|
||||
let mut node_metadata = DocumentNodeMetadata::default();
|
||||
|
||||
node.inputs = old_node.inputs;
|
||||
node.manual_composition = old_node.manual_composition;
|
||||
node.call_argument = old_node.manual_composition.unwrap();
|
||||
node.visible = old_node.visible;
|
||||
node.skip_deduplication = old_node.skip_deduplication;
|
||||
node.original_location = old_node.original_location;
|
||||
@@ -2522,7 +2516,7 @@ impl NodeNetworkInterface {
|
||||
InputConnector::Node { node_id, input_index } => {
|
||||
let input_metadata = self.transient_input_metadata(node_id, *input_index, network_path)?;
|
||||
let TransientMetadata::Loaded(wire) = &input_metadata.wire else {
|
||||
log::error!("Could not load wire for input: {:?}", input);
|
||||
log::error!("Could not load wire for input: {input:?}");
|
||||
return None;
|
||||
};
|
||||
wire.clone()
|
||||
@@ -2530,7 +2524,7 @@ impl NodeNetworkInterface {
|
||||
InputConnector::Export(export_index) => {
|
||||
let network_metadata = self.network_metadata(network_path)?;
|
||||
let Some(TransientMetadata::Loaded(wire)) = network_metadata.transient_metadata.wires.get(*export_index) else {
|
||||
log::error!("Could not load wire for input: {:?}", input);
|
||||
log::error!("Could not load wire for input: {input:?}");
|
||||
return None;
|
||||
};
|
||||
wire.clone()
|
||||
@@ -2701,12 +2695,12 @@ impl NodeNetworkInterface {
|
||||
return None;
|
||||
}
|
||||
let Some(input_position) = self.get_input_center(&input, network_path) else {
|
||||
log::error!("Could not get dom rect for wire end in root node: {:?}", input);
|
||||
log::error!("Could not get dom rect for wire end in root node: {input:?}");
|
||||
return None;
|
||||
};
|
||||
let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index);
|
||||
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
|
||||
log::error!("Could not get dom rect for wire start in root node: {:?}", upstream_output);
|
||||
log::error!("Could not get dom rect for wire start in root node: {upstream_output:?}");
|
||||
return None;
|
||||
};
|
||||
let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0);
|
||||
@@ -2733,7 +2727,7 @@ impl NodeNetworkInterface {
|
||||
/// Returns the vector subpath and a boolean of whether the wire should be thick.
|
||||
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> {
|
||||
let Some(input_position) = self.get_input_center(input, network_path) else {
|
||||
log::error!("Could not get dom rect for wire end: {:?}", input);
|
||||
log::error!("Could not get dom rect for wire end: {input:?}");
|
||||
return None;
|
||||
};
|
||||
// An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector
|
||||
@@ -2741,7 +2735,7 @@ impl NodeNetworkInterface {
|
||||
return Some((BezPath::new(), false));
|
||||
};
|
||||
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
|
||||
log::error!("Could not get dom rect for wire start: {:?}", upstream_output);
|
||||
log::error!("Could not get dom rect for wire start: {upstream_output:?}");
|
||||
return None;
|
||||
};
|
||||
let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0);
|
||||
@@ -3357,7 +3351,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]))
|
||||
.filter(|node| self.is_layer(node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
@@ -3366,7 +3360,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_locked(&node, &[]))
|
||||
.filter(|node| self.is_layer(node, &[]) && !self.is_locked(node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
@@ -4138,7 +4132,7 @@ impl NodeNetworkInterface {
|
||||
if let DocumentNodeImplementation::Network(network) = &node.implementation {
|
||||
let number_of_exports = network.exports.len();
|
||||
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get metadata for node: {:?}", node_id);
|
||||
log::error!("Could not get metadata for node: {node_id:?}");
|
||||
return;
|
||||
};
|
||||
metadata.persistent_metadata.output_names.resize(number_of_exports, "".to_string());
|
||||
@@ -4155,7 +4149,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
|
||||
pub fn set_manual_compostion(&mut self, node_id: &NodeId, network_path: &[NodeId], manual_composition: Option<Type>) {
|
||||
pub fn set_call_argument(&mut self, node_id: &NodeId, network_path: &[NodeId], call_argument: Type) {
|
||||
let Some(network) = self.network_mut(network_path) else {
|
||||
log::error!("Could not get nested network in set_implementation");
|
||||
return;
|
||||
@@ -4164,7 +4158,7 @@ impl NodeNetworkInterface {
|
||||
log::error!("Could not get node in set_implementation");
|
||||
return;
|
||||
};
|
||||
node.manual_composition = manual_composition;
|
||||
node.call_argument = call_argument;
|
||||
}
|
||||
|
||||
pub fn set_input(&mut self, input_connector: &InputConnector, new_input: NodeInput, network_path: &[NodeId]) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user