Skip to main content

tui_lipan/widgets/
context_menu.rs

1//! Context menu widget.
2
3use std::sync::Arc;
4
5use crate::callback::Callback;
6use crate::core::element::{Element, IntoElement};
7use crate::style::{BorderStyle, Length, Padding, ScrollbarConfig, Style, StyleSlot};
8use crate::widgets::{List, ListItem, Popover, PopoverOffset, PopoverPlacement};
9
10/// A popup menu widget.
11#[derive(Clone)]
12pub struct ContextMenu {
13    trigger: Element,
14    items: Vec<ListItem>,
15    open: bool,
16    on_select: Option<Callback<usize>>,
17    on_close: Option<Callback<()>>,
18    placement: PopoverPlacement,
19    offset: PopoverOffset,
20    clamp: bool,
21    auto_flip: bool,
22    anchor: Option<(u16, u16)>,
23    width: Length,
24    height: Length,
25    border: bool,
26    border_style: BorderStyle,
27    padding: Padding,
28    style: Style,
29    selection_style: StyleSlot,
30    unfocused_selection_style: StyleSlot,
31    item_hover_style: Option<StyleSlot>,
32    selection_symbol: Option<Arc<str>>,
33    selection_symbol_style: Option<Style>,
34    unfocused_selection_symbol_style: Option<Style>,
35    scrollbar: bool,
36    scrollbar_config: ScrollbarConfig,
37}
38
39impl ContextMenu {
40    /// Create a new context menu.
41    pub fn new(trigger: impl IntoElement) -> Self {
42        Self {
43            trigger: trigger.into(),
44            items: Vec::new(),
45            open: false,
46            on_select: None,
47            on_close: None,
48            placement: PopoverPlacement::BelowStart,
49            offset: PopoverOffset::ZERO,
50            clamp: true,
51            auto_flip: true,
52            anchor: None,
53            width: Length::Px(20),
54            height: Length::Auto,
55            border: true,
56            border_style: BorderStyle::Plain,
57            padding: Padding::default(),
58            style: Style::default(),
59            selection_style: StyleSlot::Inherit,
60            unfocused_selection_style: StyleSlot::Inherit,
61            item_hover_style: None,
62            selection_symbol: Some("> ".into()),
63            selection_symbol_style: None,
64            unfocused_selection_symbol_style: None,
65            scrollbar: false,
66            scrollbar_config: ScrollbarConfig::default(),
67        }
68    }
69
70    /// Replace all menu items.
71    pub fn items(mut self, items: impl IntoIterator<Item = impl Into<ListItem>>) -> Self {
72        self.items = items.into_iter().map(Into::into).collect();
73        self
74    }
75
76    /// Set open state.
77    pub fn open(mut self, open: bool) -> Self {
78        self.open = open;
79        self
80    }
81
82    /// Set selection callback.
83    pub fn on_select(mut self, cb: Callback<usize>) -> Self {
84        self.on_select = Some(cb);
85        self
86    }
87
88    /// Set close callback.
89    pub fn on_close(mut self, cb: Callback<()>) -> Self {
90        self.on_close = Some(cb);
91        self
92    }
93
94    /// Set popover placement.
95    pub fn placement(mut self, placement: PopoverPlacement) -> Self {
96        self.placement = placement;
97        self
98    }
99
100    /// Set popover offset.
101    pub fn offset(mut self, offset: impl Into<PopoverOffset>) -> Self {
102        self.offset = offset.into();
103        self
104    }
105
106    /// Clamp popover to the viewport bounds.
107    pub fn clamp(mut self, clamp: bool) -> Self {
108        self.clamp = clamp;
109        self
110    }
111
112    /// Automatically flip placement when it overflows the viewport.
113    pub fn auto_flip(mut self, auto_flip: bool) -> Self {
114        self.auto_flip = auto_flip;
115        self
116    }
117
118    /// Anchor the menu to an absolute position (content coordinates).
119    pub fn anchor(mut self, anchor: Option<(u16, u16)>) -> Self {
120        self.anchor = anchor;
121        self
122    }
123
124    /// Set width.
125    pub fn width(mut self, width: Length) -> Self {
126        self.width = width;
127        self
128    }
129
130    /// Set height.
131    pub fn height(mut self, height: Length) -> Self {
132        self.height = height;
133        self
134    }
135
136    /// Draw a border.
137    pub fn border(mut self, border: bool) -> Self {
138        self.border = border;
139        self
140    }
141
142    /// Set border style.
143    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
144        self.border_style = border_style;
145        self
146    }
147
148    /// Set padding.
149    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
150        self.padding = padding.into();
151        self
152    }
153
154    /// Set base style.
155    pub fn style(mut self, style: Style) -> Self {
156        self.style = style;
157        self
158    }
159
160    /// Set highlight style.
161    pub fn selection_style(mut self, style: Style) -> Self {
162        self.selection_style = StyleSlot::Replace(style);
163        self
164    }
165
166    /// Extend the themed highlight style.
167    pub fn extend_selection_style(mut self, style: Style) -> Self {
168        self.selection_style = StyleSlot::Extend(style);
169        self
170    }
171
172    /// Inherit the themed highlight style.
173    pub fn inherit_selection_style(mut self) -> Self {
174        self.selection_style = StyleSlot::Inherit;
175        self
176    }
177
178    /// Set highlight style slot directly for composite forwarding.
179    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
180        self.selection_style = slot;
181        self
182    }
183
184    /// Set highlight style while the menu list is not focused.
185    pub fn unfocused_selection_style(mut self, style: Style) -> Self {
186        self.unfocused_selection_style = StyleSlot::Replace(style);
187        self
188    }
189
190    /// Extend the themed highlight style while the menu list is not focused.
191    pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
192        self.unfocused_selection_style = StyleSlot::Extend(style);
193        self
194    }
195
196    /// Inherit the themed highlight style while the menu list is not focused.
197    pub fn inherit_unfocused_selection_style(mut self) -> Self {
198        self.unfocused_selection_style = StyleSlot::Inherit;
199        self
200    }
201
202    /// Set unfocused highlight style slot directly for composite forwarding.
203    pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
204        self.unfocused_selection_style = slot;
205        self
206    }
207
208    /// Set hovered item style.
209    pub fn item_hover_style(mut self, style: Style) -> Self {
210        self.item_hover_style = Some(StyleSlot::Replace(style));
211        self
212    }
213
214    /// Extend the themed hovered item style.
215    pub fn extend_item_hover_style(mut self, style: Style) -> Self {
216        self.item_hover_style = Some(StyleSlot::Extend(style));
217        self
218    }
219
220    /// Inherit the themed hovered item style.
221    pub fn inherit_item_hover_style(mut self) -> Self {
222        self.item_hover_style = Some(StyleSlot::Inherit);
223        self
224    }
225
226    /// Set hovered item style slot directly for composite forwarding.
227    pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
228        self.item_hover_style = Some(slot);
229        self
230    }
231
232    /// Set highlight symbol.
233    pub fn selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
234        self.selection_symbol = symbol.map(Into::into);
235        self
236    }
237
238    /// Set highlight symbol style.
239    pub fn selection_symbol_style(mut self, style: Style) -> Self {
240        self.selection_symbol_style = Some(style);
241        self
242    }
243
244    /// Set highlight symbol style while the menu list is not focused.
245    pub fn unfocused_selection_symbol_style(mut self, style: Style) -> Self {
246        self.unfocused_selection_symbol_style = Some(style);
247        self
248    }
249
250    /// Enable a scrollbar.
251    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
252        self.scrollbar = scrollbar;
253        self
254    }
255
256    /// Set scrollbar configuration.
257    pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
258        self.scrollbar_config = config;
259        self
260    }
261}
262
263impl From<ContextMenu> for Element {
264    fn from(menu: ContextMenu) -> Self {
265        let mut list = List::new()
266            .items(menu.items)
267            .border(menu.border)
268            .border_style(menu.border_style)
269            .padding(menu.padding)
270            .style(menu.style)
271            .selection_symbol_style(
272                menu.selection_symbol_style
273                    .unwrap_or(menu.selection_style.explicit_style().unwrap_or_default()),
274            )
275            .unfocused_selection_symbol_style(
276                menu.unfocused_selection_symbol_style
277                    .or_else(|| menu.unfocused_selection_style.explicit_style())
278                    .unwrap_or(menu.selection_style.explicit_style().unwrap_or_default()),
279            )
280            .selection_symbol(menu.selection_symbol)
281            .scrollbar(menu.scrollbar)
282            .scrollbar_config(menu.scrollbar_config)
283            .width(menu.width)
284            .height(menu.height);
285        list = list
286            .selection_style_slot(menu.selection_style)
287            .unfocused_selection_style_slot(menu.unfocused_selection_style)
288            .item_hover_style_slot(menu.item_hover_style.unwrap_or(menu.selection_style));
289
290        if let Some(cb) = menu.on_select {
291            list = list.on_select(Callback::new(move |ev: crate::widgets::ListEvent| {
292                cb.emit(ev.index)
293            }));
294        }
295
296        Popover::new()
297            .trigger(menu.trigger)
298            .content(list)
299            .open(menu.open)
300            .placement(menu.placement)
301            .offset(menu.offset)
302            .clamp(menu.clamp)
303            .auto_flip(menu.auto_flip)
304            .min_trigger_width(false)
305            .anchor(menu.anchor)
306            .on_close(menu.on_close.unwrap_or_else(|| Callback::new(|_| {})))
307            .into()
308    }
309}