diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs index d07c3ef9bb..d3e159e410 100644 --- a/desktop/bundle/src/win.rs +++ b/desktop/bundle/src/win.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use crate::common::*; const PACKAGE: &str = "graphite-desktop-platform-win"; -const EXECUTABLE: &str = "graphite-editor.exe"; +const EXECUTABLE: &str = "graphite.exe"; pub fn main() -> Result<(), Box> { let app_bin = build_bin(PACKAGE, None)?; diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 2b8f7df42d..96c877f82f 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -18,22 +18,22 @@ use crate::cef; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::event::{AppEvent, AppEventScheduler}; use crate::persist::PersistentData; -use crate::render::GraphicsState; +use crate::render::{RenderError, RenderState}; use crate::window::Window; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { - cef_context: Box, + render_state: Option, + wgpu_context: WgpuContext, window: Option, window_scale: f64, - cef_schedule: Option, - cef_view_info_sender: Sender, - graphics_state: Option, - wgpu_context: WgpuContext, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, desktop_wrapper: DesktopWrapper, + cef_context: Box, + cef_schedule: Option, + cef_view_info_sender: Sender, last_ui_update: Instant, avg_frame_time: f32, start_render_sender: SyncSender<()>, @@ -77,17 +77,17 @@ impl App { persistent_data.load_from_disk(); Self { - cef_context, - window: None, - window_scale: 1.0, - cef_schedule: Some(Instant::now()), - graphics_state: None, - cef_view_info_sender, + render_state: None, wgpu_context, + window: None, + window_scale: 1., app_event_receiver, app_event_scheduler, desktop_wrapper: DesktopWrapper::new(), last_ui_update: Instant::now(), + cef_context, + cef_schedule: Some(Instant::now()), + cef_view_info_sender, avg_frame_time: 0., start_render_sender, web_communication_initialized: false, @@ -162,23 +162,23 @@ impl App { }); } DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => { - if let Some(graphics_state) = &mut self.graphics_state + if let Some(render_state) = &mut self.render_state && let Some(window) = &self.window { let window_size = window.surface_size(); let viewport_offset_x = x / window_size.width as f64; let viewport_offset_y = y / window_size.height as f64; - graphics_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]); + render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]); let viewport_scale_x = if width != 0.0 { window_size.width as f64 / width } else { 1.0 }; let viewport_scale_y = if height != 0.0 { window_size.height as f64 / height } else { 1.0 }; - graphics_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]); + render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]); } } DesktopFrontendMessage::UpdateOverlays(scene) => { - if let Some(graphics_state) = &mut self.graphics_state { - graphics_state.set_overlays_scene(scene); + if let Some(render_state) = &mut self.render_state { + render_state.set_overlays_scene(scene); } } DesktopFrontendMessage::PersistenceWriteDocument { id, document } => { @@ -331,19 +331,18 @@ impl App { 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(render_state) = self.render_state.as_mut() && let Some(window) = self.window.as_ref() { - graphics_state.bind_viewport_texture(texture); + render_state.bind_viewport_texture(texture); window.request_redraw(); } } NodeGraphExecutionResult::NotRun => {} }, AppEvent::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); + if let Some(render_state) = self.render_state.as_mut() { + render_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 { @@ -385,13 +384,18 @@ impl ApplicationHandler for App { self.window_scale = window.scale_factor(); let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(self.window_scale)); + + // Ensures the CEF texture does not remain at 1x1 pixels until the window is resized by the user + // Affects only some Mac devices (issue found on 2023 M2 Mac Mini). + let PhysicalSize { width, height } = window.surface_size(); + let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { width, height }); + self.cef_context.notify_view_info_changed(); self.window = Some(window); - let graphics_state = GraphicsState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); - - self.graphics_state = Some(graphics_state); + let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); + self.render_state = Some(render_state); self.desktop_wrapper.init(self.wgpu_context.clone()); @@ -418,14 +422,18 @@ impl ApplicationHandler for App { self.app_event_scheduler.schedule(AppEvent::CloseWindow); } WindowEvent::SurfaceResized(PhysicalSize { width, height }) => { - let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { - width: width as usize, - height: height as usize, - }); + let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { width, height }); self.cef_context.notify_view_info_changed(); + + if let Some(render_state) = &mut self.render_state { + render_state.resize(width, height); + } + if let Some(window) = &self.window { let maximized = window.is_maximized(); self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized })); + + window.request_redraw(); } } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { @@ -434,18 +442,24 @@ impl ApplicationHandler for App { self.cef_context.notify_view_info_changed(); } WindowEvent::RedrawRequested => { - let Some(ref mut graphics_state) = self.graphics_state else { return }; - // Only rerender once we have a new UI texture to display + let Some(render_state) = &mut self.render_state else { return }; if let Some(window) = &self.window { - match graphics_state.render(window) { + let size = window.surface_size(); + render_state.resize(size.width, size.height); + + match render_state.render(window) { Ok(_) => {} - Err(wgpu::SurfaceError::Lost) => { + Err(RenderError::OutdatedUITextureError) => { + self.cef_context.notify_view_info_changed(); + } + Err(RenderError::SurfaceError(wgpu::SurfaceError::Lost)) => { tracing::warn!("lost surface"); } - Err(wgpu::SurfaceError::OutOfMemory) => { + Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => { + tracing::error!("GPU out of memory"); event_loop.exit(); } - Err(e) => tracing::error!("{:?}", e), + Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e), } let _ = self.start_render_sender.try_send(()); } diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index 1b5383c4fb..c28dad60e7 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -55,8 +55,8 @@ pub(crate) trait CefEventHandler: Send + Sync + 'static { #[derive(Clone, Copy)] pub(crate) struct ViewInfo { - width: usize, - height: usize, + width: u32, + height: u32, scale: f64, } impl ViewInfo { @@ -78,10 +78,10 @@ impl ViewInfo { pub(crate) fn zoom(&self) -> f64 { self.scale.ln() / 1.2_f64.ln() } - pub(crate) fn width(&self) -> usize { + pub(crate) fn width(&self) -> u32 { self.width } - pub(crate) fn height(&self) -> usize { + pub(crate) fn height(&self) -> u32 { self.height } } @@ -92,7 +92,7 @@ impl Default for ViewInfo { } pub(crate) enum ViewInfoUpdate { - Size { width: usize, height: usize }, + Size { width: u32, height: u32 }, Scale(f64), } diff --git a/desktop/src/cli.rs b/desktop/src/cli.rs index 4e54bd2477..3af8e10c4a 100644 --- a/desktop/src/cli.rs +++ b/desktop/src/cli.rs @@ -1,5 +1,5 @@ #[derive(clap::Parser)] -#[clap(name = "graphite-editor", version)] +#[clap(name = "graphite", version)] pub struct Cli { #[arg(help = "Files to open on startup")] pub files: Vec, diff --git a/desktop/src/consts.rs b/desktop/src/consts.rs index 28879574d1..03154e4206 100644 --- a/desktop/src/consts.rs +++ b/desktop/src/consts.rs @@ -1,4 +1,5 @@ pub(crate) const APP_NAME: &str = "Graphite"; +#[cfg(target_os = "linux")] pub(crate) const APP_ID: &str = "rs.graphite.Graphite"; pub(crate) const APP_DIRECTORY_NAME: &str = "graphite"; diff --git a/desktop/src/render.rs b/desktop/src/render.rs index df03381f08..e5d57982dc 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -1,5 +1,5 @@ mod frame_buffer_ref; pub(crate) use frame_buffer_ref::FrameBufferRef; -mod graphics_state; -pub(crate) use graphics_state::GraphicsState; +mod state; +pub(crate) use state::{RenderError, RenderState}; diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index 20ee43f1b2..f49144be74 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -23,6 +23,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { struct Constants { viewport_scale: vec2, viewport_offset: vec2, + ui_scale: vec2, + background_color: vec4, }; var constants: Constants; @@ -38,19 +40,29 @@ var s_diffuse: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, in.tex_coords)); + let ui_coordinate = in.tex_coords * constants.ui_scale; + if (ui_coordinate.x < 0.0 || ui_coordinate.x > 1.0 || + ui_coordinate.y < 0.0 || ui_coordinate.y > 1.0) { + return srgb_to_linear(constants.background_color); + } + + let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate)); if (ui_linear.a >= 0.999) { return ui_linear; } + // UI texture is premultiplied, we need to unpremultiply before blending + let ui_srgb = linear_to_srgb(unpremultiply(ui_linear)); + let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; + if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 || + viewport_coordinate.y < 0.0 || viewport_coordinate.y > 1.0) { + return srgb_to_linear(constants.background_color); + } let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate); let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate); - // UI texture is premultiplied, we need to unpremultiply before blending - let ui_srgb = linear_to_srgb(unpremultiply(ui_linear)); - if (overlay_srgb.a < 0.001) { if (ui_srgb.a < 0.001) { return srgb_to_linear(viewport_srgb); diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/state.rs similarity index 88% rename from desktop/src/render/graphics_state.rs rename to desktop/src/render/state.rs index 476e592650..b03e4659ef 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/state.rs @@ -4,7 +4,7 @@ use crate::wrapper::{Color, WgpuContext, WgpuExecutor}; #[derive(derivative::Derivative)] #[derivative(Debug)] -pub(crate) struct GraphicsState { +pub(crate) struct RenderState { surface: wgpu::Surface<'static>, context: WgpuContext, executor: WgpuExecutor, @@ -12,6 +12,8 @@ pub(crate) struct GraphicsState { render_pipeline: wgpu::RenderPipeline, transparent_texture: wgpu::Texture, sampler: wgpu::Sampler, + desired_width: u32, + desired_height: u32, viewport_scale: [f32; 2], viewport_offset: [f32; 2], viewport_texture: Option, @@ -22,7 +24,7 @@ pub(crate) struct GraphicsState { overlays_scene: Option, } -impl GraphicsState { +impl RenderState { pub(crate) fn new(window: &Window, context: WgpuContext) -> Self { let size = window.surface_size(); let surface = window.create_surface(context.instance.clone()); @@ -171,6 +173,8 @@ impl GraphicsState { render_pipeline, transparent_texture, sampler, + desired_width: size.width, + desired_height: size.height, viewport_scale: [1.0, 1.0], viewport_offset: [0.0, 0.0], viewport_texture: None, @@ -182,6 +186,13 @@ impl GraphicsState { } pub(crate) fn resize(&mut self, width: u32, height: u32) { + if width == self.desired_width && height == self.desired_height { + return; + } + + self.desired_width = width; + self.desired_height = height; + if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) { self.config.width = width; self.config.height = height; @@ -230,24 +241,33 @@ impl GraphicsState { self.bind_overlays_texture(texture); } - pub(crate) fn render(&mut self, window: &Window) -> Result<(), wgpu::SurfaceError> { + pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> { + let ui_scale = if let Some(ui_texture) = &self.ui_texture + && (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height()) + { + Some([self.desired_width as f32 / ui_texture.width() as f32, self.desired_height as f32 / ui_texture.height() as f32]) + } else { + None + }; + if let Some(scene) = self.overlays_scene.take() { self.render_overlays(scene); } - let output = self.surface.get_current_texture()?; + let output = self.surface.get_current_texture().map_err(RenderError::SurfaceError)?; + let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), + label: Some("Graphite Composition Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }), + load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1. }), store: wgpu::StoreOp::Store, }, depth_slice: None, @@ -264,11 +284,14 @@ impl GraphicsState { bytemuck::bytes_of(&Constants { viewport_scale: self.viewport_scale, viewport_offset: self.viewport_offset, + ui_scale: ui_scale.unwrap_or([1., 1.]), + _pad: [0., 0.], + background_color: [0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 1.], // #222222 }), ); if let Some(bind_group) = &self.bind_group { render_pass.set_bind_group(0, bind_group, &[]); - render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle + render_pass.draw(0..3, 0..1); // Draw 3 vertices for fullscreen triangle } else { tracing::warn!("No bind group available - showing clear color only"); } @@ -277,6 +300,10 @@ impl GraphicsState { window.pre_present_notify(); output.present(); + if ui_scale.is_some() { + return Err(RenderError::OutdatedUITextureError); + } + Ok(()) } @@ -312,9 +339,17 @@ impl GraphicsState { } } +pub(crate) enum RenderError { + OutdatedUITextureError, + SurfaceError(wgpu::SurfaceError), +} + #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct Constants { viewport_scale: [f32; 2], viewport_offset: [f32; 2], + ui_scale: [f32; 2], + _pad: [f32; 2], + background_color: [f32; 4], } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 24ee4c5b1b..5b049eac34 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -111,10 +111,7 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); } #[cfg(target_os = "macos")] - FrontendMessage::UpdateMenuBarLayout { - layout_target: graphite_editor::messages::tool::tool_messages::tool_prelude::LayoutTarget::MenuBar, - diff, - } => { + FrontendMessage::UpdateMenuBarLayout { diff } => { use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff}; match diff.as_slice() { [ diff --git a/desktop/wrapper/src/utils.rs b/desktop/wrapper/src/utils.rs index 73a49960a1..c62fe8f466 100644 --- a/desktop/wrapper/src/utils.rs +++ b/desktop/wrapper/src/utils.rs @@ -3,14 +3,14 @@ pub(crate) mod menu { use base64::engine::Engine; use base64::engine::general_purpose::STANDARD as BASE64; - use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LabeledKey, LabeledShortcut}; + use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LabeledKeyOrMouseMotion, LabeledShortcut}; use graphite_editor::messages::input_mapper::utility_types::misc::ActionShortcut; use graphite_editor::messages::layout::LayoutMessage; use graphite_editor::messages::tool::tool_messages::tool_prelude::{Layout, LayoutGroup, LayoutTarget, MenuListEntry, Widget, WidgetId}; use crate::messages::{EditorMessage, KeyCode, MenuItem, Modifiers, Shortcut}; - pub(crate) fn convert_menu_bar_layout_to_menu_items(layout: &Layout) -> Vec { + pub(crate) fn convert_menu_bar_layout_to_menu_items(Layout(layout): &Layout) -> Vec { let layout_group = match layout.as_slice() { [layout_group] => layout_group, _ => panic!("Menu bar layout is supposed to have exactly one layout group"), @@ -68,9 +68,9 @@ pub(crate) mod menu { value, label, icon, - shortcut_keys, - children, disabled, + tooltip_shortcut, + children, .. }: &MenuListEntry = entry; path.push(value.clone()); @@ -83,7 +83,7 @@ pub(crate) mod menu { return MenuItem::SubMenu { id, text, enabled, items }; } - let shortcut = match shortcut_keys { + let shortcut = match tooltip_shortcut { Some(ActionShortcut::Shortcut(LabeledShortcut(shortcut))) => convert_labeled_keys_to_shortcut(shortcut), _ => None, }; @@ -126,10 +126,14 @@ pub(crate) mod menu { items } - fn convert_labeled_keys_to_shortcut(labeled_keys: &Vec) -> Option { + fn convert_labeled_keys_to_shortcut(labeled_keys: &Vec) -> Option { let mut key: Option = None; let mut modifiers = Modifiers::default(); for labeled_key in labeled_keys { + let LabeledKeyOrMouseMotion::Key(labeled_key) = labeled_key else { + // Return None for shortcuts that include mouse motion because we can't show them in native menu + return None; + }; match labeled_key.key() { Key::Shift => modifiers |= Modifiers::SHIFT, Key::Control => modifiers |= Modifiers::CONTROL, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 0f4f75a6f2..7fe27b46c5 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -488,7 +488,7 @@ impl LayoutMessageHandler { if layout_target == LayoutTarget::MenuBar { widget_diffs = vec![WidgetDiff { widget_path: Vec::new(), - new_value: DiffUpdate::Layout(current.layout.clone()), + new_value: DiffUpdate::Layout(self.layouts[LayoutTarget::MenuBar as usize].clone()), }]; }