gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
//! Selection widget.
//!
//! ```text
//! +--------+
//! | frame  |
//! +---+----+
//!     |\
//!     |  \-------+----------+---- ...
//!     |          |          |
//!  +--+---+  +---+---+  +---+---+
//!  | menu |  | item0 |  | item1 | ...
//!  +------+  +---+---+  +---+---+
//!               /|\        /|\
//! ```
//!
//! The menu (selection) widget is the initial element parented to a frame widget. The
//! `next_item` and `previous_item` control functions target the parent frame.

use std::convert::TryFrom;
use std::sync::LazyLock;

use crate::prelude::*;

pub use self::builder::MenuBuilder as Builder;

pub type Menu <'element> = Widget <'element,
  Selection, model::Component, view::Component>;

//
//  controls
//

pub static CONTROLS : LazyLock <Controls> = LazyLock::new (||
  controls::Builder::new()
    .buttons (vec![
      controls::button::Builtin::MenuNextItem,
      controls::button::Builtin::MenuPreviousItem,
      controls::button::Builtin::MenuFirstItem,
      controls::button::Builtin::MenuLastItem
    ].into_iter().map (Into::into).collect::<Vec <_>>().into())
    .build()
);

/// Builtin button control ID `MenuNextItem`.
///
/// This control should target the "root" frame node of the menu, and assumes that there
/// is at least one focused item node after the initial selection node.
pub fn next_item (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  frame_id      : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  use controller::component::{Kind, Selection};
  log::trace!("next_item...");
  let frame = elements.get (frame_id).unwrap();
  let mut children_ids = frame.children().iter();
  let menu_id = children_ids.next().unwrap();
  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
  if let Some (selected_id) = selection.current.as_ref() {
    // find the currently selected child node
    for child_id in children_ids.by_ref() {
      if child_id == selected_id {
        // should be focused
        debug_assert_eq!(
          elements.get_element (child_id).controller.state,
          State::Focused);
        break
      }
    }
  }
  // if none is selected, the next enabled child frame will be selected
  let mut select_id = None;
  for next_id in children_ids {
    let next = elements.get_element (next_id);
    if next.controller.state == State::Enabled && Frame::try_from (next).is_ok() {
      select_id = Some (next_id);
      break
    }
  }
  if select_id.is_none() && selection.loop_ {
    for next_id in frame.children().iter().skip (1) {
      let next = elements.get_element (next_id);
      if next.controller.state == State::Enabled && Frame::try_from (next).is_ok() {
        select_id = Some (next_id);
        break
      }
    }
  }
  if let Some (next_id) = select_id {
    let select_id   = next_id.clone();
    let select_next = Box::new (move |controller : &mut Controller|{
      let selection = Selection::try_ref_mut (&mut controller.component).unwrap();
      selection.current = Some (select_id);
    });
    action_buffer.push ((menu_id.clone(), Action::ModifyController (select_next)));
    action_buffer.push ((next_id.clone(), Action::Focus));
  }
  log::trace!("...next_item");
}

/// Builtin button control ID `MenuPreviousItem`.
///
/// This control should target the "root" frame node of the menu, and assumes that there
/// is at least one focused item node after the initial selection node.
pub fn previous_item (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  frame_id      : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  use controller::component::{Kind, Selection};
  log::trace!("previous_item...");
  let frame = elements.get (frame_id).unwrap();
  let mut children_ids = frame.children().iter();
  let menu_id = children_ids.next().unwrap();
  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
  let mut children_ids = children_ids.rev();
  if let Some (selected_id) = selection.current.as_ref() {
    // find the currently selected child node
    for child_id in children_ids.by_ref() {
      if child_id == selected_id {
        // should be focused
        debug_assert_eq!(
          elements.get_element (child_id).controller.state,
          State::Focused);
        break
      }
    }
  }
  // if none is selected, the previous enabled child will be selected
  let mut select_id = None;
  for prev_id in children_ids {
    let prev = elements.get_element (prev_id);
    if prev.controller.state == State::Enabled && Frame::try_from (prev).is_ok() {
      select_id = Some (prev_id);
      break
    }
  }
  if select_id.is_none() && selection.loop_ {
    for prev_id in frame.children().iter().rev() {
      let prev = elements.get_element (prev_id);
      if prev.controller.state == State::Enabled && Frame::try_from (prev).is_ok() {
        select_id = Some (prev_id);
        break
      }
    }
  }
  if let Some (prev_id) = select_id {
    let select_id   = prev_id.clone();
    let select_prev = Box::new (move |controller : &mut Controller|{
      let selection = Selection::try_ref_mut (&mut controller.component).unwrap();
      selection.current = Some (select_id);
    });
    action_buffer.push ((menu_id.clone(), Action::ModifyController (select_prev)));
    action_buffer.push ((prev_id.clone(), Action::Focus));
  }
  log::trace!("...previous_item");
}

/// Builtin button control ID `MenuFirstItem`.
///
/// This control should target the "root" frame node of the menu, and assumes that there
/// is at least one focused item node after the initial selection node.
pub fn first_item (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  frame_id      : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  use controller::component::{Kind, Selection};
  log::trace!("first_item...");
  let frame = elements.get (frame_id).unwrap();
  let mut children_ids = frame.children().iter();
  let menu_id = children_ids.next().unwrap();
  let mut select_id = None;
  for child_id in children_ids {
    let next = elements.get_element (child_id);
    if next.controller.state == State::Enabled && Frame::try_from (next).is_ok() {
      select_id = Some (child_id);
      break
    }
  }
  if let Some (first_id) = select_id {
    let select_id    = first_id.clone();
    let select_first = Box::new (move |controller : &mut Controller|{
      let selection = Selection::try_ref_mut (&mut controller.component).unwrap();
      selection.current = Some (select_id);
    });
    action_buffer.push ((menu_id.clone(),  Action::ModifyController (select_first)));
    action_buffer.push ((first_id.clone(), Action::Focus));
  }
  log::trace!("...first_item");
}

/// Builtin button control ID `MenuLastItem`.
///
/// This control should target the "root" frame node of the menu, and assumes that there
/// is at least one focused item node after the initial selection node.
pub fn last_item (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  frame_id      : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  use controller::component::{Kind, Selection};
  log::trace!("last_item...");
  let frame = elements.get (frame_id).unwrap();
  let mut children_ids = frame.children().iter();
  let menu_id = children_ids.next().unwrap();
  let mut select_id = None;
  for child_id in children_ids.rev() {
    let next = elements.get_element (child_id);
    if next.controller.state == State::Enabled && Frame::try_from (next).is_ok() {
      select_id = Some (child_id);
      break
    }
  }
  if let Some (last_id) = select_id {
    let select_id   = last_id.clone();
    let select_last = Box::new (move |controller : &mut Controller|{
      let selection = Selection::try_ref_mut (&mut controller.component).unwrap();
      selection.current = Some (select_id);
    });
    action_buffer.push ((menu_id.clone(), Action::ModifyController (select_last)));
    action_buffer.push ((last_id.clone(), Action::Focus));
  }
  log::trace!("...last_item");
}

//
//  util
//

pub fn get_selected_id <'a> (elements : &'a Tree <Element>, menu_id : &NodeId)
  -> Option <&'a NodeId>
{
  let Widget (selection, _, _) = Menu::try_get (elements, menu_id).unwrap();
  selection.current.as_ref()
}

/// Given a list of node IDs, finds the first that is an enabled frame, if any
pub (crate) fn find_first_item (
  elements : &Tree <Element>, node_ids : std::slice::Iter <NodeId>
) -> Option <NodeId> {
  let mut first_item = None;
  for node_id in node_ids {
    let node = elements.get_element (node_id);
    if node.controller.state == State::Enabled && Frame::try_from (node).is_ok() {
      first_item = Some (node_id.clone());
      break
    }
  }
  first_item
}
//
//  builder
//

mod builder {
  use derive_builder::Builder;
  use crate::prelude::*;

  #[derive(Builder)]
  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
  struct Menu <'a, A : Application> {
    elements     : &'a Tree <Element>,
    frame_id     : &'a NodeId,
    #[builder(default)]
    bindings     : Option <&'a Bindings <A>>,
    #[builder(default)]
    name         : Option <String>,
    #[builder(default)]
    selection    : Selection
  }

  impl <'a, A : Application> MenuBuilder <'a, A> {
    pub const fn new (elements : &'a Tree <Element>, frame_id  : &'a NodeId) -> Self {
      MenuBuilder {
        elements:  Some (elements),
        frame_id:  Some (frame_id),
        bindings:  None,
        name:      None,
        selection: None
      }
    }
  }

  // NOTE: using the bindings in the closure requires this 'static bound
  impl <A : Application + 'static> BuildActions for MenuBuilder <'_, A> {
    /// Creates a new Menu prepended to the target Frame and adds menu controls to the
    /// target Frame.
    ///
    /// This is intended to be called after items have been added, and will set
    /// `focus_top = false` on all items.
    fn build_actions (self) -> Vec<(NodeId, Action)> {
      log::trace!("build actions...");
      let Menu { elements, frame_id, bindings, name, selection } = self.build()
        .map_err(|err| log::error!("frame builder error: {err:?}"))
        .unwrap();
      let bindings_empty = Bindings::empty();
      let bindings = bindings.unwrap_or (&bindings_empty)
        .get_bindings (&super::CONTROLS);
      let mut out = Vec::new();
      { // add menu controls to frame
        let set_controls = Box::new (move |controller : &mut Controller|
          controller.add_bindings (&bindings));
        out.push ((frame_id.clone(), Action::ModifyController (set_controls)));
      }
      { // disable focus_top on all items
        let set_focus_top_false = Box::new (|controller : &mut Controller|
          controller.focus_top = false);
        for child_id in elements.children_ids (frame_id).unwrap() {
          out.push ((child_id.clone(),
            Action::ModifyController (set_focus_top_false.clone())));
        }
      }
      { // prepend selection node
        let selection = {
          let controller = ControllerComponent::from (selection).into();
          let name = name.unwrap_or ("Menu".to_string());
          Element::new (name, controller, Model::default(), View::default())
        };
        out.push ((
          frame_id.clone(),
          Action::create_singleton (selection, CreateOrder::Prepend)));
      }
      // refocus to selected item parent is focused
      if elements.get_element (frame_id).controller.state == State::Focused
        && let Some (focus_id) = super::find_first_item (
          elements, elements.get (frame_id).unwrap().children().iter())
      {
        out.push ((focus_id, Action::Focus))
      }
      log::trace!("...build actions");
      out
    }
  }
}