gooey/widget/
menu.rs

1//! Selection widget.
2//!
3//! ```text
4//! +--------+
5//! | frame  |
6//! +---+----+
7//!     |\
8//!     |  \-------+----------+---- ...
9//!     |          |          |
10//!  +--+---+  +---+---+  +---+---+
11//!  | menu |  | item0 |  | item1 | ...
12//!  +------+  +---+---+  +---+---+
13//!               /|\        /|\
14//! ```
15//!
16//! The menu (selection) widget is the initial element parented to a frame
17//! widget. The `next_item` and `previous_item` control functions target the
18//! parent frame.
19
20use std::convert::TryFrom;
21use lazy_static::lazy_static;
22
23use crate::prelude::*;
24
25pub use self::builder::MenuBuilder as Builder;
26
27pub type Menu <'element> = Widget <'element,
28  controller::component::Selection, model::Component, view::Component>;
29
30//
31//  controls
32//
33
34lazy_static!{
35  pub static ref CONTROLS : Controls = controls::Builder::new()
36    .buttons (vec![
37      controls::button::Builtin::MenuNextItem,
38      controls::button::Builtin::MenuPreviousItem,
39      controls::button::Builtin::MenuFirstItem,
40      controls::button::Builtin::MenuLastItem
41    ].into_iter().map (Into::into).collect::<Vec <_>>().into())
42    .build();
43}
44
45/// Builtin button control ID 'MenuNextItem'
46///
47/// This control should target the "root" frame node of the menu, and assumes
48/// that there is at least one focused item node after the initial selection
49/// node.
50pub fn next_item (
51  _             : &controls::button::Release,
52  elements      : &Tree <Element>,
53  frame_id      : &NodeId,
54  action_buffer : &mut Vec <(NodeId, Action)>
55) {
56  use controller::component::{Kind, Selection};
57  log::trace!("next_item...");
58  let frame = elements.get (frame_id).unwrap();
59  let mut children_ids = frame.children().iter();
60  let menu_id = children_ids.next().unwrap();
61  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
62  if let Some (selected_id) = selection.current.as_ref() {
63    // find the currently selected child node
64    while let Some (child_id) = children_ids.next() {
65      if child_id == selected_id {
66        // should be focused
67        debug_assert_eq!(
68          elements.get_element (child_id).controller.state,
69          controller::State::Focused);
70        break
71      }
72    }
73  }
74  // if none is selected, the next enabled child frame will be selected
75  let mut select_id = None;
76  while let Some (next_id) = children_ids.next() {
77    let next = elements.get_element (next_id);
78    if next.controller.state == controller::State::Enabled &&
79      super::Frame::try_from (next).is_ok()
80    {
81      select_id = Some (next_id);
82      break
83    }
84  }
85  if select_id.is_none() && selection.loop_ {
86    for next_id in frame.children().iter().skip (1) {
87      let next = elements.get_element (next_id);
88      if next.controller.state == controller::State::Enabled &&
89        super::Frame::try_from (next).is_ok()
90      {
91        select_id = Some (next_id);
92        break
93      }
94    }
95  }
96  if let Some (next_id) = select_id {
97    let select_id   = next_id.clone();
98    let select_next = Box::new (move |controller : &mut Controller|{
99      let selection = Selection::try_ref_mut (&mut controller.component)
100        .unwrap();
101      selection.current = Some (select_id);
102    });
103    action_buffer.push ((menu_id.clone(),
104      Action::ModifyController (select_next)));
105    action_buffer.push ((next_id.clone(), Action::Focus));
106  }
107  log::trace!("...next_item");
108}
109
110/// Builtin button control ID 'MenuPreviousItem'
111///
112/// This control should target the "root" frame node of the menu, and assumes
113/// that there is at least one focused item node after the initial selection
114/// node.
115pub fn previous_item (
116  _             : &controls::button::Release,
117  elements      : &Tree <Element>,
118  frame_id      : &NodeId,
119  action_buffer : &mut Vec <(NodeId, Action)>
120) {
121  use controller::component::{Kind, Selection};
122  log::trace!("previous_item...");
123  let frame = elements.get (frame_id).unwrap();
124  let mut children_ids = frame.children().iter();
125  let menu_id = children_ids.next().unwrap();
126  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
127  let mut children_ids = children_ids.rev();
128  if let Some (selected_id) = selection.current.as_ref() {
129    // find the currently selected child node
130    while let Some (child_id) = children_ids.next() {
131      if child_id == selected_id {
132        // should be focused
133        debug_assert_eq!(
134          elements.get_element (child_id).controller.state,
135          controller::State::Focused);
136        break
137      }
138    }
139  }
140  // if none is selected, the previous enabled child will be selected
141  let mut select_id = None;
142  while let Some (prev_id) = children_ids.next() {
143    let prev = elements.get_element (prev_id);
144    if prev.controller.state == controller::State::Enabled &&
145      super::Frame::try_from (prev).is_ok()
146    {
147      select_id = Some (prev_id);
148      break
149    }
150  }
151  if select_id.is_none() && selection.loop_ {
152    for prev_id in frame.children().iter().rev() {
153      let prev = elements.get_element (prev_id);
154      if prev.controller.state == controller::State::Enabled &&
155        super::Frame::try_from (prev).is_ok()
156      {
157        select_id = Some (prev_id);
158        break
159      }
160    }
161  }
162  if let Some (prev_id) = select_id {
163    let select_id   = prev_id.clone();
164    let select_prev = Box::new (move |controller : &mut Controller|{
165      let selection = Selection::try_ref_mut (&mut controller.component)
166        .unwrap();
167      selection.current = Some (select_id);
168    });
169    action_buffer.push ((menu_id.clone(),
170      Action::ModifyController (select_prev)));
171    action_buffer.push ((prev_id.clone(), Action::Focus));
172  }
173  log::trace!("...previous_item");
174}
175
176/// Builtin button control ID 'MenuFirstItem'
177///
178/// This control should target the "root" frame node of the menu, and assumes
179/// that there is at least one focused item node after the initial selection
180/// node.
181pub fn first_item (
182  _             : &controls::button::Release,
183  elements      : &Tree <Element>,
184  frame_id      : &NodeId,
185  action_buffer : &mut Vec <(NodeId, Action)>
186) {
187  use controller::component::{Kind, Selection};
188  log::trace!("first_item...");
189  let frame = elements.get (frame_id).unwrap();
190  let mut children_ids = frame.children().iter();
191  let menu_id = children_ids.next().unwrap();
192  let mut select_id = None;
193  for child_id in children_ids {
194    let next = elements.get_element (child_id);
195    if next.controller.state == controller::State::Enabled &&
196      super::Frame::try_from (next).is_ok()
197    {
198      select_id = Some (child_id);
199      break
200    }
201  }
202  if let Some (first_id) = select_id {
203    let select_id   = first_id.clone();
204    let select_first = Box::new (move |controller : &mut Controller|{
205      let selection = Selection::try_ref_mut (&mut controller.component)
206        .unwrap();
207      selection.current = Some (select_id);
208    });
209    action_buffer.push ((menu_id.clone(),
210      Action::ModifyController (select_first)));
211    action_buffer.push ((first_id.clone(), Action::Focus));
212  }
213  log::trace!("...first_item");
214}
215
216/// Builtin button control ID 'MenuLastItem'
217///
218/// This control should target the "root" frame node of the menu, and assumes
219/// that there is at least one focused item node after the initial selection
220/// node.
221pub fn last_item (
222  _             : &controls::button::Release,
223  elements      : &Tree <Element>,
224  frame_id      : &NodeId,
225  action_buffer : &mut Vec <(NodeId, Action)>
226) {
227  use controller::component::{Kind, Selection};
228  log::trace!("last_item...");
229  let frame = elements.get (frame_id).unwrap();
230  let mut children_ids = frame.children().iter();
231  let menu_id = children_ids.next().unwrap();
232  let mut select_id = None;
233  for child_id in children_ids.rev() {
234    let next = elements.get_element (child_id);
235    if next.controller.state == controller::State::Enabled &&
236      super::Frame::try_from (next).is_ok()
237    {
238      select_id = Some (child_id);
239      break
240    }
241  }
242  if let Some (last_id) = select_id {
243    let select_id   = last_id.clone();
244    let select_last = Box::new (move |controller : &mut Controller|{
245      let selection = Selection::try_ref_mut (&mut controller.component)
246        .unwrap();
247      selection.current = Some (select_id);
248    });
249    action_buffer.push ((menu_id.clone(),
250      Action::ModifyController (select_last)));
251    action_buffer.push ((last_id.clone(), Action::Focus));
252  }
253  log::trace!("...last_item");
254}
255
256//
257//  util
258//
259
260pub fn get_selected_id <'a> (elements : &'a Tree <Element>, menu_id : &NodeId)
261  -> Option <&'a NodeId>
262{
263  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
264  selection.current.as_ref()
265}
266
267/// Given a list of node IDs, finds the first that is an enabled frame, if any
268pub (crate) fn find_first_item (
269  elements : &Tree <Element>, node_ids : std::slice::Iter <NodeId>
270) -> Option <NodeId> {
271  let mut first_item = None;
272  for node_id in node_ids {
273    let node = elements.get_element (node_id);
274    if node.controller.state == controller::State::Enabled &&
275      Frame::try_from (node).is_ok()
276    {
277      first_item = Some (node_id.clone());
278      break
279    }
280  }
281  first_item
282}
283//
284//  builder
285//
286
287mod builder {
288  use derive_builder::Builder;
289  use crate::prelude::*;
290
291  #[derive(Builder)]
292  #[builder(pattern="owned", build_fn(private), setter(strip_option))]
293  pub struct Menu <'a, A : Application> {
294    elements     : &'a Tree <Element>,
295    frame_id     : &'a NodeId,
296    #[builder(default)]
297    bindings     : Option <&'a controller::Bindings <A>>,
298    #[builder(default)]
299    selection    : controller::component::Selection
300  }
301
302  impl <'a, A : Application> MenuBuilder <'a, A> {
303    pub fn new (elements : &'a Tree <Element>, frame_id  : &'a NodeId) -> Self {
304      MenuBuilder {
305        elements:  Some (elements),
306        frame_id:  Some (frame_id),
307        bindings:  None,
308        selection: None,
309      }
310    }
311  }
312
313  // NOTE: using the bindings in the closure requires this 'static bound
314  impl <'a, A : Application + 'static> BuildActions for MenuBuilder <'a, A> {
315    /// Creates a new Menu prepended to the target Frame and adds menu controls
316    /// to the target Frame.
317    ///
318    /// This is intended to be called after items have been added, and will set
319    /// `focus_top = false` on all items.
320    fn build_actions (self) -> Vec<(NodeId, Action)> {
321      log::trace!("build actions...");
322      let Menu { elements, frame_id, bindings, selection } = self.build()
323        .map_err(|err| log::error!("frame builder error: {:?}", err))
324        .unwrap();
325      let bindings_empty = controller::Bindings::empty();
326      let bindings = bindings.unwrap_or (&bindings_empty)
327        .get_bindings (&super::CONTROLS);
328      let mut out = Vec::new();
329      { // add menu controls to frame
330        let set_controls = Box::new (move |controller : &mut Controller|
331          controller.add_bindings (&bindings));
332        out.push ((frame_id.clone(), Action::ModifyController (set_controls)));
333      }
334      { // disable focus_top on all items
335        let set_focus_top_false = Box::new (|controller : &mut Controller|
336          controller.focus_top = false);
337        for child_id in elements.children_ids (frame_id).unwrap() {
338          out.push ((child_id.clone(),
339            Action::ModifyController (set_focus_top_false.clone())));
340        }
341      }
342      { // prepend selection node
343        let selection = {
344          let controller = controller::Component::from (selection).into();
345          Element::new (
346            "Menu".to_string(), controller, Model::default(), View::default())
347        };
348        out.push ((
349          frame_id.clone(),
350          Action::create_singleton (selection, CreateOrder::Prepend)));
351      }
352      // refocus to selected item parent is focused
353      if elements.get_element (frame_id).controller.state ==
354        controller::State::Focused
355      {
356        if let Some (focus_id) = super::find_first_item (
357          elements, elements.get (frame_id).unwrap().children().iter()
358        ) {
359          out.push ((focus_id, Action::Focus))
360        }
361      }
362      log::trace!("...build actions");
363      out
364    }
365  }
366}