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
10pub 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
19pub 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
36pub enum MenuItem<'a, T> {
38 Item {
40 content: Node<'a, T>,
42 on_select: Option<T>,
44 },
45 Menu {
47 content: Node<'a, T>,
49 items: Vec<MenuItem<'a, T>>,
51 },
52}
53
54impl<'a, T: 'a> Menu<'a, T, Vec<MenuItem<'a, T>>> {
55 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 pub fn position(mut self, (x, y): (f32, f32)) -> Self {
68 self.x = x;
69 self.y = y;
70 self
71 }
72
73 pub fn on_close(mut self, on_close: T) -> Self {
75 self.on_close = Some(on_close);
76 self
77 }
78
79 pub fn items(mut self, items: Vec<MenuItem<'a, T>>) -> Self {
81 self.items = items;
82 self
83 }
84
85 pub fn push(mut self, item: MenuItem<'a, T>) -> Self {
87 self.items.push(item);
88 self
89 }
90
91 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 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 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 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 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 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}