Skip to content
Open
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
27 changes: 26 additions & 1 deletion Cargo.lock

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

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ url = { version = "2.5" }

# GUI
async-compat = { version = "0.2" }
i-slint-backend-winit = "1.15"
i-slint-backend-winit = "1.17"
open = "5.3"

reqwest = { version = "0.13", features = [
Expand All @@ -58,7 +58,7 @@ reqwest = { version = "0.13", features = [
"json",
] }

slint = { version = "1.15", default-features = false, features = [
slint = { version = "1.17", default-features = false, features = [
"accessibility",
"backend-winit",
"compat-1-2",
Expand All @@ -67,6 +67,7 @@ slint = { version = "1.15", default-features = false, features = [
"renderer-software",
"serde",
"std",
"system-tray",
] }

sys-locale = "0.3"
Expand All @@ -78,5 +79,5 @@ winit = "0.30"
build-target = "0.8"
git-version = "0.3"
rustc_version = "0.4"
slint-build = "1.15"
slint-build = "1.17"
winres = "0.1"
2 changes: 2 additions & 0 deletions crates/desktop/src/bridges/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod settings;
pub mod system_info;
pub mod tray;
pub mod ui_state;
pub mod window_control;

Expand All @@ -9,4 +10,5 @@ pub fn setup(window: &MainWindow) {
window_control::setup(window);
system_info::setup(window);
ui_state::setup(window);
tray::setup(window);
}
29 changes: 29 additions & 0 deletions crates/desktop/src/bridges/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,42 @@ pub fn load_config(window: &MainWindow) {
bridge.set_language(config.language.into());
bridge.set_running_in_tray(config.running_in_tray);

if config.running_in_tray {
crate::bridges::tray::ensure_created(window);
}

let window_clone = window.as_weak();
bridge.on_change_language(move |lang| {
let window = window_clone.upgrade().unwrap();
let bridge = window.global::<SettingsBridge>();
bridge.set_language(lang.clone());

slint::select_bundled_translation(lang.as_str()).ok();

// Refresh the tray menu labels to match the new language.
if crate::bridges::tray::is_created() {
crate::bridges::tray::destroy();
crate::bridges::tray::ensure_created(&window);
}
});

let window_clone = window.as_weak();
bridge.on_toggle_running_in_tray(move || {
let window = match window_clone.upgrade() {
Some(w) => w,
None => return,
};
let bridge = window.global::<SettingsBridge>();
let new_val = !bridge.get_running_in_tray();
bridge.set_running_in_tray(new_val);

if new_val {
crate::bridges::tray::ensure_created(&window);
} else {
crate::bridges::tray::destroy();
}

save_config(&window);
});
}

Expand Down
93 changes: 93 additions & 0 deletions crates/desktop/src/bridges/tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use std::cell::RefCell;

use i_slint_backend_winit::WinitWindowAccessor;
use slint::ComponentHandle;
use tracing::{debug, error};

use crate::ui::{MainWindow, SettingsBridge, WsrxTray};

thread_local! {
static TRAY: RefCell<Option<WsrxTray>> = const { RefCell::new(None) };
}

/// Localised labels for the tray context menu, picked from the active UI
/// language so the tray matches the rest of the interface.
fn localized_labels(language: &str) -> (&'static str, &'static str) {
match language {
"zh_CN" => ("显示窗口", "退出"),
"zh_TW" => ("顯示視窗", "退出"),
_ => ("Show", "Quit"),
}
}

/// Restore and focus the main window.
fn show_window(window_weak: &slint::Weak<MainWindow>) {
if let Some(window) = window_weak.upgrade() {
window.window().with_winit_window(|winit_window| {
// Restore the window that was hidden via winit::Window::set_visible(false).
winit_window.set_visible(true);
winit_window.set_minimized(false);
winit_window.focus_window();
});
}
}

/// No global setup is required when using Slint's built-in [`SystemTrayIcon`].
/// The tray instance is created on demand in [`ensure_created`].
pub fn setup(_window: &MainWindow) {}

/// Create the tray icon if it does not already exist. Safe to call
/// repeatedly — subsequent calls are no-ops.
pub fn ensure_created(window: &MainWindow) {
let already_exists = TRAY.with(|t| t.borrow().is_some());
if already_exists {
return;
}

let language = window.global::<SettingsBridge>().get_language().to_string();
let (show_label, quit_label) = localized_labels(&language);
let window_weak = window.as_weak();

let tray = match WsrxTray::new() {
Ok(tray) => tray,
Err(e) => {
error!("Failed to create system tray icon: {e}");
return;
}
};

tray.set_show_label(show_label.into());
tray.set_quit_label(quit_label.into());

let weak = window_weak.clone();
tray.on_show_window(move || show_window(&weak));

let weak = window_weak;
tray.on_quit(move || crate::launcher::shutdown(&weak));

if let Err(e) = tray.show() {
error!("Failed to show system tray icon: {e}");
return;
}

TRAY.with(|t| {
*t.borrow_mut() = Some(tray);
});
debug!("System tray icon created");
}

/// Hide and drop the tray icon if it exists. Dropping the [`WsrxTray`]
/// instance removes it from the system tray.
pub fn destroy() {
TRAY.with(|t| {
if t.borrow().is_some() {
debug!("Destroying system tray icon");
}
*t.borrow_mut() = None;
});
}

/// Whether a tray icon is currently active.
pub fn is_created() -> bool {
TRAY.with(|t| t.borrow().is_some())
}
44 changes: 40 additions & 4 deletions crates/desktop/src/bridges/window_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,36 @@ use winit::window::ResizeDirection;

use crate::{
launcher,
ui::{MainWindow, WindowControlBridge},
ui::{MainWindow, SettingsBridge, WindowControlBridge},
};

/// Hide the window into the system tray when the user has enabled that
/// behaviour. Returns `true` when the window was hidden (the close event
/// has been consumed), `false` when the application should proceed to shut
/// down.
fn hide_to_tray(window: &MainWindow) -> bool {
let settings = window.global::<SettingsBridge>();
if !settings.get_running_in_tray() {
return false;
}

// Make sure the tray icon exists before hiding the window, otherwise
// the user would have no way to bring the application back.
crate::bridges::tray::ensure_created(window);
if !crate::bridges::tray::is_created() {
return false;
}

window.window().with_winit_window(|winit_window| {
// Use the winit API directly instead of Slint's `hide()`. On some
// backends `Window::hide()` is treated as a close, which would make
// `ui.run()` return and terminate the process.
winit_window.set_visible(false);
});
crate::bridges::settings::save_config(window);
true
}

pub fn setup(window: &MainWindow) {
let mut resize_map = HashMap::new();
resize_map.insert("r".to_string(), ResizeDirection::East);
Expand Down Expand Up @@ -36,7 +63,11 @@ pub fn setup(window: &MainWindow) {
EventResult::Propagate
}
winit::event::WindowEvent::CloseRequested => {
launcher::shutdown(&window_weak);
if let Some(window) = window_weak.upgrade() {
if !hide_to_tray(&window) {
launcher::shutdown(&window_weak);
}
}
EventResult::PreventDefault
}
_ => EventResult::Propagate,
Expand Down Expand Up @@ -65,8 +96,13 @@ pub fn setup(window: &MainWindow) {

let window_clone_pin = window.as_weak();
window_control_bridge.on_close(move || {
// TODO: system tray implementation
launcher::shutdown(&window_clone_pin);
let window = match window_clone_pin.upgrade() {
Some(w) => w,
None => return,
};
if !hide_to_tray(&window) {
launcher::shutdown(&window_clone_pin);
}
});

let window_clone_pin = window.as_weak();
Expand Down
10 changes: 7 additions & 3 deletions crates/desktop/src/daemon/api_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ async fn launch_instance(
if instance.label != instance_data.label {
instance.label = instance_data.label.clone();
}

return Ok(Json(instance.data.clone()));
let data = instance.data.clone();
drop(instances);
super::save_instances(&state).await;
return Ok(Json(data));
}

let instance = ProxyInstance::new(
Expand All @@ -196,6 +198,7 @@ async fn launch_instance(
let instance_resp: InstanceData = (&instance).into();
instances.push(instance);
drop(instances);
super::save_instances(&state).await;

let state_clone = state.clone();
let instance = instance_resp.clone();
Expand Down Expand Up @@ -273,6 +276,7 @@ async fn close_instance(
}

instances.retain(|i| i.local.as_str() != req.local);
super::save_instances(&state).await;

match slint::invoke_from_event_loop(move || {
let ui_handle = state.ui.upgrade().unwrap();
Expand Down Expand Up @@ -468,8 +472,8 @@ async fn popup_window(
) -> Result<impl IntoResponse, (StatusCode, String)> {
slint::invoke_from_event_loop(move || {
let ui_handle = state.ui.upgrade().unwrap();
ui_handle.show().ok();
ui_handle.window().with_winit_window(|winit_window| {
winit_window.set_visible(true);
winit_window.set_minimized(false);
});
})
Expand Down
Loading