mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 00:28:12 +08:00
Desktop: Move wrapper to separate crate (#3073)
Move desktop wrapper to separate crate
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -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,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> },
|
||||
}
|
||||
Reference in New Issue
Block a user