Skip to content

Commit bee6474

Browse files
Prototyping desktop wrapper api
1 parent 2f4aef3 commit bee6474

File tree

1 file changed

+237
-0
lines changed

1 file changed

+237
-0
lines changed

desktop/src/editor_api.rs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
use graphene_std::{Color, raster::Image};
2+
use graphite_editor::{
3+
application::Editor,
4+
messages::prelude::{DocumentId, DocumentMessage, FrontendMessage, InputPreprocessorMessage, Message, PortfolioMessage},
5+
};
6+
use std::{io::Cursor, path::PathBuf};
7+
8+
pub struct EditorApi {
9+
editor: Editor,
10+
}
11+
12+
impl EditorApi {
13+
pub fn new() -> Self {
14+
Self { editor: Editor::new() }
15+
}
16+
17+
pub fn dispatch(&mut self, message: EditorMessage) -> Vec<NativeMessage> {
18+
let mut responses = Vec::new();
19+
match message {
20+
EditorMessage::FromFrontend(data) => {
21+
let string = std::str::from_utf8(&data).unwrap();
22+
match ron::from_str::<Message>(string) {
23+
Ok(message) => {
24+
self.handle_message(message, &mut responses);
25+
}
26+
Err(e) => {
27+
tracing::error!("Failed to deserialize message {:?}", e)
28+
}
29+
}
30+
}
31+
EditorMessage::OpenFileDialogResult { path, content, context } => match context.0 {
32+
OpenFileDialogContextInner::Document => match String::from_utf8(content) {
33+
Ok(content) => {
34+
let message = PortfolioMessage::OpenDocumentFile {
35+
document_name: None,
36+
document_path: Some(path),
37+
document_serialized_content: content,
38+
};
39+
self.handle_message(message.into(), &mut responses);
40+
}
41+
Err(e) => {
42+
tracing::error!("Failed to deserialize document content: {:?}", e);
43+
}
44+
},
45+
OpenFileDialogContextInner::Import => {
46+
let extension = path.extension().and_then(|s| s.to_str());
47+
let name = path.file_stem().map(|s| s.to_string_lossy().to_string());
48+
match extension {
49+
Some("svg") => match String::from_utf8(content) {
50+
Ok(content) if !content.is_empty() => {
51+
let message = PortfolioMessage::PasteSvg {
52+
name,
53+
svg: content,
54+
mouse: None,
55+
parent_and_insert_index: None,
56+
};
57+
self.handle_message(message.into(), &mut responses);
58+
}
59+
Ok(_) => {
60+
tracing::warn!("Svg file is empty: {}", path.display());
61+
}
62+
Err(e) => {
63+
tracing::error!("Failed to deserialize document content: {:?}", e);
64+
}
65+
},
66+
Some(_) => {
67+
let reader = image::ImageReader::new(Cursor::new(content));
68+
match reader.decode() {
69+
Ok(image) => {
70+
let width = image.width();
71+
let height = image.height();
72+
let image_data = image.to_rgba8();
73+
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
74+
75+
let message = PortfolioMessage::PasteImage {
76+
name,
77+
image,
78+
mouse: None,
79+
parent_and_insert_index: None,
80+
};
81+
self.handle_message(message.into(), &mut responses);
82+
}
83+
Err(e) => {
84+
tracing::error!("Failed to decode image: {}: {}", path.display(), e);
85+
}
86+
}
87+
}
88+
_ => {
89+
tracing::warn!("Unsupported file type: {}", path.display());
90+
}
91+
}
92+
}
93+
},
94+
EditorMessage::SaveFileDialogResult { path, context } => match context.0 {
95+
SaveFileDialogContextInner::Document { document_id } => {
96+
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
97+
document_id,
98+
message: DocumentMessage::SavedDocument { path: Some(path) },
99+
});
100+
self.handle_message(message, &mut responses);
101+
}
102+
SaveFileDialogContextInner::Export => {}
103+
},
104+
}
105+
responses
106+
}
107+
108+
fn handle_message(&mut self, message: Message, responses: &mut Vec<NativeMessage>) {
109+
handle_frontend_messages(self.editor.handle_message(message), responses);
110+
}
111+
112+
pub fn run_node_graph(&self) {}
113+
}
114+
115+
fn handle_message(message: Message, responses: &mut Vec<NativeMessage>) -> Option<Message> {
116+
if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message {
117+
let top_left = bounds_of_viewports[0].top_left;
118+
let bottom_right = bounds_of_viewports[0].bottom_right;
119+
responses.push(NativeMessage::UpdateViewportBounds {
120+
x: top_left.x as f32,
121+
y: top_left.y as f32,
122+
width: (bottom_right.x - top_left.x) as f32,
123+
height: (bottom_right.y - top_left.y) as f32,
124+
});
125+
}
126+
None
127+
}
128+
129+
fn handle_frontend_messages(messages: Vec<FrontendMessage>, responses: &mut Vec<NativeMessage>) {
130+
let frontend_messages = messages.into_iter().filter_map(|m| handle_frontend_message(m, responses)).collect::<Vec<_>>();
131+
responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes()));
132+
}
133+
134+
fn handle_frontend_message(message: FrontendMessage, responses: &mut Vec<NativeMessage>) -> Option<FrontendMessage> {
135+
match message {
136+
FrontendMessage::RenderOverlays(overlay_context) => {
137+
responses.push(NativeMessage::UpdateOverlays(overlay_context.take_scene()));
138+
}
139+
FrontendMessage::TriggerOpenDocument => {
140+
responses.push(NativeMessage::OpenFileDialog {
141+
title: "Open Document".to_string(),
142+
filters: vec![FileFilter {
143+
name: "Graphite".to_string(),
144+
extensions: vec!["graphite".to_string()],
145+
}],
146+
context: OpenFileDialogContext(OpenFileDialogContextInner::Document),
147+
});
148+
}
149+
FrontendMessage::TriggerImport => {
150+
responses.push(NativeMessage::OpenFileDialog {
151+
title: "Import File".to_string(),
152+
filters: vec![
153+
FileFilter {
154+
name: "Svg".to_string(),
155+
extensions: vec!["svg".to_string()],
156+
},
157+
FileFilter {
158+
name: "Image".to_string(),
159+
extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()],
160+
},
161+
],
162+
context: OpenFileDialogContext(OpenFileDialogContextInner::Import),
163+
});
164+
}
165+
FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => {
166+
responses.push(NativeMessage::SaveFileDialog {
167+
title: "Save Document".to_string(),
168+
default_filename: name,
169+
default_folder: path.and_then(|p| p.parent().map(PathBuf::from)),
170+
content,
171+
context: SaveFileDialogContext(SaveFileDialogContextInner::Document { document_id }),
172+
});
173+
}
174+
FrontendMessage::TriggerSaveFile { name, content } => {
175+
responses.push(NativeMessage::SaveFileDialog {
176+
title: "Save File".to_string(),
177+
default_filename: name,
178+
default_folder: None,
179+
content,
180+
context: SaveFileDialogContext(SaveFileDialogContextInner::Export),
181+
});
182+
}
183+
FrontendMessage::TriggerVisitLink { url } => {
184+
responses.push(NativeMessage::OpenUrl(url));
185+
}
186+
m => return Some(m),
187+
}
188+
None
189+
}
190+
191+
pub enum NativeMessage {
192+
ToFrontend(Vec<u8>),
193+
OpenFileDialog {
194+
title: String,
195+
filters: Vec<FileFilter>,
196+
context: OpenFileDialogContext,
197+
},
198+
SaveFileDialog {
199+
title: String,
200+
default_filename: String,
201+
default_folder: Option<PathBuf>,
202+
content: Vec<u8>,
203+
context: SaveFileDialogContext,
204+
},
205+
OpenUrl(String),
206+
UpdateViewport(wgpu::Texture),
207+
UpdateViewportBounds {
208+
x: f32,
209+
y: f32,
210+
width: f32,
211+
height: f32,
212+
},
213+
UpdateOverlays(vello::Scene),
214+
}
215+
216+
pub enum EditorMessage {
217+
FromFrontend(Vec<u8>),
218+
OpenFileDialogResult { path: PathBuf, content: Vec<u8>, context: OpenFileDialogContext },
219+
SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext },
220+
}
221+
222+
pub struct FileFilter {
223+
pub name: String,
224+
pub extensions: Vec<String>,
225+
}
226+
227+
pub struct OpenFileDialogContext(OpenFileDialogContextInner);
228+
enum OpenFileDialogContextInner {
229+
Document,
230+
Import,
231+
}
232+
233+
pub struct SaveFileDialogContext(SaveFileDialogContextInner);
234+
enum SaveFileDialogContextInner {
235+
Document { document_id: DocumentId },
236+
Export,
237+
}

0 commit comments

Comments
 (0)