argui-platform 0.2.1

Native platform integrations for Argui applications
Documentation
use ksni::{
    blocking::{Handle, TrayMethods as _},
    menu,
};

use crate::{
    ApplicationId, IconSet, TrayAction, TrayConfig, TrayEvent, TrayItemId, TrayMenuItem,
    TrayPointerButton, native_tray::TrayEventHandler,
};

pub(super) struct PlatformTray {
    handle: Handle<LinuxTray>,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl PlatformTray {
    pub(super) fn new(
        application_id: &ApplicationId,
        config: TrayConfig,
        fallback_icons: &IconSet,
        handler: TrayEventHandler,
    ) -> Result<Self, String> {
        let tray = LinuxTray {
            application_id: application_id.as_str().to_owned(),
            config,
            fallback_icons: fallback_icons.clone(),
            handler,
        };
        let handle = tray
            .assume_sni_available(true)
            .spawn()
            .map_err(|error| error.to_string())?;
        Ok(Self { handle })
    }

    pub(super) fn sync(
        &mut self,
        config: TrayConfig,
        fallback_icons: &IconSet,
    ) -> Result<(), String> {
        self.handle
            .update(|tray| {
                tray.config = config;
                tray.fallback_icons = fallback_icons.clone();
            })
            .ok_or_else(|| "Linux tray service has stopped".to_owned())
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl Drop for PlatformTray {
    fn drop(&mut self) {
        self.handle.shutdown().wait();
    }
}

struct LinuxTray {
    application_id: String,
    config: TrayConfig,
    fallback_icons: IconSet,
    handler: TrayEventHandler,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl ksni::Tray for LinuxTray {
    fn id(&self) -> String {
        self.application_id.clone()
    }

    fn title(&self) -> String {
        self.config
            .title
            .clone()
            .or_else(|| self.config.tooltip.clone())
            .unwrap_or_default()
    }

    fn status(&self) -> ksni::Status {
        if self.config.visible {
            ksni::Status::Active
        } else {
            ksni::Status::Passive
        }
    }

    fn icon_pixmap(&self) -> Vec<ksni::Icon> {
        let icons = self.config.icon.as_ref().unwrap_or(&self.fallback_icons);
        icons
            .icons()
            .iter()
            .filter_map(|icon| {
                let width = i32::try_from(icon.width).ok()?;
                let height = i32::try_from(icon.height).ok()?;
                let mut argb = Vec::with_capacity(icon.rgba8.len());
                for rgba in icon.rgba8.as_chunks::<4>().0 {
                    argb.extend_from_slice(&[rgba[3], rgba[0], rgba[1], rgba[2]]);
                }
                Some(ksni::Icon {
                    width,
                    height,
                    data: argb,
                })
            })
            .collect()
    }

    fn activate(&mut self, _x: i32, _y: i32) {
        (self.handler)(TrayEvent::Click {
            button: TrayPointerButton::Primary,
            double: false,
        });
    }

    fn secondary_activate(&mut self, _x: i32, _y: i32) {
        (self.handler)(TrayEvent::Click {
            button: TrayPointerButton::Middle,
            double: false,
        });
    }

    fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
        linux_menu(&self.config.menu)
    }
}

fn linux_menu(items: &[TrayMenuItem]) -> Vec<ksni::MenuItem<LinuxTray>> {
    items
        .iter()
        .map(|item| match item {
            TrayMenuItem::Action {
                id,
                label,
                enabled,
                action,
            } => menu::StandardItem {
                label: label.clone(),
                enabled: *enabled,
                activate: activation(id.clone(), action.clone()),
                ..menu::StandardItem::default()
            }
            .into(),
            TrayMenuItem::Check {
                id,
                label,
                enabled,
                checked,
                action,
            } => menu::CheckmarkItem {
                label: label.clone(),
                enabled: *enabled,
                checked: *checked,
                activate: activation(id.clone(), action.clone()),
                ..menu::CheckmarkItem::default()
            }
            .into(),
            TrayMenuItem::Separator => ksni::MenuItem::Separator,
            TrayMenuItem::Submenu {
                label,
                enabled,
                items,
                ..
            } => menu::SubMenu {
                label: label.clone(),
                enabled: *enabled,
                submenu: linux_menu(items),
                ..menu::SubMenu::default()
            }
            .into(),
        })
        .collect()
}

fn activation(id: TrayItemId, action: TrayAction) -> Box<dyn Fn(&mut LinuxTray) + Send + Sync> {
    Box::new(move |tray| {
        (tray.handler)(TrayEvent::Action {
            id: id.clone(),
            action: action.clone(),
        });
    })
}