gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! A canvas's context menu, whose entries answer to their shortcuts too.

use egui::{Button, Context, KeyboardShortcut, Response};

struct Entry<A> {
    label: &'static str,
    action: A,
    shortcut: Option<KeyboardShortcut>,
    /// Whether the shortcut acts here, or is only shown beside the entry.
    acts: bool,
}

/// The entries of a context menu, and keys that act without one.
pub(crate) struct ContextMenu<A> {
    entries: Vec<Entry<A>>,
    /// Shortcuts that act without a menu entry of their own.
    keys: Vec<(KeyboardShortcut, A)>,
    /// No text field has the keyboard, so shortcuts may act: typing into a
    /// field must never delete or select on the canvas behind it. A clicked
    /// button keeps the focus, so only a text field counts.
    keyboard: bool,
}

impl<A: Clone> ContextMenu<A> {
    pub(crate) fn new(ctx: &Context) -> Self {
        Self {
            entries: vec![],
            keys: vec![],
            keyboard: !ctx.text_edit_focused(),
        }
    }

    /// An entry, whose shortcut acts too.
    pub(crate) fn entry(
        mut self,
        label: &'static str,
        action: A,
        shortcut: Option<KeyboardShortcut>,
    ) -> Self {
        self.entries.push(Entry {
            label,
            action,
            shortcut,
            acts: true,
        });
        self
    }

    /// An entry whose shortcut is only shown beside it, because something
    /// else handles the key — egui turns the clipboard shortcuts into events
    /// of their own.
    pub(crate) fn entry_shown(
        mut self,
        label: &'static str,
        action: A,
        shortcut: KeyboardShortcut,
    ) -> Self {
        self.entries.push(Entry {
            label,
            action,
            shortcut: Some(shortcut),
            acts: false,
        });
        self
    }

    /// A shortcut with no entry of its own: a second key for an entry's
    /// action, say.
    pub(crate) fn key(mut self, shortcut: KeyboardShortcut, action: A) -> Self {
        self.keys.push((shortcut, action));
        self
    }

    /// Shows the menu on `response`, and returns the action a click or a
    /// shortcut asked for. Shortcuts are tried in the order they were added,
    /// so a shifted one has to come before its unshifted twin.
    pub(crate) fn show(self, response: &Response) -> Option<A> {
        let mut clicked = None;
        response.context_menu(|ui| {
            for entry in &self.entries {
                let mut button = Button::new(entry.label);
                if let Some(shortcut) = &entry.shortcut {
                    button = button.shortcut_text(ui.ctx().format_shortcut(shortcut));
                }
                if ui.add(button).clicked() {
                    clicked = Some(entry.action.clone());
                    return;
                }
            }
        });
        if clicked.is_some() || !self.keyboard {
            return clicked;
        }
        let mut acting = self
            .entries
            .iter()
            .filter(|entry| entry.acts)
            .filter_map(|entry| Some((entry.shortcut?, &entry.action)))
            .chain(
                self.keys
                    .iter()
                    .map(|(shortcut, action)| (*shortcut, action)),
            );
        response.ctx.input_mut(|input| {
            acting
                .find(|(shortcut, _)| input.consume_shortcut(shortcut))
                .map(|(_, action)| action.clone())
        })
    }
}