Skip to content

Commit 9eb0719

Browse files
Desktop: Recover from UI crashes and apply preferences without restarting (#4827)
* Desktop: Recover from UI crashes and apply preferences without restarting * Show open dialog again after a UI restart
1 parent 3bb5bde commit 9eb0719

39 files changed

Lines changed: 279 additions & 205 deletions

‎desktop/src/app.rs‎

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ use crate::persist;
2323
use crate::preferences;
2424
use crate::render::{RenderError, RenderState};
2525
use crate::spawner::TokioSpawner;
26-
use crate::ui::{InputEvent, UiCommand, UiInstance};
26+
use crate::ui::{Acceleration, InputEvent, UiCommand, UiConfig, UiInstance};
2727
use crate::window::Window;
28-
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, IngestAction, Preferences};
28+
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, IngestAction};
2929
use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, WgpuContext, WgpuExecutor, serialize_frontend_messages};
3030

3131
pub(crate) struct App {
@@ -40,18 +40,16 @@ pub(crate) struct App {
4040
pending_dnd_fetch: Option<AsyncRequestSerial>,
4141
pending_clipboard_fetch: Option<AsyncRequestSerial>,
4242
input_state: InputState,
43-
ui_scale: f64,
4443
app_event_receiver: Receiver<AppEvent>,
4544
app_event_scheduler: AppEventScheduler,
4645
desktop_wrapper: DesktopWrapper,
4746
ui: UiInstance,
4847
ui_frame_received: bool,
4948
start_render_sender: SyncSender<()>,
50-
web_communication_initialized: bool,
49+
ui_initialized: Option<Instant>,
5150
web_communication_startup_buffer: Vec<Vec<u8>>,
52-
preferences: Preferences,
51+
preferences: preferences::DesktopPreferences,
5352
launch_documents: Option<Vec<PathBuf>>,
54-
startup_time: Option<Instant>,
5553
exiting: Arc<AtomicBool>,
5654
exit_reason: ExitReason,
5755
}
@@ -66,7 +64,7 @@ impl App {
6664
wgpu_context: WgpuContext,
6765
app_event_receiver: Receiver<AppEvent>,
6866
app_event_scheduler: AppEventScheduler,
69-
preferences: Preferences,
67+
preferences: preferences::DesktopPreferences,
7068
launch_documents: Vec<PathBuf>,
7169
) -> Self {
7270
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
@@ -96,7 +94,6 @@ impl App {
9694
let runtime_wake = move || {
9795
let _ = runtime_wake_sender.try_send(());
9896
};
99-
// The node graph and the window's overlays render through this one executor, sharing its renderer and texture cache
10097
let wgpu_executor = WgpuExecutor::with_context(wgpu_context).expect("Failed to create WgpuExecutor");
10198
let (desktop_wrapper, mut node_runtime) = DesktopWrapper::new(
10299
rand::rng().random(),
@@ -133,18 +130,16 @@ impl App {
133130
pending_dnd_fetch: None,
134131
pending_clipboard_fetch: None,
135132
input_state: InputState::new(),
136-
ui_scale: 1.,
137133
app_event_receiver,
138134
app_event_scheduler,
139135
desktop_wrapper,
140136
ui,
141137
ui_frame_received: false,
142138
start_render_sender,
143-
web_communication_initialized: false,
139+
ui_initialized: None,
144140
web_communication_startup_buffer: Vec::new(),
145141
preferences,
146142
launch_documents: Some(launch_documents),
147-
startup_time: None,
148143
exiting,
149144
exit_reason: ExitReason::Shutdown,
150145
}
@@ -166,6 +161,22 @@ impl App {
166161
self.app_event_scheduler.schedule(AppEvent::Exit);
167162
}
168163

164+
fn restart_ui(&mut self) {
165+
let acceleration = self.preferences.acceleration;
166+
if let Err(error) = self.ui.restart(UiConfig { acceleration }) {
167+
tracing::error!("Failed to restart the UI: {error}");
168+
self.exit(Some(ExitReason::Shutdown));
169+
return;
170+
}
171+
self.ui_initialized = None;
172+
173+
self.window_size = PhysicalSize { width: 0, height: 0 };
174+
self.window_scale = 1.;
175+
self.window_maximized = false;
176+
self.window_fullscreen = false;
177+
self.resize();
178+
}
179+
169180
fn resize(&mut self) {
170181
let Some(window) = &self.window else {
171182
tracing::error!("Resize failed due to missing window");
@@ -186,7 +197,7 @@ impl App {
186197
}
187198

188199
let size = window.surface_size();
189-
let scale = window.scale_factor() * self.ui_scale;
200+
let scale = window.scale_factor() * self.preferences.ui_scale;
190201
let is_new_size = size != self.window_size;
191202
let is_new_scale = scale != self.window_scale;
192203

@@ -309,13 +320,9 @@ impl App {
309320
DesktopFrontendMessage::WindowUpdateDirectInput { enabled } => {
310321
self.input_state.set_direct_input(enabled);
311322
}
312-
DesktopFrontendMessage::UpdateUIScale { scale } => {
313-
self.ui_scale = scale;
314-
self.resize();
315-
}
316323
DesktopFrontendMessage::UpdateOverlays(scene) => {
317324
if let Some(render_state) = &mut self.render_state {
318-
render_state.set_overlays_scene(scene);
325+
render_state.set_overlays_scene(*scene);
319326
}
320327
if let Some(window) = &self.window {
321328
window.request_redraw();
@@ -327,8 +334,26 @@ impl App {
327334
DesktopFrontendMessage::PersistenceReadState => {
328335
responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
329336
}
330-
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
337+
DesktopFrontendMessage::PersistenceUpdatePreferences { preferences } => {
338+
let old_preferences = self.preferences.clone();
339+
self.preferences = preferences::DesktopPreferences::from(&preferences);
340+
331341
preferences::write(preferences);
342+
343+
if old_preferences.present_mode != self.preferences.present_mode
344+
&& let Some(render_state) = &mut self.render_state
345+
{
346+
render_state.set_present_mode(self.preferences.present_mode);
347+
}
348+
349+
if old_preferences.ui_scale != self.preferences.ui_scale {
350+
self.resize();
351+
}
352+
353+
if old_preferences.acceleration != self.preferences.acceleration {
354+
tracing::info!("UI acceleration preference changed, restarting UI");
355+
self.restart_ui();
356+
}
332357
}
333358
DesktopFrontendMessage::PersistenceLoadPreferences => {
334359
let preferences = preferences::read();
@@ -337,7 +362,6 @@ impl App {
337362
}
338363
DesktopFrontendMessage::OpenLaunchDocuments => {
339364
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
340-
tracing::error!("OpenLaunchDocuments should only be sent once");
341365
return;
342366
};
343367
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
@@ -434,7 +458,7 @@ impl App {
434458
}
435459

436460
fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
437-
if self.web_communication_initialized {
461+
if self.ui_initialized.is_some() {
438462
self.ui.send(UiCommand::Message(message));
439463
} else {
440464
self.web_communication_startup_buffer.push(message);
@@ -444,7 +468,8 @@ impl App {
444468
fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) {
445469
match event {
446470
AppEvent::WebCommunicationInitialized => {
447-
self.web_communication_initialized = true;
471+
self.ui_initialized = Some(Instant::now());
472+
self.ui_frame_received = false;
448473
for message in self.web_communication_startup_buffer.drain(..) {
449474
self.ui.send(UiCommand::Message(message));
450475
}
@@ -505,6 +530,10 @@ impl App {
505530
tracing::info!("Exiting main event loop");
506531
event_loop.exit();
507532
}
533+
AppEvent::UiCrashed if self.ui_initialized.is_some() => {
534+
tracing::error!("UI process crashed, restarting it.");
535+
self.restart_ui();
536+
}
508537
AppEvent::UiCrashed => {
509538
tracing::error!("UI process crashed, exiting.");
510539
self.exit(Some(ExitReason::Shutdown));
@@ -548,21 +577,17 @@ impl ApplicationHandler for App {
548577
let window = Window::new(event_loop, self.app_event_scheduler.clone());
549578
self.window = Some(window);
550579

551-
#[cfg(not(target_os = "macos"))]
552-
let present_mode = None;
553-
#[cfg(target_os = "macos")]
554-
let present_mode = if !self.preferences.vsync { Some(wgpu::PresentMode::Immediate) } else { None };
555-
556-
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_executor.clone(), present_mode);
580+
let mut render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_executor.clone());
581+
if self.preferences.present_mode.is_some() {
582+
render_state.set_present_mode(self.preferences.present_mode);
583+
}
557584
self.render_state = Some(render_state);
558585

559586
if let Some(window) = &self.window.as_ref() {
560587
window.show();
561588
}
562589

563590
self.resize();
564-
565-
self.startup_time = Some(Instant::now());
566591
}
567592

568593
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
@@ -625,13 +650,14 @@ impl ApplicationHandler for App {
625650
}
626651

627652
if !self.ui_frame_received
628-
&& !self.preferences.disable_ui_acceleration
629-
&& self.web_communication_initialized
630-
&& let Some(startup_time) = self.startup_time
631-
&& startup_time.elapsed() > Duration::from_secs(3)
653+
&& self.preferences.acceleration != Acceleration::Disabled
654+
&& let Some(initialized) = self.ui_initialized
655+
&& initialized.elapsed() > Duration::from_secs(3)
632656
{
633-
tracing::error!("UI acceleration not working, exiting.");
634-
self.exit(Some(ExitReason::UiAccelerationFailure));
657+
tracing::error!("UI acceleration not working, disabling it.");
658+
let mut preferences = preferences::read();
659+
preferences.disable_ui_acceleration = true;
660+
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::LoadPreferences { preferences });
635661
}
636662
}
637663
WindowEvent::DragEntered { id, .. } => {
@@ -733,5 +759,4 @@ impl ApplicationHandler for App {
733759
pub(crate) enum ExitReason {
734760
Shutdown,
735761
Restart,
736-
UiAccelerationFailure,
737762
}

‎desktop/src/input.rs‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -337,21 +337,16 @@ impl ViewportInfo {
337337
}
338338
}
339339

340-
/// Counts clicks the way the operating system does, so a single click is never taken as the end of a double-click.
341-
/// The count is decided when a button is pressed and reported again when it's released.
342340
#[derive(Default)]
343341
struct ClickTracker {
344-
/// The latest press of any button, since pressing another button in between ends a chain of clicks.
345342
latest_press: Option<Press>,
346343
}
347344

348-
/// A button press, kept to tell whether the next press continues its chain of clicks.
349345
struct Press {
350346
button: MouseButton,
351347
time: Instant,
352348
position: (i32, i32),
353349
count: u32,
354-
/// Whether the button was released away from where it was pressed, which makes the press a drag rather than a click.
355350
dragged: bool,
356351
}
357352

@@ -362,7 +357,6 @@ impl ClickTracker {
362357
let within_travel = |from: (i32, i32)| from.0.abs_diff(position.0) as usize <= MULTICLICK_ALLOWED_TRAVEL && from.1.abs_diff(position.1) as usize <= MULTICLICK_ALLOWED_TRAVEL;
363358

364359
match state {
365-
// A press continues the chain when it comes soon after a click of the same button and lands near it
366360
ElementState::Pressed => {
367361
let count = match &self.latest_press {
368362
Some(previous) if previous.button == button && !previous.dragged && now.saturating_duration_since(previous.time) <= MULTICLICK_TIMEOUT && within_travel(previous.position) => {
@@ -379,7 +373,6 @@ impl ClickTracker {
379373
});
380374
count
381375
}
382-
// A release reports the count of its press, unless the pointer traveled away in between, which makes it a drag that ends the chain
383376
ElementState::Released => {
384377
let Some(press) = self.latest_press.as_mut().filter(|press| press.button == button) else {
385378
return 1;

‎desktop/src/lib.rs‎

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ pub fn start() -> ExitCode {
7777
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
7878
dirs::delete_old_cef_browser_directory();
7979

80-
let mut prefs = preferences::read();
81-
8280
// Must be called before event loop initialization or native window integrations will break
8381
App::init();
8482

@@ -90,15 +88,19 @@ pub fn start() -> ExitCode {
9088

9189
let _socket_handle = socket::start(app_event_scheduler.clone());
9290

91+
let mut prefs = preferences::DesktopPreferences::from(&preferences::read());
92+
9393
if cli.disable_ui_acceleration {
94-
prefs.disable_ui_acceleration = true;
94+
prefs.acceleration = Acceleration::Disabled;
95+
preferences::modify(|prefs| {
96+
prefs.disable_ui_acceleration = true;
97+
});
9598
}
96-
if prefs.disable_ui_acceleration {
99+
if prefs.acceleration == Acceleration::Disabled {
97100
println!("UI acceleration is disabled");
98101
}
99102

100-
let acceleration = if prefs.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto };
101-
let ui_context = match ui_context.start(UiConfig { acceleration }) {
103+
let ui_context = match ui_context.start(UiConfig { acceleration: prefs.acceleration }) {
102104
Ok(context) => context,
103105
Err(error) => {
104106
tracing::error!("Failed to start the UI runtime: {error}");
@@ -148,29 +150,18 @@ pub fn start() -> ExitCode {
148150
// ui needs to be shutdown before restarting
149151
ui.shutdown();
150152

151-
// If exiting due to a UI acceleration failure, update preferences to disable it for next launch
152-
if matches!(exit_reason, app::ExitReason::UiAccelerationFailure) {
153-
tracing::error!("Disabling UI acceleration");
154-
preferences::modify(|prefs| {
155-
prefs.disable_ui_acceleration = true;
156-
});
157-
}
158-
159153
// Explicitly drop the instance lock
160154
drop(lock);
161155

162-
match exit_reason {
163-
app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => {
164-
tracing::info!("Restarting application");
165-
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
166-
#[cfg(target_family = "unix")]
167-
let _ = std::os::unix::process::CommandExt::exec(&mut command);
168-
#[cfg(target_family = "unix")]
169-
tracing::error!("Failed to restart application");
170-
#[cfg(not(target_family = "unix"))]
171-
let _ = command.spawn();
172-
}
173-
_ => {}
156+
if matches!(exit_reason, app::ExitReason::Restart) {
157+
tracing::info!("Restarting application");
158+
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
159+
#[cfg(target_family = "unix")]
160+
let _ = std::os::unix::process::CommandExt::exec(&mut command);
161+
#[cfg(target_family = "unix")]
162+
tracing::error!("Failed to restart application");
163+
#[cfg(not(target_family = "unix"))]
164+
let _ = command.spawn();
174165
}
175166

176167
ExitCode::SUCCESS

‎desktop/src/preferences.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
11
use graphite_desktop_wrapper::messages::Preferences;
2+
use wgpu::PresentMode;
3+
4+
use crate::ui::Acceleration;
5+
6+
#[derive(Clone, PartialEq)]
7+
pub(crate) struct DesktopPreferences {
8+
pub(crate) acceleration: Acceleration,
9+
pub(crate) present_mode: Option<PresentMode>,
10+
pub(crate) ui_scale: f64,
11+
}
12+
13+
impl From<&Preferences> for DesktopPreferences {
14+
fn from(preferences: &Preferences) -> Self {
15+
#[cfg(not(target_os = "macos"))]
16+
let present_mode = None;
17+
#[cfg(target_os = "macos")]
18+
let present_mode = if !preferences.vsync { Some(PresentMode::Immediate) } else { None };
19+
20+
let acceleration = if preferences.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto };
21+
22+
Self {
23+
acceleration,
24+
present_mode,
25+
ui_scale: preferences.ui_scale,
26+
}
27+
}
28+
}
229

330
pub(crate) fn write(preferences: Preferences) {
431
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {

0 commit comments

Comments
 (0)