2356 lines · 85.0 KB · Rust
RawDownload
use crate::{    IconButtonShape, KeyBinding, List, ListItem, ListSeparator, ListSubHeader, Tooltip, prelude::*,    utils::WithRemSize,};use gpui::{    Action, AnyElement, App, Bounds, Corner, DismissEvent, Entity, EventEmitter, FocusHandle,    Focusable, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Size,    Subscription, anchored, canvas, prelude::*, px,};use menu::{SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious};use settings::Settings;use std::{    cell::{Cell, RefCell},    collections::HashMap,    rc::Rc,    time::{Duration, Instant},};use theme::ThemeSettings; #[derive(Copy, Clone, Debug, PartialEq, Eq)]enum SubmenuOpenTrigger {    Pointer,    Keyboard,} struct OpenSubmenu {    item_index: usize,    entity: Entity<ContextMenu>,    trigger_bounds: Option<Bounds<Pixels>>,    offset: Option<Pixels>,    _dismiss_subscription: Subscription,} enum SubmenuState {    Closed,    Open(OpenSubmenu),} #[derive(Clone, Copy, PartialEq, Eq, Default)]enum HoverTarget {    #[default]    None,    MainMenu,    Submenu,} pub enum ContextMenuItem {    Separator,    Header(SharedString),    /// title, link_label, link_url    HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header    Label(SharedString),    Entry(ContextMenuEntry),    CustomEntry {        entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,        handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,        selectable: bool,        documentation_aside: Option<DocumentationAside>,    },    Submenu {        label: SharedString,        icon: Option<IconName>,        icon_color: Option<Color>,        builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,    },} impl ContextMenuItem {    pub fn custom_entry(        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,        handler: impl Fn(&mut Window, &mut App) + 'static,        documentation_aside: Option<DocumentationAside>,    ) -> Self {        Self::CustomEntry {            entry_render: Box::new(entry_render),            handler: Rc::new(move |_, window, cx| handler(window, cx)),            selectable: true,            documentation_aside,        }    }} pub struct ContextMenuEntry {    toggle: Option<(IconPosition, bool)>,    label: SharedString,    icon: Option<IconName>,    custom_icon_path: Option<SharedString>,    custom_icon_svg: Option<SharedString>,    icon_position: IconPosition,    icon_size: IconSize,    icon_color: Option<Color>,    handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,    secondary_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,    action: Option<Box<dyn Action>>,    disabled: bool,    documentation_aside: Option<DocumentationAside>,    end_slot_icon: Option<IconName>,    end_slot_title: Option<SharedString>,    end_slot_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,    show_end_slot_on_hover: bool,} impl ContextMenuEntry {    pub fn new(label: impl Into<SharedString>) -> Self {        ContextMenuEntry {            toggle: None,            label: label.into(),            icon: None,            custom_icon_path: None,            custom_icon_svg: None,            icon_position: IconPosition::Start,            icon_size: IconSize::Small,            icon_color: None,            handler: Rc::new(|_, _, _| {}),            secondary_handler: None,            action: None,            disabled: false,            documentation_aside: None,            end_slot_icon: None,            end_slot_title: None,            end_slot_handler: None,            show_end_slot_on_hover: false,        }    }     pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {        self.toggle = Some((toggle_position, toggled));        self    }     pub fn icon(mut self, icon: IconName) -> Self {        self.icon = Some(icon);        self    }     pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {        self.custom_icon_path = Some(path.into());        self.custom_icon_svg = None; // Clear other icon sources if custom path is set        self.icon = None;        self    }     pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {        self.custom_icon_svg = Some(svg.into());        self.custom_icon_path = None; // Clear other icon sources if custom path is set        self.icon = None;        self    }     pub fn icon_position(mut self, position: IconPosition) -> Self {        self.icon_position = position;        self    }     pub fn icon_size(mut self, icon_size: IconSize) -> Self {        self.icon_size = icon_size;        self    }     pub fn icon_color(mut self, icon_color: Color) -> Self {        self.icon_color = Some(icon_color);        self    }     pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {        self.toggle = Some((toggle_position, toggled));        self    }     pub fn action(mut self, action: Box<dyn Action>) -> Self {        self.action = Some(action);        self    }     pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {        self.handler = Rc::new(move |_, window, cx| handler(window, cx));        self    }     pub fn secondary_handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {        self.secondary_handler = Some(Rc::new(move |_, window, cx| handler(window, cx)));        self    }     pub fn disabled(mut self, disabled: bool) -> Self {        self.disabled = disabled;        self    }     pub fn documentation_aside(        mut self,        side: DocumentationSide,        render: impl Fn(&mut App) -> AnyElement + 'static,    ) -> Self {        self.documentation_aside = Some(DocumentationAside {            side,            render: Rc::new(render),        });         self    }} impl FluentBuilder for ContextMenuEntry {} impl From<ContextMenuEntry> for ContextMenuItem {    fn from(entry: ContextMenuEntry) -> Self {        ContextMenuItem::Entry(entry)    }} pub struct ContextMenu {    builder: Option<Rc<dyn Fn(Self, &mut Window, &mut Context<Self>) -> Self>>,    items: Vec<ContextMenuItem>,    focus_handle: FocusHandle,    action_context: Option<FocusHandle>,    selected_index: Option<usize>,    delayed: bool,    clicked: bool,    end_slot_action: Option<Box<dyn Action>>,    key_context: SharedString,    _on_blur_subscription: Subscription,    keep_open_on_confirm: bool,    fixed_width: Option<DefiniteLength>,    main_menu: Option<Entity<ContextMenu>>,    main_menu_observed_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,    // Docs aide-related fields    documentation_aside: Option<(usize, DocumentationAside)>,    aside_trigger_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,    // Submenu-related fields    submenu_state: SubmenuState,    hover_target: HoverTarget,    submenu_safety_threshold_x: Option<Pixels>,    submenu_trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,    submenu_trigger_mouse_down: bool,    ignore_blur_until: Option<Instant>,} #[derive(Copy, Clone, PartialEq, Eq)]pub enum DocumentationSide {    Left,    Right,} #[derive(Clone)]pub struct DocumentationAside {    pub side: DocumentationSide,    pub render: Rc<dyn Fn(&mut App) -> AnyElement>,} impl DocumentationAside {    pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {        Self { side, render }    }} impl Focusable for ContextMenu {    fn focus_handle(&self, _cx: &App) -> FocusHandle {        self.focus_handle.clone()    }} impl EventEmitter<DismissEvent> for ContextMenu {} impl FluentBuilder for ContextMenu {} impl ContextMenu {    pub fn new(        window: &mut Window,        cx: &mut Context<Self>,        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,    ) -> Self {        let focus_handle = cx.focus_handle();        let _on_blur_subscription = cx.on_blur(            &focus_handle,            window,            |this: &mut ContextMenu, window, cx| {                if let Some(ignore_until) = this.ignore_blur_until {                    if Instant::now() < ignore_until {                        return;                    } else {                        this.ignore_blur_until = None;                    }                }                 if this.main_menu.is_none() {                    if let SubmenuState::Open(open_submenu) = &this.submenu_state {                        let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();                        if submenu_focus.contains_focused(window, cx) {                            return;                        }                    }                }                 this.cancel(&menu::Cancel, window, cx)            },        );        window.refresh();         f(            Self {                builder: None,                items: Default::default(),                focus_handle,                action_context: None,                selected_index: None,                delayed: false,                clicked: false,                end_slot_action: None,                key_context: "menu".into(),                _on_blur_subscription,                keep_open_on_confirm: false,                fixed_width: None,                main_menu: None,                main_menu_observed_bounds: Rc::new(Cell::new(None)),                documentation_aside: None,                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),                submenu_state: SubmenuState::Closed,                hover_target: HoverTarget::MainMenu,                submenu_safety_threshold_x: None,                submenu_trigger_bounds: Rc::new(Cell::new(None)),                submenu_trigger_mouse_down: false,                ignore_blur_until: None,            },            window,            cx,        )    }     pub fn build(        window: &mut Window,        cx: &mut App,        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,    ) -> Entity<Self> {        cx.new(|cx| Self::new(window, cx, f))    }     /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation.    ///    /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto    /// it to call it again.    pub fn build_persistent(        window: &mut Window,        cx: &mut App,        builder: impl Fn(Self, &mut Window, &mut Context<Self>) -> Self + 'static,    ) -> Entity<Self> {        cx.new(|cx| {            let builder = Rc::new(builder);             let focus_handle = cx.focus_handle();            let _on_blur_subscription = cx.on_blur(                &focus_handle,                window,                |this: &mut ContextMenu, window, cx| {                    if let Some(ignore_until) = this.ignore_blur_until {                        if Instant::now() < ignore_until {                            return;                        } else {                            this.ignore_blur_until = None;                        }                    }                     if this.main_menu.is_none() {                        if let SubmenuState::Open(open_submenu) = &this.submenu_state {                            let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();                            if submenu_focus.contains_focused(window, cx) {                                return;                            }                        }                    }                     this.cancel(&menu::Cancel, window, cx)                },            );            window.refresh();             (builder.clone())(                Self {                    builder: Some(builder),                    items: Default::default(),                    focus_handle,                    action_context: None,                    selected_index: None,                    delayed: false,                    clicked: false,                    end_slot_action: None,                    key_context: "menu".into(),                    _on_blur_subscription,                    keep_open_on_confirm: true,                    fixed_width: None,                    main_menu: None,                    main_menu_observed_bounds: Rc::new(Cell::new(None)),                    documentation_aside: None,                    aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),                    submenu_state: SubmenuState::Closed,                    hover_target: HoverTarget::MainMenu,                    submenu_safety_threshold_x: None,                    submenu_trigger_bounds: Rc::new(Cell::new(None)),                    submenu_trigger_mouse_down: false,                    ignore_blur_until: None,                },                window,                cx,            )        })    }     /// Rebuilds the menu.    ///    /// This is used to refresh the menu entries when entries are toggled when the menu is configured with    /// `keep_open_on_confirm = true`.    ///    /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is    /// a no-op.    pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {        let Some(builder) = self.builder.clone() else {            return;        };         // The way we rebuild the menu is a bit of a hack.        let focus_handle = cx.focus_handle();        let new_menu = (builder.clone())(            Self {                builder: Some(builder),                items: Default::default(),                focus_handle: focus_handle.clone(),                action_context: None,                selected_index: None,                delayed: false,                clicked: false,                end_slot_action: None,                key_context: "menu".into(),                _on_blur_subscription: cx.on_blur(                    &focus_handle,                    window,                    |this: &mut ContextMenu, window, cx| {                        if let Some(ignore_until) = this.ignore_blur_until {                            if Instant::now() < ignore_until {                                return;                            } else {                                this.ignore_blur_until = None;                            }                        }                         if this.main_menu.is_none() {                            if let SubmenuState::Open(open_submenu) = &this.submenu_state {                                let submenu_focus =                                    open_submenu.entity.read(cx).focus_handle.clone();                                if submenu_focus.contains_focused(window, cx) {                                    return;                                }                            }                        }                         this.cancel(&menu::Cancel, window, cx)                    },                ),                keep_open_on_confirm: false,                fixed_width: None,                main_menu: None,                main_menu_observed_bounds: Rc::new(Cell::new(None)),                documentation_aside: None,                aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),                submenu_state: SubmenuState::Closed,                hover_target: HoverTarget::MainMenu,                submenu_safety_threshold_x: None,                submenu_trigger_bounds: Rc::new(Cell::new(None)),                submenu_trigger_mouse_down: false,                ignore_blur_until: None,            },            window,            cx,        );         self.items = new_menu.items;         cx.notify();    }     pub fn context(mut self, focus: FocusHandle) -> Self {        self.action_context = Some(focus);        self    }     pub fn header(mut self, title: impl Into<SharedString>) -> Self {        self.items.push(ContextMenuItem::Header(title.into()));        self    }     pub fn header_with_link(        mut self,        title: impl Into<SharedString>,        link_label: impl Into<SharedString>,        link_url: impl Into<SharedString>,    ) -> Self {        self.items.push(ContextMenuItem::HeaderWithLink(            title.into(),            link_label.into(),            link_url.into(),        ));

Lines 1–500 of 2356.