paneru 0.4.3

A sliding, tiling window manager for MacOS.
use bevy::ecs::entity::Entity;
use bevy::ecs::query::With;
use bevy::ecs::system::{Local, NonSendMut, Query};
use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2_app_kit::{NSColor, NSStatusBar, NSStatusItem, NSVariableStatusItemLength};
use objc2_core_foundation::CGFloat;
use objc2_foundation::NSString;
use tracing::warn;

use crate::ecs::ActiveWorkspaceMarker;
use crate::ecs::layout::LayoutStrip;

pub struct MenuBarManager {
    mtm: MainThreadMarker,
    status_bar: Retained<NSStatusBar>,
    status_item: Retained<NSStatusItem>,
    current_label: Option<String>,
}

const STATUS_ITEM_BACKGROUND_ALPHA: CGFloat = 0.18;
const STATUS_ITEM_CORNER_RADIUS: CGFloat = 5.0;

impl MenuBarManager {
    pub fn new(mtm: MainThreadMarker) -> Self {
        let status_bar = NSStatusBar::systemStatusBar();
        let status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength);
        status_item.setVisible(true);

        Self {
            mtm,
            status_bar,
            status_item,
            current_label: None,
        }
    }

    pub fn show_virtual_workspace(&mut self, virtual_index: u32) {
        let label = format_virtual_workspace_label(virtual_index);
        if self.current_label.as_deref() == Some(label.as_str()) {
            return;
        }

        let title = NSString::from_str(&label);
        let tooltip = NSString::from_str("Paneru virtual workspace");
        let Some(button) = self.status_item.button(self.mtm) else {
            warn!("unable to update menu bar: status item has no button");
            return;
        };

        button.setWantsLayer(true);
        if let Some(layer) = button.layer() {
            let background = NSColor::controlAccentColor()
                .colorWithAlphaComponent(STATUS_ITEM_BACKGROUND_ALPHA)
                .CGColor();
            layer.setBackgroundColor(Some(&background));
            layer.setCornerRadius(STATUS_ITEM_CORNER_RADIUS);
            layer.setMasksToBounds(true);
        }
        button.setTitle(&title);
        button.setToolTip(Some(&tooltip));
        self.current_label = Some(label);
    }
}

impl Drop for MenuBarManager {
    fn drop(&mut self) {
        self.status_bar.removeStatusItem(&self.status_item);
    }
}

pub fn update_virtual_workspace_status_item(
    active_workspace: Query<(Entity, &LayoutStrip), With<ActiveWorkspaceMarker>>,
    menu_bar: Option<NonSendMut<MenuBarManager>>,
    mut displayed_workspace: Local<Option<(Entity, u32)>>,
) {
    let Some(mut menu_bar) = menu_bar else {
        return;
    };
    let Some((entity, strip)) = active_workspace.iter().next() else {
        return;
    };

    let next = (entity, strip.virtual_index);
    if displayed_workspace
        .as_ref()
        .is_some_and(|displayed| *displayed == next)
    {
        return;
    }

    menu_bar.show_virtual_workspace(strip.virtual_index);
    *displayed_workspace = Some(next);
}

pub(crate) fn format_virtual_workspace_label(virtual_index: u32) -> String {
    format!("VW {}", virtual_index + 1)
}

#[cfg(test)]
mod tests {
    use super::format_virtual_workspace_label;

    #[test]
    fn label_is_one_based() {
        assert_eq!(format_virtual_workspace_label(0), "VW 1");
        assert_eq!(format_virtual_workspace_label(4), "VW 5");
    }
}