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