Skip to content
Merged
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
69 changes: 69 additions & 0 deletions gpui/Cargo.lock

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

1 change: 1 addition & 0 deletions gpui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gpui-pre-apple = { path = "vendor/gpui-pre-apple" }

[target.'cfg(windows)'.dependencies]
winreg = "0.52"
tray-icon = { version = "0.24", default-features = false }
raw-window-handle = "0.6"

# GPUI's layout and text work is very slow unoptimized (a plain scrolling page
Expand Down
15 changes: 12 additions & 3 deletions gpui/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,19 @@ fn main() {
}) {
view.update(cx, |view, cx| view.set_page(page, cx));
}
// Started by the OS at login (see autostart.rs): stay out of the way.
// There is no tray yet, so minimized is the quiet option.
// Closing the window hides it to the tray icon (where there is one).
let has_tray = openmouse_gpui::tray::install(cx, handle.into());
// Started by the OS at login (see autostart.rs): stay out of the way,
// in the tray if there is one, else minimized.
if openmouse_gpui::autostart::launched_by_os() {
let _ = cx.update_window(handle, |_, window, _| window.minimize_window());
let _ = cx.update_window(handle, |_, window, _| {
if has_tray {
#[cfg(windows)]
openmouse_gpui::tray::hide(window);
} else {
window.minimize_window();
}
});
// A window that never became active reports no deactivation, so
// say so here or the background scans would stay at full rate.
openmouse_gpui::activity::ACTIVITY.set_active(false);
Expand Down
1 change: 1 addition & 0 deletions gpui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod native_window;
pub mod overlay;
pub mod presence_text;
pub mod proc;
pub mod tray;
pub mod profiles;
pub mod settings;
pub mod theme;
Expand Down
141 changes: 141 additions & 0 deletions gpui/src/tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! System tray: closing the main window hides it to a tray icon instead of
//! quitting, and the icon brings it back. Quitting is the tray menu's job.
//!
//! Windows only for now; elsewhere [`install`] does nothing and closing the
//! window quits as before.

#[cfg(windows)]
mod imp {
use futures::channel::mpsc;
use futures::StreamExt as _;
use gpui_kit::{AnyWindowHandle, App, AppContext as _, Window};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use tray_icon::menu::{Menu, MenuEvent, MenuItem, PredefinedMenuItem};
use tray_icon::{Icon, MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};

use crate::activity::ACTIVITY;

#[link(name = "user32")]
extern "system" {
fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
fn SetForegroundWindow(hwnd: isize) -> i32;
}

const SW_HIDE: i32 = 0;
/// Shows a hidden window and un-minimizes a minimized one.
const SW_RESTORE: i32 = 9;
/// The app icon `build.rs` embeds as the exe's first icon resource.
const APP_ICON_RESOURCE: u16 = 1;

enum Command {
Show,
Quit,
}

fn hwnd_of(window: &Window) -> Option<isize> {
match HasWindowHandle::window_handle(window).map(|handle| handle.as_raw()) {
Ok(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get()),
_ => None,
}
}

/// Hides the window (no taskbar button either) and tells the background
/// loops nobody is looking.
pub fn hide(window: &Window) {
if let Some(hwnd) = hwnd_of(window) {
unsafe {
ShowWindow(hwnd, SW_HIDE);
}
}
ACTIVITY.set_active(false);
}

fn show(window: &mut Window) {
if let Some(hwnd) = hwnd_of(window) {
unsafe {
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
}
}
window.activate_window();
ACTIVITY.set_active(true);
}

/// Creates the tray icon and makes closing `window` hide it. Returns
/// whether the tray exists; without one, closing still quits.
pub fn install(cx: &mut App, window: AnyWindowHandle) -> bool {
let Ok(icon) = Icon::from_resource(APP_ICON_RESOURCE, Some((32, 32))) else {
return false;
};
let open = MenuItem::with_id("open", "Open OpenMouse", true, None);
let quit = MenuItem::with_id("quit", "Quit", true, None);
let menu = Menu::new();
if menu.append_items(&[&open, &PredefinedMenuItem::separator(), &quit]).is_err() {
return false;
}
let Ok(tray) = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("OpenMouse Desktop")
.with_menu(Box::new(menu))
.with_menu_on_left_click(false)
.build()
else {
return false;
};

// The handlers run inside the tray's window procedure on the main
// thread; they only queue a command for the task below.
let (commands, mut queue) = mpsc::unbounded();
let menu_commands = commands.clone();
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
let command = match event.id.as_ref() {
"open" => Command::Show,
"quit" => Command::Quit,
_ => return,
};
let _ = menu_commands.unbounded_send(command);
}));
TrayIconEvent::set_event_handler(Some(move |event: TrayIconEvent| {
let clicked = matches!(
event,
TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. }
| TrayIconEvent::DoubleClick { button: MouseButton::Left, .. }
);
if clicked {
let _ = commands.unbounded_send(Command::Show);
}
}));

// Owns the icon, so it lives as long as the app does.
cx.spawn(async move |cx| {
let _tray = tray;
while let Some(command) = queue.next().await {
match command {
Command::Show => {
let _ = cx.update_window(window, |_, window, _| show(window));
}
Command::Quit => {
let _ = cx.update(|cx| cx.quit());
}
}
}
})
.detach();

let _ = cx.update_window(window, |_, window, cx| {
window.on_window_should_close(cx, |window, _| {
hide(window);
false
});
});
true
}
}

#[cfg(windows)]
pub use imp::{hide, install};

#[cfg(not(windows))]
pub fn install(_cx: &mut gpui_kit::App, _window: gpui_kit::AnyWindowHandle) -> bool {
false
}
Loading