Skip to content

Commit 30e5d66

Browse files
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
1 parent a70c48f commit 30e5d66

File tree

14 files changed

+554
-261
lines changed

14 files changed

+554
-261
lines changed

desktop/src/app.rs

Lines changed: 120 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
use crate::CustomEvent;
22
use crate::WindowSize;
33
use crate::consts::APP_NAME;
4-
use crate::dialogs::dialog_open_graphite_file;
5-
use crate::dialogs::dialog_save_file;
6-
use crate::dialogs::dialog_save_graphite_file;
4+
use crate::desktop_wrapper::DesktopWrapper;
5+
use crate::desktop_wrapper::NodeGraphExecutionResult;
6+
use crate::desktop_wrapper::WgpuContext;
7+
use crate::desktop_wrapper::messages::DesktopFrontendMessage;
8+
use crate::desktop_wrapper::messages::DesktopWrapperMessage;
9+
use crate::desktop_wrapper::serialize_frontend_messages;
710
use crate::render::GraphicsState;
8-
use crate::render::WgpuContext;
9-
use graph_craft::wasm_application_io::WasmApplicationIo;
10-
use graphene_std::Color;
11-
use graphene_std::raster::Image;
12-
use graphite_editor::application::Editor;
13-
use graphite_editor::messages::prelude::*;
14-
use std::fs;
11+
use rfd::AsyncFileDialog;
1512
use std::sync::Arc;
1613
use std::sync::mpsc::Sender;
1714
use std::thread;
@@ -37,11 +34,12 @@ pub(crate) struct WinitApp {
3734
graphics_state: Option<GraphicsState>,
3835
wgpu_context: WgpuContext,
3936
event_loop_proxy: EventLoopProxy<CustomEvent>,
40-
editor: Editor,
37+
desktop_wrapper: DesktopWrapper,
4138
}
4239

4340
impl WinitApp {
4441
pub(crate) fn new(cef_context: cef::Context<cef::Initialized>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
42+
let desktop_wrapper = DesktopWrapper::new();
4543
Self {
4644
cef_context,
4745
window: None,
@@ -50,97 +48,106 @@ impl WinitApp {
5048
window_size_sender,
5149
wgpu_context,
5250
event_loop_proxy,
53-
editor: Editor::new(),
51+
desktop_wrapper,
5452
}
5553
}
5654

57-
fn dispatch_message(&mut self, message: Message) {
58-
let responses = self.editor.handle_message(message);
59-
self.send_messages_to_editor(responses);
60-
}
61-
62-
fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
63-
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays { .. })) {
64-
let FrontendMessage::RenderOverlays { context: overlay_context } = message else { unreachable!() };
65-
if let Some(graphics_state) = &mut self.graphics_state {
66-
let scene = overlay_context.take_scene();
67-
graphics_state.set_overlays_scene(scene);
55+
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) {
56+
match message {
57+
DesktopFrontendMessage::ToWeb(messages) => {
58+
let Some(bytes) = serialize_frontend_messages(messages) else {
59+
tracing::error!("Failed to serialize frontend messages");
60+
return;
61+
};
62+
self.cef_context.send_web_message(bytes.as_slice());
6863
}
69-
}
70-
71-
for _ in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerOpenDocument)) {
72-
let event_loop_proxy = self.event_loop_proxy.clone();
73-
let _ = thread::spawn(move || {
74-
let path = futures::executor::block_on(dialog_open_graphite_file());
75-
if let Some(path) = path {
76-
let content = std::fs::read_to_string(&path).unwrap_or_else(|_| {
77-
tracing::error!("Failed to read file: {}", path.display());
78-
String::new()
79-
});
80-
let message = PortfolioMessage::OpenDocumentFile {
81-
document_name: None,
82-
document_path: Some(path),
83-
document_serialized_content: content,
84-
};
85-
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
86-
}
87-
});
88-
}
89-
90-
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) {
91-
let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else {
92-
unreachable!()
93-
};
94-
if let Some(path) = path {
95-
let _ = std::fs::write(&path, content);
96-
} else {
64+
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
9765
let event_loop_proxy = self.event_loop_proxy.clone();
9866
let _ = thread::spawn(move || {
99-
let path = futures::executor::block_on(dialog_save_graphite_file(name));
100-
if let Some(path) = path {
101-
if let Err(e) = std::fs::write(&path, content) {
102-
tracing::error!("Failed to save file: {}: {}", path.display(), e);
103-
} else {
104-
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
105-
document_id,
106-
message: DocumentMessage::SavedDocument { path: Some(path) },
107-
});
108-
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message));
109-
}
67+
let mut dialog = AsyncFileDialog::new().set_title(title);
68+
for filter in filters {
69+
dialog = dialog.add_filter(filter.name, &filter.extensions);
70+
}
71+
72+
let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) };
73+
74+
if let Some(path) = futures::executor::block_on(show_dialog)
75+
&& let Ok(content) = std::fs::read(&path)
76+
{
77+
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
78+
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
11079
}
11180
});
11281
}
113-
}
82+
DesktopFrontendMessage::SaveFileDialog {
83+
title,
84+
default_filename,
85+
default_folder,
86+
filters,
87+
context,
88+
} => {
89+
let event_loop_proxy = self.event_loop_proxy.clone();
90+
let _ = thread::spawn(move || {
91+
let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
92+
if let Some(folder) = default_folder {
93+
dialog = dialog.set_directory(folder);
94+
}
95+
for filter in filters {
96+
dialog = dialog.add_filter(filter.name, &filter.extensions);
97+
}
98+
99+
let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };
114100

115-
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) {
116-
let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() };
117-
let _ = thread::spawn(move || {
118-
let path = futures::executor::block_on(dialog_save_file(name));
119-
if let Some(path) = path {
120-
if let Err(e) = std::fs::write(&path, content) {
121-
tracing::error!("Failed to save file: {}: {}", path.display(), e);
101+
if let Some(path) = futures::executor::block_on(show_dialog) {
102+
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
103+
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
122104
}
105+
});
106+
}
107+
DesktopFrontendMessage::WriteFile { path, content } => {
108+
if let Err(e) = std::fs::write(&path, content) {
109+
tracing::error!("Failed to write file {}: {}", path.display(), e);
123110
}
124-
});
125-
}
111+
}
112+
DesktopFrontendMessage::OpenUrl(url) => {
113+
let _ = thread::spawn(move || {
114+
if let Err(e) = open::that(&url) {
115+
tracing::error!("Failed to open URL: {}: {}", url, e);
116+
}
117+
});
118+
}
119+
DesktopFrontendMessage::UpdateViewportBounds { x, y, width, height } => {
120+
if let Some(graphics_state) = &mut self.graphics_state
121+
&& let Some(window) = &self.window
122+
{
123+
let window_size = window.inner_size();
126124

127-
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
128-
let _ = thread::spawn(move || {
129-
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };
130-
if let Err(e) = open::that(&url) {
131-
tracing::error!("Failed to open URL: {}: {}", url, e);
125+
let viewport_offset_x = x / window_size.width as f32;
126+
let viewport_offset_y = y / window_size.height as f32;
127+
graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]);
128+
129+
let viewport_scale_x = if width != 0.0 { window_size.width as f32 / width } else { 1.0 };
130+
let viewport_scale_y = if height != 0.0 { window_size.height as f32 / height } else { 1.0 };
131+
graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]);
132+
}
133+
}
134+
DesktopFrontendMessage::UpdateOverlays(scene) => {
135+
if let Some(graphics_state) = &mut self.graphics_state {
136+
graphics_state.set_overlays_scene(scene);
132137
}
133-
});
138+
}
134139
}
140+
}
135141

136-
if responses.is_empty() {
137-
return;
142+
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
143+
for message in messages {
144+
self.handle_desktop_frontend_message(message);
138145
}
139-
let Ok(message) = ron::to_string(&responses) else {
140-
tracing::error!("Failed to serialize Messages");
141-
return;
142-
};
143-
self.cef_context.send_web_message(message.as_bytes());
146+
}
147+
148+
fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
149+
let responses = self.desktop_wrapper.dispatch(message);
150+
self.handle_desktop_frontend_messages(responses);
144151
}
145152
}
146153

@@ -194,13 +201,25 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
194201

195202
tracing::info!("Winit window created and ready");
196203

197-
let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone());
198-
199-
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
204+
self.desktop_wrapper.init(self.wgpu_context.clone());
200205
}
201206

202207
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
203208
match event {
209+
CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
210+
CustomEvent::NodeGraphExecutionResult(result) => match result {
211+
NodeGraphExecutionResult::HasRun(texture) => {
212+
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
213+
if let Some(texture) = texture
214+
&& let Some(graphics_state) = self.graphics_state.as_mut()
215+
&& let Some(window) = self.window.as_ref()
216+
{
217+
graphics_state.bind_viewport_texture(texture);
218+
window.request_redraw();
219+
}
220+
}
221+
NodeGraphExecutionResult::NotRun => {}
222+
},
204223
CustomEvent::UiUpdate(texture) => {
205224
if let Some(graphics_state) = self.graphics_state.as_mut() {
206225
graphics_state.resize(texture.width(), texture.height());
@@ -217,127 +236,13 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
217236
self.cef_schedule = Some(instant);
218237
}
219238
}
220-
CustomEvent::DispatchMessage(message) => {
221-
self.dispatch_message(message);
222-
}
223-
CustomEvent::MessageReceived(message) => {
224-
if let Message::InputPreprocessor(_) = &message {
225-
if let Some(window) = &self.window {
226-
window.request_redraw();
227-
}
228-
}
229-
if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message {
230-
if let Some(graphic_state) = &mut self.graphics_state {
231-
let window_size = self.window.as_ref().unwrap().inner_size();
232-
let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32);
233-
let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size;
234-
let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size;
235-
let offset = top_left.to_array();
236-
let scale = (bottom_right - top_left).recip();
237-
graphic_state.set_viewport_offset(offset);
238-
graphic_state.set_viewport_scale(scale.to_array());
239-
} else {
240-
panic!("graphics state not intialized, viewport offset might be lost");
241-
}
242-
}
243-
244-
self.dispatch_message(message);
245-
}
246-
CustomEvent::NodeGraphRan(texture) => {
247-
if let Some(texture) = texture
248-
&& let Some(graphics_state) = &mut self.graphics_state
249-
{
250-
graphics_state.bind_viewport_texture(texture);
251-
}
252-
let mut responses = VecDeque::new();
253-
let err = self.editor.poll_node_graph_evaluation(&mut responses);
254-
if let Err(e) = err {
255-
if e != "No active document" {
256-
tracing::error!("Error poling node graph: {}", e);
257-
}
258-
}
259-
260-
for message in responses {
261-
self.dispatch_message(message);
262-
}
263-
}
264239
}
265240
}
266241

267242
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
268243
let Some(event) = self.cef_context.handle_window_event(event) else { return };
269244

270245
match event {
271-
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
272-
WindowEvent::DroppedFile(path) => {
273-
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
274-
let Some(extension) = path.extension().and_then(|s| s.to_str()) else {
275-
tracing::warn!("Unsupported file dropped: {}", path.display());
276-
// Fine to early return since we don't need to do cef work in this case
277-
return;
278-
};
279-
let load_string = |path: &std::path::PathBuf| {
280-
let Ok(content) = fs::read_to_string(path) else {
281-
tracing::error!("Failed to read file: {}", path.display());
282-
return None;
283-
};
284-
285-
if content.is_empty() {
286-
tracing::warn!("Dropped file is empty: {}", path.display());
287-
return None;
288-
}
289-
Some(content)
290-
};
291-
// 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
292-
match extension {
293-
"graphite" => {
294-
let Some(content) = load_string(&path) else { return };
295-
296-
let message = PortfolioMessage::OpenDocumentFile {
297-
document_name: None,
298-
document_path: Some(path),
299-
document_serialized_content: content,
300-
};
301-
self.dispatch_message(message.into());
302-
}
303-
"svg" => {
304-
let Some(content) = load_string(&path) else { return };
305-
306-
let message = PortfolioMessage::PasteSvg {
307-
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
308-
svg: content,
309-
mouse: None,
310-
parent_and_insert_index: None,
311-
};
312-
self.dispatch_message(message.into());
313-
}
314-
_ => match image::ImageReader::open(&path) {
315-
Ok(reader) => match reader.decode() {
316-
Ok(image) => {
317-
let width = image.width();
318-
let height = image.height();
319-
// TODO: support loading images with more than 8 bits per channel
320-
let image_data = image.to_rgba8();
321-
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
322-
323-
let message = PortfolioMessage::PasteImage {
324-
name,
325-
image,
326-
mouse: None,
327-
parent_and_insert_index: None,
328-
};
329-
self.dispatch_message(message.into());
330-
}
331-
Err(e) => {
332-
tracing::error!("Failed to decode image: {}: {}", path.display(), e);
333-
}
334-
},
335-
Err(e) => {
336-
tracing::error!("Failed to open image file: {}: {}", path.display(), e);
337-
}
338-
},
339-
}
340-
}
341246
WindowEvent::CloseRequested => {
342247
tracing::info!("The close button was pressed; stopping");
343248
event_loop.exit();
@@ -362,6 +267,19 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
362267
Err(e) => tracing::error!("{:?}", e),
363268
}
364269
}
270+
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
271+
WindowEvent::DroppedFile(path) => {
272+
match std::fs::read(&path) {
273+
Ok(content) => {
274+
let message = DesktopWrapperMessage::OpenFile { path, content };
275+
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
276+
}
277+
Err(e) => {
278+
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
279+
return;
280+
}
281+
};
282+
}
365283
_ => {}
366284
}
367285

0 commit comments

Comments
 (0)