mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-22 14:04:05 +00:00
Desktop: Implement desktop wrapper module (#3039)
* Prototyping desktop wrapper api * Separate into multiple modules * Some fixup * Reimplement most functionality with editor api * Fix texture life time crashes * Fix scale * Implement editor wrapper message queue * Improve performance * Handle native messages directly without submitting to event loop * Fix overlay latency * Move editor message execution to executor allows no shared state in editor wrapper * Small clean up * Small cleanup * Some renames * Cleaning up desktop wrapper interface * Fix formatting * Fix naming * Move node graph execution result handling to app * Fix FrontendMessage RenderOverlays usage * Reimplement file drop and clean up file import and open messages * Remove dbg * Post merge fix * Review changes
This commit is contained in:
parent
a70c48f69a
commit
30e5d66105
14 changed files with 558 additions and 265 deletions
|
@ -1,17 +1,14 @@
|
|||
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::desktop_wrapper::DesktopWrapper;
|
||||
use crate::desktop_wrapper::NodeGraphExecutionResult;
|
||||
use crate::desktop_wrapper::WgpuContext;
|
||||
use crate::desktop_wrapper::messages::DesktopFrontendMessage;
|
||||
use crate::desktop_wrapper::messages::DesktopWrapperMessage;
|
||||
use crate::desktop_wrapper::serialize_frontend_messages;
|
||||
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::messages::prelude::*;
|
||||
use std::fs;
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::thread;
|
||||
|
@ -37,11 +34,12 @@ pub(crate) struct WinitApp {
|
|||
graphics_state: Option<GraphicsState>,
|
||||
wgpu_context: WgpuContext,
|
||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
||||
editor: Editor,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
}
|
||||
|
||||
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 {
|
||||
let desktop_wrapper = DesktopWrapper::new();
|
||||
Self {
|
||||
cef_context,
|
||||
window: None,
|
||||
|
@ -50,97 +48,106 @@ impl WinitApp {
|
|||
window_size_sender,
|
||||
wgpu_context,
|
||||
event_loop_proxy,
|
||||
editor: Editor::new(),
|
||||
desktop_wrapper,
|
||||
}
|
||||
}
|
||||
|
||||
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 { context: 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.cef_context.send_web_message(bytes.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
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: None,
|
||||
document_path: Some(path),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
let Ok(message) = ron::to_string(&responses) else {
|
||||
tracing::error!("Failed to serialize Messages");
|
||||
return;
|
||||
};
|
||||
self.cef_context.send_web_message(message.as_bytes());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,13 +201,25 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||
|
||||
tracing::info!("Winit window created and ready");
|
||||
|
||||
let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone());
|
||||
|
||||
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
|
||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||
}
|
||||
|
||||
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
|
||||
match event {
|
||||
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());
|
||||
|
@ -217,50 +236,6 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,76 +243,6 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||
let Some(event) = self.cef_context.handle_window_event(event) else { return };
|
||||
|
||||
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: None,
|
||||
document_path: Some(path),
|
||||
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();
|
||||
|
@ -362,6 +267,19 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||
Err(e) => tracing::error!("{:?}", e),
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{CustomEvent, WgpuContext, render::FrameBufferRef};
|
||||
use std::{
|
||||
sync::{Arc, Mutex, mpsc::Receiver},
|
||||
time::Instant,
|
||||
};
|
||||
use crate::desktop_wrapper::WgpuContext;
|
||||
use crate::render::FrameBufferRef;
|
||||
use crate::{CustomEvent, desktop_wrapper::deserialize_editor_message};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
mod context;
|
||||
mod dirs;
|
||||
|
@ -121,14 +122,10 @@ impl CefEventHandler for CefHandler {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize message {:?}", e)
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
69
desktop/src/desktop_wrapper.rs
Normal file
69
desktop/src/desktop_wrapper.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::messages::prelude::{FrontendMessage, Message};
|
||||
|
||||
pub use wgpu_executor::Context as WgpuContext;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
110
desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs
Normal file
110
desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::messages::prelude::{DocumentMessage, PortfolioMessage};
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
23
desktop/src/desktop_wrapper/intercept_editor_message.rs
Normal file
23
desktop/src/desktop_wrapper/intercept_editor_message.rs
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
70
desktop/src/desktop_wrapper/intercept_frontend_message.rs
Normal file
70
desktop/src/desktop_wrapper/intercept_frontend_message.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
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));
|
||||
}
|
||||
m => return Some(m),
|
||||
}
|
||||
None
|
||||
}
|
76
desktop/src/desktop_wrapper/message_dispatcher.rs
Normal file
76
desktop/src/desktop_wrapper/message_dispatcher.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
60
desktop/src/desktop_wrapper/messages.rs
Normal file
60
desktop/src/desktop_wrapper/messages.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
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),
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
pub enum OpenFileDialogContext {
|
||||
Document,
|
||||
Import,
|
||||
}
|
||||
|
||||
pub enum SaveFileDialogContext {
|
||||
Document { document_id: DocumentId, content: Vec<u8> },
|
||||
File { content: Vec<u8> },
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
use std::process::exit;
|
||||
use std::time::Instant;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
use graphite_editor::messages::prelude::Message;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
|
@ -12,22 +9,21 @@ mod cef;
|
|||
use cef::{Setup, WindowSize};
|
||||
|
||||
mod render;
|
||||
use render::WgpuContext;
|
||||
|
||||
mod app;
|
||||
use app::WinitApp;
|
||||
|
||||
mod dirs;
|
||||
|
||||
mod dialogs;
|
||||
mod desktop_wrapper;
|
||||
use desktop_wrapper::messages::DesktopWrapperMessage;
|
||||
use desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CustomEvent {
|
||||
UiUpdate(wgpu::Texture),
|
||||
ScheduleBrowserWork(Instant),
|
||||
DispatchMessage(Message),
|
||||
MessageReceived(Message),
|
||||
NodeGraphRan(Option<wgpu::Texture>),
|
||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -46,7 +42,7 @@ fn main() {
|
|||
|
||||
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
|
||||
let wgpu_context = futures::executor::block_on(desktop_wrapper::WgpuContext::new()).unwrap();
|
||||
let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) {
|
||||
Ok(c) => c,
|
||||
Err(cef::InitError::AlreadyRunning) => {
|
||||
|
@ -66,10 +62,10 @@ fn main() {
|
|||
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 result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
||||
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
|
||||
|
||||
let frame_time = Duration::from_secs_f32((target_fps as f32).recip());
|
||||
let sleep = last_render + frame_time - Instant::now();
|
||||
std::thread::sleep(sleep);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use wgpu_executor::WgpuExecutor;
|
||||
use winit::window::Window;
|
||||
|
||||
pub(crate) use wgpu_executor::Context as WgpuContext;
|
||||
use crate::desktop_wrapper::WgpuContext;
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
|
|
|
@ -52,7 +52,7 @@ impl Size for web_sys::HtmlCanvasElement {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub texture: Arc<wgpu::Texture>,
|
||||
pub texture: wgpu::Texture,
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
pub texture: (),
|
||||
}
|
||||
|
|
|
@ -209,7 +209,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl Render, editor: &
|
|||
.await
|
||||
.expect("Failed to render Vello scene");
|
||||
|
||||
RenderOutputType::Texture(ImageTexture { texture: Arc::new(texture) })
|
||||
RenderOutputType::Texture(ImageTexture { texture })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue