use gpui::{ AnyElement, App, Div, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement, Role, SharedString, Stateful, Styled, div, prelude::*, }; use super::{ menu::{Open, at_depth, choose, mark, measure}, model::{Entry, Kind, Menu, MenuItem}, }; use crate::{ forms::{Run, surface}, navigation::marked, primitives::{Icon, IconName, Tooltip}, theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize}, typography::KbdCombo, }; fn row(item: &MenuItem, ix: usize, current: bool, cx: &App) -> Stateful
{ let theme = cx.theme(); let colors = &theme.colors; let fg = if item.disabled { colors.fg_disabled } else { colors.fg }; let (lead, lead_color) = match &item.kind { Kind::Check(true) => (Some(IconName::Check), fg), Kind::Radio(true) => (Some(IconName::Dot), fg), _ => (item.icon, if item.disabled { fg } else { colors.fg_muted }), }; div() .id(("row", ix)) .relative() .flex() .items_center() .gap_2() .h(theme.control_height(ControlSize::Md)) .px_2() .rounded(theme.radius(Radius::Md)) .text_color(fg) .when(current && !item.disabled, |row| row.bg(colors.hover)) .when(!item.disabled, |row| row.cursor_pointer()) .child( div() .flex() .flex_none() .items_center() .justify_center() .size(theme.icon_size(IconSize::Sm)) .when_some(lead, |slot, icon| { slot.child(Icon::new(icon).size(IconSize::Sm).color(lead_color)) }), ) .child( div() .flex_1() .min_w_0() .overflow_hidden() .text_ellipsis() .whitespace_nowrap() .child(if item.hits.is_empty() { item.label.clone().into_any_element() } else { marked(item.label.clone(), item.hits.clone(), cx) }), ) .when_some(item.keys.clone(), |row, keys| { row.child( div() .flex_none() .pl_4() .text_color(colors.fg_subtle) .child(KbdCombo::new(&keys)), ) }) .when(matches!(item.kind, Kind::Sub(_)), |row| { row.child( Icon::new(IconName::ChevronRight) .size(IconSize::Xs) .color(colors.fg_subtle), ) }) } /// Button `column` of row `ix`, named by its label. fn button(item: &MenuItem, ix: usize, column: usize, current: bool, cx: &App) -> Stateful
{ let theme = cx.theme(); let colors = &theme.colors; let fg = match (item.disabled, current) { (true, _) => colors.fg_disabled, (false, true) => colors.fg, (false, false) => colors.fg_muted, }; let (label, keys) = (item.label.clone(), item.keys.clone()); let icon = item.icon.expect("a menu button has its icon"); div() .id((SharedString::from(format!("button-{ix}")), column)) .debug_selector(move || format!("menu-button-{ix}-{column}")) .role(Role::Button) .aria_label(item.label.clone()) .flex_1() .flex() .items_center() .justify_center() .h(theme.control_height(ControlSize::Md)) .rounded(theme.radius(Radius::Md)) .border_1() .border_color(colors.border) .when(current && !item.disabled, |button| button.bg(colors.hover)) .when(!item.disabled, |button| button.cursor_pointer()) .child(Icon::new(icon).size(IconSize::Sm).color(fg)) .tooltip(Tooltip::rich(move |_, cx| { div() .flex() .items_center() .gap_2() .child(label.clone()) .when_some(keys.clone(), |tip, keys| { tip.child( div() .text_color(cx.theme().colors.tooltip_fg.opacity(0.6)) .child(KbdCombo::new(&keys)), ) }) .into_any_element() })) } pub(super) fn panel( id: &ElementId, menu: &Menu, state: &Entity, depth: usize, close: &Run, head: Option, cx: &App, ) -> Stateful
{ let open = state.read(cx); let here = at_depth(menu, &open.levels, depth).expect("settled levels follow submenu rows"); let at = open.levels[depth].at; let theme = cx.theme(); let colors = &theme.colors; let rows = here .entries .iter() .enumerate() .map(|(ix, entry)| match entry { Entry::Separator => div().h_px().my_1().bg(colors.border).into_any_element(), Entry::Heading(title) => div() .px_2() .pt_1p5() .pb_1() .text_size(theme.text_size(TextSize::Xs)) .font_weight(FontWeight::MEDIUM) .text_color(colors.fg_subtle) .child(title.clone()) .into_any_element(), Entry::Buttons(buttons) => div() .flex() .gap_1() .py_0p5() .children(buttons.iter().enumerate().map(|(column, item)| { let current = at == Some(ix) && open.levels[depth].column == column; let (hover, click, close, menu) = (state.clone(), state.clone(), close.clone(), menu.clone()); let hover_menu = menu.clone(); button(item, ix, column, current, cx).when(!item.disabled, |button| { button .on_mouse_move(move |_, _, cx| { let open = hover.read(cx); let marked = open.levels.get(depth).map(|l| (l.at, l.column)); if marked != Some((Some(ix), column)) { mark(&hover, &hover_menu, depth, (ix, column), false, cx); } }) .on_mouse_down(MouseButton::Left, move |_, window, cx| { window.prevent_default(); cx.stop_propagation(); choose(&click, &menu, depth, (ix, column), &close, window, cx); }) }) })) .into_any_element(), Entry::Item(item) => { let marked = at == Some(ix); let sub = matches!(item.kind, Kind::Sub(_)); let (hover, click, close, menu) = (state.clone(), state.clone(), close.clone(), menu.clone()); let hover_menu = menu.clone(); row(item, ix, marked, cx) .when(!item.disabled, |row| { row.on_mouse_move(move |_, _, cx| { let open = hover.read(cx); let Some(level) = open.levels.get(depth) else { return; }; let shown = open.levels.len() > depth + 1; if level.at != Some(ix) || (sub && !shown) { mark(&hover, &hover_menu, depth, (ix, 0), true, cx); } }) .on_mouse_down( MouseButton::Left, move |_, window, cx| { window.prevent_default(); cx.stop_propagation(); choose(&click, &menu, depth, (ix, 0), &close, window, cx); }, ) }) .when(marked && sub, |row| { row.child(measure(state.clone(), move |open, bounds| { let level = &mut open.levels[depth]; let changed = level.row != bounds; level.row = bounds; changed })) }) .into_any_element() } }); surface((id.clone(), format!("panel-{depth}")), cx) .relative() // Presses here are the menu's, not its host's. .on_mouse_down(MouseButton::Left, |_, window, cx| { window.prevent_default(); cx.stop_propagation(); }) .min_w(theme.menu_width()) .p_1() .flex() .flex_col() .children(head) .children(rows) .child(measure(state.clone(), move |open, bounds| { let level = &mut open.levels[depth]; let changed = level.panel != bounds; level.panel = bounds; changed })) }