cydonia 0.1.0

Desktop workspace for the ACP agents you run, keeping what they produce as files on your disk
Documentation
//! The `···` and `+` menus every sidebar row and column heading opens, and the
//! one field that says which of them is showing.

use crate::view::{root::Cydonia, sidebar::Row};
use bezel::{
    gpui::{self, AnyElement, Context, Div, SharedString, Stateful, Window, prelude::*, px},
    theme::Theme,
    ui::{
        menu::{self, Item},
        widgets::Buttons,
    },
};

/// What a row does when it is picked.
type Act = Box<dyn Fn(&mut Cydonia, &mut Window, &mut Context<Cydonia>)>;

/// Which menu is open. One field rather than a flag each, so opening one
/// closes the rest by construction.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Menu {
    /// The `+` on a project heading: what to start here.
    Add(usize),
    /// A project heading: what to do to the project.
    Project(usize),
    /// The `···` on an entry's row, whichever kind it is.
    Entry(Row),
    /// The kind picker above the projects.
    Filter,
    /// The `···` on a table's column heading.
    Column(usize),
}

/// One row of a menu, and what picking it does.
pub(crate) fn row(
    item: Item,
    act: impl Fn(&mut Cydonia, &mut Window, &mut Context<Cydonia>) + 'static,
) -> (Item, Act) {
    (item, Box::new(act))
}

impl Cydonia {
    /// Open a menu, or shut the one already open.
    ///
    /// The press that reaches a trigger is the same press the open card's
    /// `on_mouse_down_out` closes it on, so by click time the menu already
    /// reads as shut and a plain toggle would open it straight back. What the
    /// press found is noted by [`Cydonia::menu_press`] instead, in the capture
    /// phase — ahead of that handler, whichever element owns it.
    pub(crate) fn toggle_menu(&mut self, menu: Menu, cx: &mut Context<Self>) {
        let closed_by_this_press = std::mem::take(&mut self.menu_pressed);
        self.menu = (!closed_by_this_press && self.menu != Some(menu)).then_some(menu);
        cx.notify();
    }

    /// Note, on the way down, whether the press landed on the trigger of the
    /// menu that is open. Every trigger claims this, so the note is written
    /// afresh on each press and can never be read stale.
    pub(crate) fn menu_press(
        &self,
        el: Stateful<Div>,
        menu: Menu,
        cx: &Context<Self>,
    ) -> Stateful<Div> {
        el.capture_any_mouse_down(cx.listener(move |this, _, _, _| {
            this.menu_pressed = this.menu == Some(menu);
        }))
    }

    /// A `···` or `+` that opens `menu`, revealed on the row's hover.
    pub(crate) fn menu_button(
        &self,
        id: impl Into<gpui::ElementId>,
        group: &'static str,
        mark: impl IntoElement,
        menu: Menu,
        cx: &Context<Self>,
    ) -> Stateful<Div> {
        let button = Theme::of(cx)
            .ghost(id)
            .flex_none()
            .relative()
            // An open menu keeps its trigger on show — by then the pointer is
            // over the menu, not the row that opened it.
            .when(self.menu != Some(menu), |el| {
                el.invisible().group_hover(group, |el| el.visible())
            })
            .p(px(3.))
            .child(mark)
            .on_click(cx.listener(move |this, _, _, cx| {
                cx.stop_propagation();
                this.toggle_menu(menu, cx);
            }));
        self.menu_press(button, menu, cx)
    }

    /// The card every sidebar menu hangs in, dismissed by a press outside it.
    pub(crate) fn menu_card(
        &self,
        id: impl Into<SharedString>,
        rows: Vec<(Item, Act)>,
        cx: &mut Context<Self>,
    ) -> AnyElement {
        let theme = Theme::of(cx).clone();
        let (items, acts): (Vec<Item>, Vec<Act>) = rows.into_iter().unzip();
        menu::card(&theme, id, &items, None, cx, move |this, ix, window, cx| {
            this.menu = None;
            acts[ix](this, window, cx);
            cx.notify();
        })
        .on_mouse_down_out(cx.listener(|this, _, _, cx| {
            this.menu = None;
            cx.notify();
        }))
        .into_any_element()
    }
}