use std::path::PathBuf;
use crate::error::Result;
use crate::geometry::{Edge, LogicalRect};
use crate::keynav::NavKey;
use crate::menu::{Icon, Menu, MenuId};
use crate::theme::MenuOptions;
use crate::Tray;
#[derive(Debug, Clone)]
pub enum SystemFontSource {
Data(Vec<u8>),
Path(PathBuf),
Family(String),
}
#[derive(Debug, Clone)]
pub struct SystemFont {
pub source: SystemFontSource,
pub point_size: f32,
}
impl SystemFont {
pub fn apply_size_to(&self, theme: &mut crate::theme::Theme) {
if self.point_size > 0.0 {
theme.row_font.size = self.point_size;
theme.header_font.size = self.point_size;
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemPalette {
pub label: Option<(u8, u8, u8, u8)>,
pub secondary_label: Option<(u8, u8, u8, u8)>,
pub separator: Option<(u8, u8, u8, u8)>,
pub background: Option<(u8, u8, u8, u8)>,
}
impl SystemPalette {
pub fn apply_to(&self, theme: &mut crate::theme::Theme) {
use crate::style::Color;
if let Some((r, g, b, a)) = self.label {
theme.label = Color::Rgba(r, g, b, a);
}
if let Some((r, g, b, a)) = self.secondary_label {
theme.secondary_label = Color::Rgba(r, g, b, a);
}
if let Some((r, g, b, a)) = self.separator {
theme.separator = Color::Rgba(r, g, b, a);
}
if let Some((r, g, b, a)) = self.background {
theme.background = Color::Rgba(r, g, b, a);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Appearance {
Light,
Dark,
}
impl Appearance {
pub fn is_dark(self) -> bool {
matches!(self, Appearance::Dark)
}
pub fn from_is_dark(is_dark: bool) -> Self {
if is_dark {
Appearance::Dark
} else {
Appearance::Light
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PlatformEvent {
TrayActivated,
Key(NavKey),
Dismissed,
}
pub trait Platform {
fn install_tray(&mut self, icon: &Icon, tooltip: Option<&str>) -> Result<()>;
fn tray_anchor_rect(&self) -> Result<LogicalRect>;
fn supports_tray_anchor(&self) -> bool;
fn cursor_position(&self) -> Option<crate::geometry::LogicalPoint> {
None
}
fn appearance(&self) -> Appearance;
fn system_menu_font(&self) -> Option<SystemFont> {
None
}
fn system_palette(&self) -> SystemPalette {
SystemPalette::default()
}
fn work_area(&self) -> LogicalRect;
fn run_tray(self, tray: Tray) -> Result<()>
where
Self: Sized;
fn spawn_tray(self, tray: Tray) -> Result<()>
where
Self: Sized,
{
let _ = tray;
Err(crate::error::Error::Platform(
"spawning a non-blocking tray is not implemented on this platform".into(),
))
}
fn open_popup_session(
&mut self,
menu: Menu,
options: MenuOptions,
on_click: &(dyn Fn(&MenuId) + '_),
anchor: LogicalRect,
edge: Edge,
) -> Result<()> {
let _ = (menu, options, on_click, anchor, edge);
Err(crate::error::Error::Platform(
"styled popup sessions are not implemented on this platform yet".into(),
))
}
}
#[cfg(target_os = "macos")]
pub mod mac;
#[cfg(target_os = "macos")]
pub use mac::MacPlatform as PlatformImpl;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use windows::WindowsPlatform as PlatformImpl;
#[cfg(all(unix, not(target_os = "macos")))]
pub mod linux;
#[cfg(all(unix, not(target_os = "macos")))]
pub use linux::LinuxPlatform as PlatformImpl;
pub fn current() -> PlatformImpl {
PlatformImpl::new()
}
#[cfg(all(target_os = "macos", feature = "muda-compat"))]
pub(crate) const PRIMARY_MOD_IS_SUPER: bool = true;
#[cfg(all(not(target_os = "macos"), feature = "muda-compat"))]
pub(crate) const PRIMARY_MOD_IS_SUPER: bool = false;
#[cfg(any(target_os = "windows", all(unix, not(target_os = "macos")), test))]
pub(crate) type InstallReport = std::sync::mpsc::Sender<Result<()>>;
#[cfg(any(target_os = "windows", all(unix, not(target_os = "macos")), test))]
pub(crate) fn spawn_tray_thread(
tray: Tray,
run: fn(Tray, &InstallReport) -> Result<()>,
) -> Result<()> {
use std::sync::mpsc;
let (report, wait) = mpsc::channel::<Result<()>>();
std::thread::Builder::new()
.name("muri-tray".to_owned())
.spawn(move || {
if let Err(e) = run(tray, &report) {
eprintln!("muri: tray thread exited with error: {e}");
}
})
.map_err(|e| crate::error::Error::ThreadSpawn(e.to_string()))?;
match wait.recv() {
Ok(result) => result,
Err(_) => Err(crate::error::Error::TrayInstall(
"muri tray thread exited before reporting the tray install".into(),
)),
}
}
#[cfg(test)]
mod handshake_tests {
use super::*;
use crate::error::Error;
use crate::menu::Icon;
fn dummy_tray() -> Tray {
Tray::new(Icon::Symbol("tray"))
}
#[test]
fn spawn_tray_thread_surfaces_a_reported_install_failure() {
fn failing(_tray: Tray, report: &InstallReport) -> Result<()> {
let _ = report.send(Err(Error::TrayInstall("forced install failure".into())));
Ok(())
}
let err = spawn_tray_thread(dummy_tray(), failing).unwrap_err();
assert!(
matches!(err, Error::TrayInstall(_)),
"a reported install failure must surface as Error::TrayInstall, got {err:?}"
);
}
#[test]
fn spawn_tray_thread_returns_ok_on_a_confirmed_install() {
fn ok_then_pump(_tray: Tray, report: &InstallReport) -> Result<()> {
let _ = report.send(Ok(()));
Ok(())
}
assert!(spawn_tray_thread(dummy_tray(), ok_then_pump).is_ok());
}
#[test]
fn spawn_tray_thread_does_not_hang_when_the_thread_dies_before_signalling() {
fn dies_silently(_tray: Tray, _report: &InstallReport) -> Result<()> {
Ok(())
}
let err = spawn_tray_thread(dummy_tray(), dies_silently).unwrap_err();
assert!(
matches!(err, Error::TrayInstall(_)),
"a thread that never signals must not hang; got {err:?}"
);
}
}