argui-platform 0.2.1

Native platform integrations for Argui applications
Documentation
use std::collections::HashSet;

use crate::{IconSet, WindowKey};

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TrayItemId(String);

impl TrayItemId {
    #[must_use]
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TrayAction {
    Custom(String),
    ShowWindow(WindowKey),
    HideWindow(WindowKey),
    ToggleWindow(WindowKey),
    FocusWindow(WindowKey),
    CloseWindow(WindowKey),
    Quit,
}

#[derive(Clone, Debug, PartialEq)]
pub enum TrayMenuItem {
    Action {
        id: TrayItemId,
        label: String,
        enabled: bool,
        action: TrayAction,
    },
    Check {
        id: TrayItemId,
        label: String,
        enabled: bool,
        checked: bool,
        action: TrayAction,
    },
    Separator,
    Submenu {
        id: TrayItemId,
        label: String,
        enabled: bool,
        items: Vec<TrayMenuItem>,
    },
}

impl TrayMenuItem {
    #[must_use]
    pub fn id(&self) -> Option<&TrayItemId> {
        match self {
            Self::Action { id, .. } | Self::Check { id, .. } | Self::Submenu { id, .. } => Some(id),
            Self::Separator => None,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct TrayConfig {
    pub tooltip: Option<String>,
    pub title: Option<String>,
    pub icon: Option<IconSet>,
    pub menu: Vec<TrayMenuItem>,
    pub visible: bool,
    pub icon_is_template: bool,
    pub show_menu_on_left_click: bool,
}

impl Default for TrayConfig {
    fn default() -> Self {
        Self {
            tooltip: None,
            title: None,
            icon: None,
            menu: Vec::new(),
            visible: true,
            icon_is_template: false,
            show_menu_on_left_click: true,
        }
    }
}

impl TrayConfig {
    pub fn validate(&self) -> Result<(), TrayConfigError> {
        let mut ids = HashSet::new();
        validate_items(&self.menu, &mut ids)
    }
}

fn validate_items(
    items: &[TrayMenuItem],
    ids: &mut HashSet<TrayItemId>,
) -> Result<(), TrayConfigError> {
    for item in items {
        let Some(id) = item.id() else {
            continue;
        };
        if id.as_str().is_empty() {
            return Err(TrayConfigError::EmptyItemId);
        }
        if !ids.insert(id.clone()) {
            return Err(TrayConfigError::DuplicateItemId(id.clone()));
        }
        if let TrayMenuItem::Submenu { items, .. } = item {
            validate_items(items, ids)?;
        }
    }
    Ok(())
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TrayConfigError {
    EmptyItemId,
    DuplicateItemId(TrayItemId),
}

impl std::fmt::Display for TrayConfigError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EmptyItemId => formatter.write_str("tray item ids cannot be empty"),
            Self::DuplicateItemId(id) => {
                write!(formatter, "duplicate tray item id: {}", id.as_str())
            }
        }
    }
}

impl std::error::Error for TrayConfigError {}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrayPointerButton {
    Primary,
    Secondary,
    Middle,
    Unknown,
}

#[derive(Clone, Debug, PartialEq)]
pub enum TrayEvent {
    Action {
        id: TrayItemId,
        action: TrayAction,
    },
    Click {
        button: TrayPointerButton,
        double: bool,
    },
}