Skip to main content

perspective_viewer/ui/
context_menu.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use web_sys::MouseEvent;
14use yew::prelude::*;
15
16/// A single command row in a `ContextMenu`.
17#[derive(Clone, PartialEq)]
18pub struct ContextMenuItem {
19    pub label: String,
20    pub on_select: Callback<()>,
21    pub disabled: bool,
22}
23
24/// A row in a `ContextMenu`: a plain command `ContextMenuItem`, a
25/// non-interactive group label, or a row with a hover-opened sub-menu flyout.
26#[derive(Clone, PartialEq)]
27pub enum ContextMenuEntry {
28    Item(ContextMenuItem),
29    Header(String),
30
31    /// Arbitrary content rendered in place, whose own handlers must close
32    /// the menu.
33    Custom(Html),
34    Submenu {
35        label: String,
36
37        on_select: Option<Callback<()>>,
38        entries: Vec<ContextMenuEntry>,
39    },
40}
41
42#[derive(Properties, PartialEq)]
43pub struct ContextMenuProps {
44    pub entries: Vec<ContextMenuEntry>,
45
46    #[prop_or_default]
47    pub on_close: Callback<()>,
48}
49
50/// The command-item list of a panel context menu.
51pub struct ContextMenu;
52
53impl Component for ContextMenu {
54    type Message = ();
55    type Properties = ContextMenuProps;
56
57    fn create(_ctx: &Context<Self>) -> Self {
58        Self
59    }
60
61    fn view(&self, ctx: &Context<Self>) -> Html {
62        let props = ctx.props();
63        let entries = props
64            .entries
65            .iter()
66            .map(|entry| render_entry(entry, &props.on_close));
67
68        html! { <div class="context-menu">{ for entries }</div> }
69    }
70}
71
72fn render_entry(entry: &ContextMenuEntry, on_close: &Callback<()>) -> Html {
73    match entry {
74        ContextMenuEntry::Item(item) => {
75            let disabled = item.disabled;
76            let onclick = {
77                let on_select = item.on_select.clone();
78                let on_close = on_close.clone();
79                Callback::from(move |e: MouseEvent| {
80                    e.stop_propagation();
81                    if !disabled {
82                        on_select.emit(());
83                        on_close.emit(());
84                    }
85                })
86            };
87
88            let mut class = classes!("context-menu-item");
89            if disabled {
90                class.push("disabled");
91            }
92
93            html! { <span {class} {onclick}>{ item.label.clone() }</span> }
94        },
95        ContextMenuEntry::Header(label) => {
96            html! { <span class="context-menu-header">{ label.clone() }</span> }
97        },
98        ContextMenuEntry::Custom(content) => content.clone(),
99        ContextMenuEntry::Submenu {
100            label,
101            on_select,
102            entries,
103        } => {
104            let onclick = {
105                let on_select = on_select.clone();
106                let on_close = on_close.clone();
107                Callback::from(move |e: MouseEvent| {
108                    e.stop_propagation();
109                    if let Some(on_select) = &on_select {
110                        on_select.emit(());
111                        on_close.emit(());
112                    }
113                })
114            };
115
116            let children = entries.iter().map(|entry| render_entry(entry, on_close));
117            html! {
118                <span class="context-menu-item has-submenu" {onclick}>
119                    { label.clone() }
120                    <div class="context-menu context-menu-submenu">{ for children }</div>
121                </span>
122            }
123        },
124    }
125}