Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 94 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ image = { version = "0.25.10", default-features = false, features = [
] }

[dev-dependencies]
futures = "0.3.33"
gpui = { version = "0.2.2", default-features = false, features = ["test-support"] }
rstest = "0.26.1"

[lints.rust]
Expand Down
97 changes: 85 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! subsystems. It intentionally lives outside `gui` so that the GUI module
//! can focus solely on rendering.

#[cfg(any(target_os = "linux", test))]
use std::time::Duration;
use std::{
cfg_select,
sync::{Arc, Mutex},
Expand All @@ -15,7 +17,7 @@ use gpui::{App, AppContext, KeyBinding, ReadGlobal, WindowHandle};
use gpui_component::Root;
#[cfg(target_os = "linux")]
use {
crate::gui::x11::X11,
crate::gui::{app::MAIN_WINDOW_TITLE, x11::X11},
std::{env, sync::OnceLock},
};

Expand All @@ -36,6 +38,32 @@ use crate::{
/// Shared X11 connection used for native window mapping and activation.
pub static X11_INSTANCE: OnceLock<X11> = OnceLock::new();

/// Grace period for GPUI to present the initial Linux frame before X11 unmaps it.
#[cfg(any(target_os = "linux", test))]
const LINUX_STARTUP_HIDE_DELAY: Duration = Duration::from_millis(100);

/// Let GPUI present the initial scene before Linux startup discovers and
/// unmaps the window.
///
/// The X11 window manager publishes the new client asynchronously, so looking
/// it up during application setup can miss it. The short foreground delay also
/// gives GPUI's initial frame time to reach the compositor before the window is
/// hidden, preserving tray-resident startup without leaving a transparent
/// surface when it is mapped again.
#[cfg(any(target_os = "linux", test))]
fn schedule_linux_window_hide_after_initial_paint(
window: &gpui::Window,
cx: &App,
hide: impl FnOnce() + 'static,
) {
window
.spawn(cx, async move |_| {
gpui::Timer::after(LINUX_STARTUP_HIDE_DELAY).await;
hide();
})
.detach();
}

/// Capacity for the clipboard event channel between the OS clipboard listener
/// and the persistence task. Large enough to absorb bursts from apps that copy
/// several times per second, while preventing unbounded memory growth if the
Expand Down Expand Up @@ -318,27 +346,72 @@ pub(crate) fn launch() {
}

#[cfg(target_os = "linux")]
if env::var("DISPLAY").is_ok() {
match X11::new() {
Ok(x11_new) => {
let x11 = X11_INSTANCE.get_or_init(|| x11_new);
let _ = x11.active_window();
}
Err(e) => {
tracing::error!(error = %e, "failed to connect x11rb; skipping X11 init");
}
}
if env::var("DISPLAY").is_ok()
&& let Err(error) = window_handle.update(cx, |_, window, cx| {
schedule_linux_window_hide_after_initial_paint(window, cx, || {
let x11 = match X11::new(MAIN_WINDOW_TITLE) {
Ok(x11) => X11_INSTANCE.get_or_init(|| x11),
Err(error) => {
tracing::error!(
error = %error,
"failed to initialize x11rb after initial paint"
);
return;
}
};

if let Err(error) = x11.hide_window() {
tracing::warn!(
error = %error,
"failed to hide Linux window after initial paint"
);
}
});
})
{
tracing::warn!(
error = %error,
"failed to schedule Linux startup window hide"
);
}
});
}

#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use std::{cell::Cell, rc::Rc, thread};

use gpui::TestAppContext;

use super::*;
use crate::repository::backend::memory::{MemoryBackend, memory_backend_factory};

#[gpui::test]
fn test_linux_startup_hide_waits_for_initial_paint_delay(cx: &mut TestAppContext) {
let hidden = Rc::new(Cell::new(false));
let hidden_after_frame = hidden.clone();
let visual_cx = cx.add_empty_window();

visual_cx.update(|window, cx| {
schedule_linux_window_hide_after_initial_paint(window, cx, move || {
hidden_after_frame.set(true);
});
});
visual_cx.run_until_parked();

assert!(
!hidden.get(),
"Linux startup must not unmap the window before the paint delay elapses"
);
thread::sleep(LINUX_STARTUP_HIDE_DELAY + Duration::from_millis(50));
visual_cx.run_until_parked();

assert!(
hidden.get(),
"Linux startup should hide the window after the initial paint delay"
);
}

fn create_test_repo() -> (tempfile::TempDir, ClipboardRepository<MemoryBackend>) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
Expand Down
25 changes: 21 additions & 4 deletions src/gui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ impl AssetSource for Assets {

/// Create the main application window.
///
/// The window is always created hidden — Ropy is a tray-resident clipboard
/// manager and is only revealed by the global hotkey or the tray menu,
/// regardless of how the process was launched.
/// Ropy starts tray-resident and is only revealed by the global hotkey or tray
/// menu. Linux must initially create the GPUI window as shown so its renderer
/// submits a real first frame; the application lifecycle hides it immediately
/// after that frame has reached X11.
pub(crate) fn create_window(
cx: &mut App,
shared_records: SharedRecords,
Expand All @@ -61,7 +62,8 @@ pub(crate) fn create_window(
window_bounds: Some(WindowBounds::Windowed(bounds)),
kind: WindowKind::PopUp,
titlebar: None,
show: false,
show: show_main_window_during_creation(cfg!(target_os = "linux")),
app_id: Some(MAIN_WINDOW_TITLE.to_owned()),
window_background: background_appearance_for_opacity(window_opacity_percent),
..Default::default()
},
Expand All @@ -82,6 +84,10 @@ pub(crate) fn create_window(
})
}

const fn show_main_window_during_creation(target_is_linux: bool) -> bool {
target_is_linux
}

const fn background_appearance_for_opacity(opacity_percent: u8) -> WindowBackgroundAppearance {
if opacity_percent < 100 {
WindowBackgroundAppearance::Transparent
Expand Down Expand Up @@ -137,3 +143,14 @@ pub(crate) fn set_app_theme(
theme.list_active = surface(rgb(palette.list_active).into());
theme.scrollbar_thumb = surface(rgb(palette.scrollbar_thumb).into());
}

#[cfg(test)]
mod tests {
use super::show_main_window_during_creation;

#[test]
fn linux_requests_initial_frame_before_startup_hide() {
assert!(show_main_window_during_creation(true));
assert!(!show_main_window_during_creation(false));
}
}
3 changes: 3 additions & 0 deletions src/gui/board/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,11 @@ impl RopyBoard {
self.active_panel = ActivePanel::ClipboardList;
self.ui_state.clear_confirm = crate::gui::board::ClearConfirmState::Hidden;
self.activated = true;
window.focus(&self.focus_handle);
reset_window_geometry_for_activation(window, default_window_size());
active_window(window, cx);
window.refresh();
cx.notify();
}

pub(crate) fn on_hide_action(
Expand Down
Loading
Loading