Skip to content

Commit

Permalink
Introduce abstract, platform generic tray icon
Browse files Browse the repository at this point in the history
  • Loading branch information
dastansam committed Aug 23, 2024
1 parent 56d7e33 commit 33ff0a4
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 161 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Install GTK4 (Ubuntu)
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libgtk-3-dev libxdo-dev libappindicator3-dev libdbus-1-dev pkg-config
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libgtk-3-dev
if: runner.os == 'Linux'

- name: Install GTK4 (macOS)
Expand Down Expand Up @@ -165,7 +165,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Install GTK4 (Ubuntu)
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libgtk-3-dev libxdo-dev libappindicator3-dev libdbus-1-dev pkg-config
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libgtk-3-dev
if: runner.os == 'Linux'

- name: Install GTK4 (macOS)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ futures = "0.3.30"
futures-timer = "3.0.3"
gtk = { version = "0.7.3", package = "gtk4" }
hex = "0.4.3"
image = { version = "0.25", features = ["png"] }
image = { version = "0.25", features = ["png"], default-features = false }
mimalloc = "0.1.41"
names = "0.14.0"
notify-rust = { version = "4.11.1", features = ["images"] }
Expand Down
288 changes: 130 additions & 158 deletions src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use gtk::glib;
use gtk::prelude::*;
use image::{ImageBuffer, Rgba};
use notify_rust::Notification;
use relm4::actions::{RelmAction, RelmActionGroup};
use relm4::prelude::*;
Expand All @@ -33,10 +34,6 @@ use tracing::{debug, error, warn};
pub const GLOBAL_CSS: &str = include_str!("../res/app.css");
const ICON: &[u8] = include_bytes!("../res/icon.png");
const ABOUT_IMAGE: &[u8] = include_bytes!("../res/about.png");
#[cfg(any(target_os = "windows", target_os = "macos"))]
const TRAY_ICON_MENU_CLOSE_ID: &str = "tray_icon_close";
#[cfg(any(target_os = "windows", target_os = "macos"))]
const TRAY_ICON_MENU_OPEN_ID: &str = "tray_icon_open";

#[cfg(all(unix, not(target_os = "macos")))]
#[thread_local]
Expand Down Expand Up @@ -100,138 +97,123 @@ impl NotificationExt for Notification {
}
}

/// Trait for platform specific tray icons
trait TrayIconTrait {
/// Initialize the tray icon
fn init() -> Result<GenericTrayIcon, ()>;
/// Set the sender to send inputs to the app
#[cfg(all(unix, not(target_os = "macos")))]
fn set_sender(&mut self, sender: AsyncComponentSender<App>);
}

/// Tray icon for Linux excluding macOS
#[cfg(all(unix, not(target_os = "macos")))]
struct LinuxTrayIcon {
icon: ksni::Icon,
sender: Option<AsyncComponentSender<App>>,
pub(crate) fn load_icon() -> ImageBuffer<Rgba<u8>, Vec<u8>> {
image::load_from_memory_with_format(ICON, image::ImageFormat::Png)
.expect("Statically correct image; qed")
.to_rgba8()
}

#[cfg(all(unix, not(target_os = "macos")))]
impl ksni::Tray for LinuxTrayIcon {
fn id(&self) -> String {
env!("CARGO_PKG_NAME").to_string()
mod generic_tray_icon {
use super::{load_icon, App, AppInput, T};
use relm4::AsyncComponentSender;

#[derive(Clone)]
pub struct TrayIcon {
icon: ksni::Icon,
sender: AsyncComponentSender<App>,
}

fn icon_name(&self) -> String {
"Space Acres".to_string()
}
impl TrayIcon {
pub(crate) fn init(sender: AsyncComponentSender<App>) -> Result<Self, ()> {
let icon_img = load_icon();

fn title(&self) -> String {
"Space Acres".to_string()
}
let (width, height) = icon_img.dimensions();

fn icon_pixmap(&self) -> Vec<ksni::Icon> {
vec![self.icon.clone()]
}
let icon = Self {
icon: ksni::Icon {
width: width as i32,
height: height as i32,
data: icon_img.into_raw().to_vec(),
},
sender,
};

fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: "Space Acres".to_string(),
..Default::default()
let tray_service = ksni::TrayService::new(icon.clone());

tray_service.spawn();

Ok(icon)
}
}

fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::*;
#[cfg(all(unix, not(target_os = "macos")))]
impl ksni::Tray for TrayIcon {
fn id(&self) -> String {
env!("CARGO_PKG_NAME").to_string()
}

vec![
StandardItem {
label: T.tray_icon_open().to_string(),
icon_data: self.icon.data.clone(),
activate: Box::new(|this: &mut Self| {
if let Some(sender) = this.sender.as_ref() {
sender.input(AppInput::ShowWindow);
}
}),
..Default::default()
}
.into(),
StandardItem {
label: T.tray_icon_close().to_string(),
icon_data: self.icon.data.clone(),
activate: Box::new(|this: &mut Self| {
if let Some(sender) = this.sender.as_ref() {
sender.input(AppInput::HideWindow);
}
}),
fn icon_name(&self) -> String {
"Space Acres".to_string()
}

fn title(&self) -> String {
"Space Acres".to_string()
}

fn icon_pixmap(&self) -> Vec<ksni::Icon> {
vec![self.icon.clone()]
}

fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: "Space Acres".to_string(),
..Default::default()
}
.into(),
]
}

fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::*;

vec![
StandardItem {
label: T.tray_icon_open().to_string(),
icon_data: self.icon.data.clone(),
activate: Box::new(|this: &mut Self| {
this.sender.input(AppInput::ShowWindow);
}),
..Default::default()
}
.into(),
StandardItem {
label: T.tray_icon_close().to_string(),
icon_data: self.icon.data.clone(),
activate: Box::new(|this: &mut Self| {
this.sender.input(AppInput::HideWindow);
}),
..Default::default()
}
.into(),
]
}
}
}

/// Tray icon for Windows and macOS
#[cfg(any(target_os = "windows", target_os = "macos"))]
struct OtherTrayIcon {
_inner: tray_icon::TrayIcon,
}

/// Variant for tray icons
enum PlatformTrayIcon {
#[cfg(all(unix, not(target_os = "macos")))]
Linux(LinuxTrayIcon),
#[cfg(any(target_os = "windows", target_os = "macos"))]
Other { _inner: OtherTrayIcon },
}

/// Generic tray icon wrapper
struct GenericTrayIcon {
_inner: PlatformTrayIcon,
}
mod generic_tray_icon {
use super::{load_icon, App, AppInput, T};
use relm4::AsyncComponentSender;
use tracing::warn;

pub struct TrayIcon {
_icon: tray_icon::TrayIcon,
sender: AsyncComponentSender<App>,
}

impl TrayIconTrait for GenericTrayIcon {
fn init() -> Result<Self, ()> {
let icon_img = image::load_from_memory_with_format(ICON, image::ImageFormat::Png)
.expect("Statically correct image; qed")
.to_rgba8();
impl TrayIcon {
pub(crate) fn init(sender: AsyncComponentSender<App>) -> Result<Self, ()> {
let icon_img = load_icon();

let (width, height) = icon_img.dimensions();
let (width, height) = icon_img.dimensions();

#[cfg(all(unix, not(target_os = "macos")))]
{
Ok(Self {
_inner: PlatformTrayIcon::Linux(LinuxTrayIcon {
icon: ksni::Icon {
width: width as i32,
height: height as i32,
data: icon_img.into_raw().to_vec(),
},
sender: None,
}),
})
}
let menu_open = &tray_icon::menu::MenuItem::new(&*T.tray_icon_open(), true, None);
let menu_close = &tray_icon::menu::MenuItem::new(&*T.tray_icon_close(), true, None);

#[cfg(any(target_os = "windows", target_os = "macos"))]
{
let menu = tray_icon::menu::Menu::with_items(&[
&tray_icon::menu::MenuItem::with_id(
TRAY_ICON_MENU_OPEN_ID,
T.tray_icon_open().to_string(),
true,
None,
),
&tray_icon::menu::MenuItem::with_id(
TRAY_ICON_MENU_CLOSE_ID,
T.tray_icon_close().to_string(),
true,
None,
),
])
.inspect_err(|error| {
warn!(%error, "Unable to create tray icon menu");
})
.map_err(|_| ())?;
let menu = tray_icon::menu::Menu::with_items(&[menu_open, menu_close])
.inspect_err(|error| {
warn!(%error, "Unable to create tray icon menu");
})
.map_err(|_| ())?;

let icon = tray_icon::TrayIconBuilder::new()
.with_tooltip("Space Acres")
Expand All @@ -240,23 +222,39 @@ impl TrayIconTrait for GenericTrayIcon {
.expect("Statically correct image; qed"),
)
.with_menu(std::boxed::Box::new(menu))
.build();
.build()
.map_err(|error| {
warn!(%error, "Unable to create tray icon");
})?;

icon.map_err(|error| {
warn!(%error, "Unable to create tray icon");
})
.map(|inner| Self {
_inner: PlatformTrayIcon::Other {
_inner: OtherTrayIcon { _inner: inner },
},
})
}
}
let menu_event = tray_icon::menu::MenuEvent::receiver();
let menu_close_id = menu_close.id().clone();
let menu_open_id = menu_open.id().clone();

#[cfg(all(unix, not(target_os = "macos")))]
fn set_sender(&mut self, _sender: AsyncComponentSender<App>) {
let PlatformTrayIcon::Linux(inner) = &mut self._inner;
inner.sender = Some(_sender);
let tray_icon = Self {
_icon: icon,
sender,
};
let sender = tray_icon.sender.clone();

sender.clone().spawn_command(move |_sender| {
while let Ok(event) = menu_event.recv() {
let input = if event.id == menu_open_id {
Some(AppInput::ShowWindow)
} else if event.id == menu_close_id {
Some(AppInput::HideWindow)
} else {
None
};

if let Some(input) = input {
sender.input(input);
}
}
});

Ok(tray_icon)
}
}
}

Expand Down Expand Up @@ -420,7 +418,7 @@ pub struct App {
backend_fut: Option<Box<dyn Future<Output = ()> + Send>>,
// Keep it around so it doesn't disappear
#[do_not_track]
_tray_icon: Option<GenericTrayIcon>,
_tray_icon: Option<generic_tray_icon::TrayIcon>,
}

#[relm4::component(pub async)]
Expand Down Expand Up @@ -743,19 +741,13 @@ impl AsyncComponent for App {
})
.transient_for(&root)
.build();

about_dialog.connect_close_request(|about_dialog| {
about_dialog.hide();
glib::Propagation::Stop
});

let input_sender = sender.clone();
let tray_icon = GenericTrayIcon::init().ok();

#[cfg(all(unix, not(target_os = "macos")))]
let tray_icon = tray_icon.map(|mut tray_icon| {
tray_icon.set_sender(input_sender);
tray_icon
});
let tray_icon = generic_tray_icon::TrayIcon::init(sender.clone()).ok();

let has_tray_icon = tray_icon.is_some();

Expand Down Expand Up @@ -876,26 +868,6 @@ impl AsyncComponent for App {
}
}
});

#[cfg(any(target_os = "windows", target_os = "macos"))]
let menu_event = tray_icon::menu::MenuEvent::receiver();

#[cfg(any(target_os = "windows", target_os = "macos"))]
sender.spawn_command(move |_sender| {
while let Ok(event) = menu_event.recv() {
let input = if event.id == TRAY_ICON_MENU_OPEN_ID {
Some(AppInput::ShowWindow)
} else if event.id == TRAY_ICON_MENU_CLOSE_ID {
Some(AppInput::HideWindow)
} else {
None
};

if let Some(input) = input {
input_sender.input(input);
}
}
});
}

AsyncComponentParts { model, widgets }
Expand Down

0 comments on commit 33ff0a4

Please sign in to comment.