pixel_widgets/widget/
menu.rs

1use std::marker::PhantomData;
2
3use crate::draw::Primitive;
4use crate::event::{Event, Key};
5use crate::layout::{Rectangle, Size};
6use crate::node::{GenericNode, IntoNode, Node};
7use crate::style::Stylesheet;
8use crate::widget::{Context, Widget};
9
10/// A (context) menu with nestable items
11pub struct Menu<'a, T: 'a, S: AsMut<[MenuItem<'a, T>]>> {
12    items: S,
13    x: f32,
14    y: f32,
15    marker: PhantomData<&'a ()>,
16    on_close: Option<T>,
17}
18
19/// State for `Menu`
20pub struct MenuState {
21    inner: InnerState,
22    left: f32,
23    right: f32,
24    top: f32,
25    bottom: f32,
26}
27
28enum InnerState {
29    Closed,
30    Idle,
31    HoverItem { index: usize },
32    HoverSubMenu { index: usize, sub_state: Box<MenuState> },
33    Pressed { index: usize },
34}
35
36/// An item in `Menu`.
37pub enum MenuItem<'a, T> {
38    /// Item
39    Item {
40        /// The content of the item
41        content: Node<'a, T>,
42        /// Message to send when the item is clicked
43        on_select: Option<T>,
44    },
45    /// Sub menu
46    Menu {
47        /// The content of the item
48        content: Node<'a, T>,
49        /// MenuItems to show when this item is hovered
50        items: Vec<MenuItem<'a, T>>,
51    },
52}
53
54impl<'a, T: 'a> Menu<'a, T, Vec<MenuItem<'a, T>>> {
55    /// Construct a new `Menu`
56    pub fn new(x: f32, y: f32, on_close: T) -> Self {
57        Self {
58            items: Vec::new(),
59            x,
60            y,
61            marker: PhantomData,
62            on_close: on_close.into(),
63        }
64    }
65
66    /// Sets the (x, y) coordinates of the menu relative to it's parent.
67    pub fn position(mut self, (x, y): (f32, f32)) -> Self {
68        self.x = x;
69        self.y = y;
70        self
71    }
72
73    /// Sets the message to be posted when the menu is closed without selecting an item.
74    pub fn on_close(mut self, on_close: T) -> Self {
75        self.on_close = Some(on_close);
76        self
77    }
78
79    /// Sets all of the items of the menu
80    pub fn items(mut self, items: Vec<MenuItem<'a, T>>) -> Self {
81        self.items = items;
82        self
83    }
84
85    /// Adds an item to the menu
86    pub fn push(mut self, item: MenuItem<'a, T>) -> Self {
87        self.items.push(item);
88        self
89    }
90
91    /// Adds items using an iterator
92    pub fn extend<I: IntoIterator<Item = MenuItem<'a, T>>>(mut self, iter: I) -> Self {
93        self.items.extend(iter);
94        self
95    }
96}
97
98impl<'a, T: 'a> Default for Menu<'a, T, Vec<MenuItem<'a, T>>> {
99    fn default() -> Self {
100        Self {
101            items: vec![],
102            x: 0.0,
103            y: 0.0,
104            marker: PhantomData,
105            on_close: None,
106        }
107    }
108}
109
110impl<'a, T: 'a + Send, S: Send + AsRef<[MenuItem<'a, T>]> + AsMut<[MenuItem<'a, T>]>> Menu<'a, T, S> {
111    fn layout(&self, state: &MenuState, viewport: Rectangle, style: &Stylesheet) -> Rectangle {
112        let (width, height) = self.size(state, style);
113        let width = match width {
114            Size::Exact(width) => width,
115            Size::Fill(_) => viewport.width() - state.right,
116            Size::Shrink => 0.0,
117        };
118        let height = match height {
119            Size::Exact(height) => height,
120            Size::Fill(_) => viewport.height() - state.top,
121            Size::Shrink => 0.0,
122        };
123
124        let (left, right) = if ((state.right + width) - viewport.width()).max(0.0) <= (-(state.left - width)).max(0.0) {
125            (state.right, state.right + width)
126        } else {
127            (state.left - width, state.left)
128        };
129
130        let (top, bottom) =
131            if ((state.top + height) - viewport.height()).max(0.0) <= (-(state.bottom - height)).max(0.0) {
132                (state.top, state.top + height)
133            } else {
134                (state.bottom - height, state.bottom)
135            };
136
137        Rectangle {
138            left,
139            top,
140            right,
141            bottom,
142        }
143    }
144
145    fn item_layouts(
146        &mut self,
147        layout: Rectangle,
148        style: &Stylesheet,
149    ) -> impl Iterator<Item = (&mut MenuItem<'a, T>, Rectangle)> {
150        let layout = style.background.content_rect(layout, style.padding);
151        let align = style.align_horizontal;
152        let available_parts = self.items.as_mut().iter().map(|i| i.content().size().1.parts()).sum();
153        let available_space = layout.height()
154            - self
155                .items
156                .as_mut()
157                .iter()
158                .map(|i| i.content().size().1.min_size())
159                .sum::<f32>();
160        let mut cursor = 0.0;
161        self.items.as_mut().iter_mut().map(move |item| {
162            let (w, h) = item.content().size();
163            let w = w.resolve(layout.width(), w.parts());
164            let h = h
165                .resolve(available_space, available_parts)
166                .min(layout.height() - cursor);
167            let x = align.resolve_start(w, layout.width());
168            let y = cursor;
169
170            cursor += h;
171            (
172                item,
173                Rectangle::from_xywh(x, y, w, h).translate(layout.left, layout.top),
174            )
175        })
176    }
177
178    #[allow(clippy::too_many_arguments)]
179    fn hover(
180        &mut self,
181        current: InnerState,
182        x: f32,
183        y: f32,
184        layout: Rectangle,
185        clip: Rectangle,
186        style: &Stylesheet,
187        context: &mut Context<T>,
188    ) -> InnerState {
189        context.redraw();
190        if clip.point_inside(x, y) {
191            let mut result = current;
192            for (index, (item, item_layout)) in self.item_layouts(layout, style).enumerate() {
193                let hover_rect = Rectangle {
194                    left: layout.left + style.padding.left,
195                    right: layout.right - style.padding.right,
196                    top: item_layout.top,
197                    bottom: item_layout.bottom,
198                };
199                if hover_rect.point_inside(x, y) {
200                    result = match item {
201                        MenuItem::Item { .. } => InnerState::HoverItem { index },
202                        MenuItem::Menu { .. } => InnerState::HoverSubMenu {
203                            index,
204                            sub_state: Box::new(MenuState {
205                                inner: InnerState::Idle,
206                                right: layout.right - style.padding.right - style.padding.left,
207                                left: layout.left + style.padding.left + style.padding.right,
208                                top: item_layout.top - style.padding.top,
209                                bottom: item_layout.bottom + style.padding.bottom,
210                            }),
211                        },
212                    };
213                }
214            }
215            result
216        } else {
217            current
218        }
219    }
220}
221
222fn visit<'a, T>(items: &mut [MenuItem<'a, T>], visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {
223    for item in items.iter_mut() {
224        match item {
225            MenuItem::Item { ref mut content, .. } => visitor(&mut **content),
226            MenuItem::Menu {
227                ref mut content,
228                ref mut items,
229            } => {
230                visitor(&mut **content);
231                visit(items.as_mut_slice(), visitor);
232            }
233        }
234    }
235}
236
237impl<'a, T: 'a + Send, S: Send + AsRef<[MenuItem<'a, T>]> + AsMut<[MenuItem<'a, T>]>> Widget<'a, T> for Menu<'a, T, S> {
238    type State = MenuState;
239
240    fn mount(&self) -> Self::State {
241        MenuState {
242            inner: InnerState::Idle,
243            left: self.x,
244            right: self.x,
245            top: self.y,
246            bottom: self.y,
247        }
248    }
249
250    fn widget(&self) -> &'static str {
251        "menu"
252    }
253
254    fn len(&self) -> usize {
255        self.items.as_ref().len()
256    }
257
258    fn visit_children(&mut self, visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {
259        visit(self.items.as_mut(), visitor);
260    }
261
262    fn size(&self, _: &MenuState, style: &Stylesheet) -> (Size, Size) {
263        let width = match style.width {
264            Size::Shrink => {
265                Size::Exact(
266                    self.items
267                        .as_ref()
268                        .iter()
269                        .fold(0.0, |size, child| match child.content().size().0 {
270                            Size::Exact(child_size) => size.max(child_size),
271                            _ => size,
272                        }),
273                )
274            }
275            other => other,
276        };
277        let height = match style.height {
278            Size::Shrink => {
279                Size::Exact(
280                    self.items
281                        .as_ref()
282                        .iter()
283                        .fold(0.0, |size, child| match child.content().size().1 {
284                            Size::Exact(child_size) => size + child_size,
285                            _ => size,
286                        }),
287                )
288            }
289            other => other,
290        };
291
292        style
293            .background
294            .resolve_size((style.width, style.height), (width, height), style.padding)
295    }
296
297    fn hit(&self, state: &MenuState, layout: Rectangle, clip: Rectangle, _style: &Stylesheet, x: f32, y: f32) -> bool {
298        self.focused(state) && layout.point_inside(x, y) && clip.point_inside(x, y)
299    }
300
301    fn focused(&self, state: &MenuState) -> bool {
302        !matches!(state.inner, InnerState::Closed)
303    }
304
305    fn event(
306        &mut self,
307        state: &mut MenuState,
308        viewport: Rectangle,
309        clip: Rectangle,
310        style: &Stylesheet,
311        event: Event,
312        context: &mut Context<T>,
313    ) {
314        if let InnerState::Closed = state.inner {
315            return;
316        }
317
318        let layout = self.layout(state, viewport, style);
319
320        state.inner = match (event, std::mem::replace(&mut state.inner, InnerState::Idle)) {
321            (Event::Cursor(x, y), InnerState::HoverSubMenu { index, sub_state }) => self.hover(
322                InnerState::HoverSubMenu { index, sub_state },
323                x,
324                y,
325                layout,
326                clip,
327                style,
328                context,
329            ),
330
331            (Event::Cursor(x, y), InnerState::Pressed { index }) => {
332                match self.hover(InnerState::Idle, x, y, layout, clip, style, context) {
333                    InnerState::HoverItem { index: hover_index } if hover_index == index => {
334                        InnerState::Pressed { index }
335                    }
336                    other => other,
337                }
338            }
339
340            (Event::Cursor(x, y), _) => self.hover(InnerState::Idle, x, y, layout, clip, style, context),
341
342            (Event::Press(Key::LeftMouseButton), InnerState::Idle) => {
343                context.redraw();
344                context.extend(self.on_close.take());
345                InnerState::Closed
346            }
347
348            (Event::Press(Key::LeftMouseButton), InnerState::HoverItem { index }) => {
349                context.redraw();
350                InnerState::Pressed { index }
351            }
352
353            (Event::Release(Key::LeftMouseButton), InnerState::Pressed { index }) => {
354                context.redraw();
355                if let Some(MenuItem::Item { on_select, .. }) = self.items.as_mut().get_mut(index) {
356                    context.extend(on_select.take());
357                }
358                context.extend(self.on_close.take());
359                InnerState::Closed
360            }
361
362            (_, unhandled) => unhandled,
363        };
364
365        let mut close = false;
366
367        if let InnerState::HoverSubMenu {
368            index,
369            ref mut sub_state,
370        } = state.inner
371        {
372            if let Some(&mut MenuItem::Menu { ref mut items, .. }) = self.items.as_mut().get_mut(index) {
373                let mut sub_menu = Menu {
374                    items: items.as_mut_slice(),
375                    x: 0.0,
376                    y: 0.0,
377                    marker: PhantomData,
378                    on_close: None,
379                };
380                sub_menu.event(&mut *sub_state, viewport, clip, style, event, context);
381            }
382
383            if let InnerState::Closed = sub_state.as_mut().inner {
384                close = true;
385            }
386        }
387
388        if close {
389            context.redraw();
390            state.inner = InnerState::Closed;
391            context.extend(self.on_close.take());
392        }
393    }
394
395    fn draw(
396        &mut self,
397        state: &mut MenuState,
398        viewport: Rectangle,
399        clip: Rectangle,
400        style: &Stylesheet,
401    ) -> Vec<Primitive<'a>> {
402        if let InnerState::Closed = state.inner {
403            return Vec::new();
404        }
405
406        let mut result = vec![Primitive::LayerUp];
407
408        let layout = self.layout(state, viewport, style);
409
410        result.extend(style.background.render(layout));
411
412        let hover_index = match state.inner {
413            InnerState::Closed => None,
414            InnerState::Idle => None,
415            InnerState::HoverItem { index } => Some(index),
416            InnerState::HoverSubMenu {
417                index,
418                ref mut sub_state,
419            } => {
420                if let Some(&mut MenuItem::Menu { ref mut items, .. }) = self.items.as_mut().get_mut(index) {
421                    let mut sub_menu = Menu {
422                        items: items.as_mut_slice(),
423                        x: 0.0,
424                        y: 0.0,
425                        marker: PhantomData,
426                        on_close: None,
427                    };
428
429                    result.extend(sub_menu.draw(&mut *sub_state, viewport, clip, style));
430                }
431                Some(index)
432            }
433            InnerState::Pressed { index } => Some(index),
434        };
435
436        result =
437            self.item_layouts(layout, style)
438                .enumerate()
439                .fold(result, |mut result, (index, (item, item_layout))| {
440                    if hover_index == Some(index) {
441                        result.push(Primitive::DrawRect(
442                            Rectangle {
443                                left: layout.left + style.padding.left,
444                                right: layout.right - style.padding.right,
445                                top: item_layout.top,
446                                bottom: item_layout.bottom,
447                            },
448                            style.color,
449                        ));
450                    }
451                    result.extend(item.content_mut().draw(item_layout, clip));
452                    result
453                });
454
455        result.push(Primitive::LayerDown);
456        result
457    }
458}
459
460impl<'a, T: 'a + Send, S: 'a + Send + AsRef<[MenuItem<'a, T>]> + AsMut<[MenuItem<'a, T>]>> IntoNode<'a, T>
461    for Menu<'a, T, S>
462{
463    fn into_node(self) -> Node<'a, T> {
464        Node::from_widget(self)
465    }
466}
467
468impl MenuState {
469    /// Opens the context menu if it's closed and `open == true`. The context menu will be positioned at (x,y) inside
470    /// of it's layout rect.
471    pub fn open(&mut self, x: f32, y: f32) {
472        self.inner = match std::mem::replace(&mut self.inner, InnerState::Idle) {
473            InnerState::Closed => {
474                self.left = x;
475                self.right = x;
476                self.top = y;
477                self.bottom = y;
478                InnerState::Idle
479            }
480            open => open,
481        };
482    }
483}
484
485impl Default for MenuState {
486    fn default() -> Self {
487        Self {
488            inner: InnerState::Closed,
489            left: 0.0,
490            right: 0.0,
491            top: 0.0,
492            bottom: 0.0,
493        }
494    }
495}
496
497impl<'a, T: 'a> MenuItem<'a, T> {
498    /// Construct a new `MenuItem` of the item type,
499    ///  with a content node and a message to be posted when this item is selected.
500    pub fn item(content: impl IntoNode<'a, T>, on_select: impl Into<Option<T>>) -> Self {
501        Self::Item {
502            content: content.into_node(),
503            on_select: on_select.into(),
504        }
505    }
506
507    /// Construct a new `MenuItem` of the sub menu type,
508    ///  with a content node. Sub menu items can be added to the returned value.
509    pub fn menu(content: impl IntoNode<'a, T>) -> Self {
510        Self::Menu {
511            content: content.into_node(),
512            items: Vec::new(),
513        }
514    }
515
516    /// Adds a sub `MenuItem` to this menu.
517    /// Will panic if this is an item instead of a submenu.
518    pub fn push(self, item: Self) -> Self {
519        if let Self::Menu { content, mut items } = self {
520            items.push(item);
521            Self::Menu { content, items }
522        } else {
523            panic!("push may only be called on menu items")
524        }
525    }
526
527    /// Adds multiple sub `MenuItem`s to this menu.
528    /// Will panic if this is an item instead of a submenu.
529    pub fn extend(self, new_items: impl IntoIterator<Item = Self>) -> Self {
530        if let Self::Menu { content, mut items } = self {
531            items.extend(new_items.into_iter());
532            Self::Menu { content, items }
533        } else {
534            panic!("extend may only be called on menu items")
535        }
536    }
537
538    fn content(&self) -> &Node<'a, T> {
539        match self {
540            MenuItem::Item { ref content, .. } => content,
541            MenuItem::Menu { ref content, .. } => content,
542        }
543    }
544
545    fn content_mut(&mut self) -> &mut Node<'a, T> {
546        match self {
547            MenuItem::Item { ref mut content, .. } => content,
548            MenuItem::Menu { ref mut content, .. } => content,
549        }
550    }
551}