gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
//! Number box.
//!
//! ```text
//! +--------+
//! | frame  |
//! +---+----+
//!     |
//! +---+----+
//! | numbox |
//! +--------+
//! ```

use std::sync::LazyLock;

use crate::TreeHelper;
use crate::interface::{view, Action, Element, Model, View};
use crate::interface::controller::{self, controls, Controls};
use crate::tree::{Tree, NodeId};

use super::{Widget, Frame};

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

pub type Numbox <'element> =
  Widget <'element, controller::component::Numeral, f64, view::component::Body>;

//
//  controls
//

pub static CONTROLS : LazyLock <Controls> = LazyLock::new (||
  controls::Builder::new()
    .buttons ([
      controls::button::Builtin::NumboxIncrease,
      controls::button::Builtin::NumboxDecrease,
      controls::button::Builtin::NumboxIncrease10x,
      controls::button::Builtin::NumboxDecrease10x
    ].into_iter().map (Into::into).collect::<Vec <_>>().into())
    .build()
);

/// Builtin button control ID `NumboxDecrease`
pub fn decrease (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("decrease...");
  let Widget (numeral, value, _) = Numbox::try_get (elements, node_id).unwrap();
  let Widget (_, _, canvas) =
    Frame::try_get (elements, elements.get_parent_id (node_id)).unwrap();
  let new_value = numeral.modify (*value, -1.0);
  let new_body  = body (canvas, numeral, new_value);
  { // update model
    let update_value =
      Box::new (move |model : &mut Model| model.component = new_value.into());
    action_buffer.push ((node_id.clone(), Action::ModifyModel (update_value)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((node_id.clone(), Action::ModifyView (update_body)));
  }
  log::trace!("...decrease");
}

/// Builtin button control ID `NumboxIncrease`
pub fn increase (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("increase...");
  let Widget (numeral, value, _) = Numbox::try_get (elements, node_id).unwrap();
  let Widget (_, _, canvas) =
    Frame::try_get (elements, elements.get_parent_id (node_id)).unwrap();
  let new_value = numeral.modify (*value, 1.0);
  let new_body  = body (canvas, numeral, new_value);
  { // update model
    let update_value =
      Box::new (move |model : &mut Model| model.component = new_value.into());
    action_buffer.push ((node_id.clone(), Action::ModifyModel (update_value)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((node_id.clone(), Action::ModifyView (update_body)));
  }
  log::trace!("...increase");
}

/// Builtin button control ID `NumboxDecrease10x`
pub fn decrease_10x (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("decrease_10x...");
  let Widget (numeral, value, _) = Numbox::try_get (elements, node_id).unwrap();
  let Widget (_, _, canvas) =
    Frame::try_get (elements, elements.get_parent_id (node_id)).unwrap();
  let new_value = numeral.modify (*value, -10.0);
  let new_body  = body (canvas, numeral, new_value);
  { // update model
    let update_value =
      Box::new (move |model : &mut Model| model.component = new_value.into());
    action_buffer.push ((node_id.clone(), Action::ModifyModel (update_value)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((node_id.clone(), Action::ModifyView (update_body)));
  }
  log::trace!("...decrease_10x");
}

/// Builtin button control ID `NumboxIncrease9x`
pub fn increase_10x (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("increase_10x...");
  let Widget (numeral, value, _) = Numbox::try_get (elements, node_id).unwrap();
  let Widget (_, _, canvas) =
    Frame::try_get (elements, elements.get_parent_id (node_id)).unwrap();
  let new_value = numeral.modify (*value, 10.0);
  let new_body  = body (canvas, numeral, new_value);
  { // update model
    let update_value =
      Box::new (move |model : &mut Model| model.component = new_value.into());
    action_buffer.push ((node_id.clone(), Action::ModifyModel (update_value)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((node_id.clone(), Action::ModifyView (update_body)));
  }
  log::trace!("...increase_10x");
}

//
//  utils
//

/// Utility function to set the umbox contents to the given value
pub fn set_value (
  elements      : &Tree <Element>,
  numbox_id     : &NodeId,
  value         : f64,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  let Widget (numeral, _, _) = Numbox::try_get (elements, numbox_id).unwrap();
  let Widget (_, _, canvas) =
    Frame::try_get (elements, elements.get_parent_id (numbox_id)).unwrap();
  let new_body = body (canvas, numeral, value);
  { // update model
    let update_value =
      Box::new (move |model : &mut Model| model.component = value.into());
    action_buffer.push ((numbox_id.clone(), Action::ModifyModel (update_value)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((numbox_id.clone(), Action::ModifyView (update_body)));
  }
}

//
//  crate
//

pub (crate) fn body (
  canvas  : &view::component::Canvas,
  numeral : &controller::component::Numeral,
  value   : f64
) -> view::component::Body {
  use controller::alignment::Horizontal;
  log::trace!("body...");
  let body = {
    let body_width = canvas.body_wh().0 as usize;
    let mut string = numeral.format.format_number (value);
    let digits = match numeral.alignment {
      Horizontal::Left => {
        string.truncate (body_width);
        string
      }
      Horizontal::Right => string.split_off (string.len().saturating_sub (body_width)),
      Horizontal::Center => if string.len() > body_width {
        let extra = string.len() - body_width;
        string.truncate (string.len() - extra / 2);
        let remaining = extra - extra / 2;
        string.split_off (remaining)
      } else {
        string
      }
    };
    debug_assert!(digits.len() <= body_width);
    match numeral.alignment {
      Horizontal::Left => view::component::Body (digits),
      Horizontal::Right => view::component::Body (format!("{digits:>body_width$}")),
      Horizontal::Center => view::component::Body (format!("{:>1$}", digits,
        body_width - (body_width - digits.len() / 2)))
    }
  };
  log::trace!("...body");
  body
}

//
//  private
//

fn canvas_size (frame_border : Option <&view::Border>, length : u32)
  -> controller::Size
{
  let (border_w, border_h) = frame_border.map_or ((0, 0), view::Border::total_wh);
  let width  = (border_w as u32 + length).into();
  let height = (border_h as u32 + 1).into();
  controller::Size { width, height }
}

//
//  builder
//

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

  #[derive(Builder)]
  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
  struct Numbox <'a, A : Application> {
    elements          : &'a Tree <Element>,
    parent_id         : &'a NodeId,
    #[builder(default)]
    appearances       : Appearances,
    #[builder(default)]
    bindings          : Option <&'a Bindings <A>>,
    #[builder(default)]
    frame_anchor      : Alignment,
    #[builder(default)]
    frame_appearances : Appearances,
    #[builder(default)]
    frame_area        : Area,
    #[builder(default)]
    frame_bindings    : Option <&'a Bindings <A>>,
    #[builder(default)]
    frame_border      : Option <Border>,
    #[builder(default)]
    frame_clear_color : Option <canvas::ClearColor>,
    #[builder(default)]
    frame_offset      : Offset,
    #[builder(default = "12")]
    length            : u32,
    #[builder(default)]
    name              : Option <String>,
    #[builder(default)]
    numeral           : Numeral,
    #[builder(default)]
    value             : Option <f64>
  }

  impl <'a, A : Application> NumboxBuilder <'a, A> {
    pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
      NumboxBuilder {
        elements:          Some (elements),
        parent_id:         Some (parent_id),
        appearances:       None,
        bindings:          None,
        frame_anchor:      None,
        frame_appearances: None,
        frame_area:        None,
        frame_bindings:    None,
        frame_border:      None,
        frame_clear_color: None,
        frame_offset:      None,
        length:            None,
        name:              None,
        numeral:           None,
        value:             None
      }
    }
  }

  impl <A : Application> BuildActions for NumboxBuilder <'_, A> {
    fn build_actions (self) -> Vec<(NodeId, Action)> {
      use std::convert::TryInto;
      use crate::tree::{InsertBehavior, Node};
      use controller::component::layout;
      log::trace!("build actions...");
      let Numbox {
        elements, parent_id, appearances, bindings, frame_anchor, frame_appearances,
        frame_area, frame_bindings, frame_border, frame_clear_color, frame_offset,
        length, name, numeral, value
      } = 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 frame_bindings = frame_bindings.unwrap_or (&bindings_empty);
      if length == 0 {
        log::warn!("zero length number box field");
      }
      { // only allow tile coordinate parents
        let Widget (_, _, canvas) = Frame::try_get (elements, parent_id).unwrap();
        assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
      }
      let value   = value.unwrap_or (0.0);
      let mut out = vec![];
      let (mut subtree, order) = {
        let layout = {
          let anchor = frame_anchor;
          let offset = frame_offset;
          let size   = canvas_size (frame_border.as_ref(), length);
          layout::Variant::from ((layout::Free { anchor, offset, size }, frame_area))
        };
        let mut actions = {
          let mut frame = frame::Builder::new (elements, parent_id)
            .appearances (frame_appearances)
            .bindings (frame_bindings)
            .layout (layout.into());
          set_option!(frame, border, frame_border);
          set_option!(frame, clear_color, frame_clear_color);
          frame.build_actions()
        };
        out.extend (actions.drain (1..));
        debug_assert_eq!(actions.len(), 1);
        actions.pop().unwrap().1.try_into().unwrap()
      };
      let frame_id = subtree.root_node_id().unwrap().clone();
      let numbox = {
        let controller = {
          let component          = numeral.clone().into();
          let mut controller     = Controller::with_bindings (&bindings);
          controller.component   = component;
          controller.appearances = appearances;
          controller
        };
        let model = Model { component: value.into(), .. Model::default() };
        let view  = {
          let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id).unwrap();
          view::Component::from (super::body (canvas, &numeral, value)).into()
        };
        let name = name.unwrap_or ("Numbox".to_string());
        Element::new (name, controller, model, view)
      };
      let _ = subtree
        .insert (Node::new (numbox), InsertBehavior::UnderNode (&frame_id))
        .unwrap();
      out.push ((parent_id.clone(), Action::Create (subtree, order)));
      log::trace!("...build actions");
      out
    }
  }
}