gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
//! Text entry widget.
//!
//! ```text
//! +--------+
//! | frame  |
//! +---+----+
//!     |
//! +---+---+
//! | field |
//! +-------+
//! ```

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

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

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

pub type Field <'element> = Widget <'element,
  controller::component::Cursor, String, view::component::Body>;

//
//  controls
//

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

/// Builtin text control ID `FieldLineEntry`.
///
/// Note: will ignore `'\t'` and `'\n'` character input, but will allow any string
/// input.
pub fn line_entry (
  input         : &input::Text,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  use std::convert::TryInto;
  log::trace!("line_entry...");
  let Widget (cursor, text, _) = Field::try_get (elements, node_id).unwrap();
  if let Some (new_text)  = match input {
    input::Text::Char (ch) => match ch {
      '\t' | '\n' => None,
      c if c.is_ascii_control() => None,
      c => if cursor.length.is_none_or (|length| (text.len() as u32) < length) {
        let pushchar = || {
          let mut text = text.clone();
          text.push (*ch);
          Some (text)
        };
        if let Ok (keycode) = (*c).try_into() {
          if cursor.ignore.contains (&keycode) {
            None
          } else {
            pushchar()
          }
        } else {
          pushchar()
        }
      } else {
        None
      }
    }
    input::Text::String (string) => {
      let remaining = cursor.length.map_or_else (|| string.len(),
        |length| (length as usize).saturating_sub (text.len()));
      if remaining > 0 {
        let mut text = text.clone();
        text.extend (string.chars().take (remaining));
        Some (text)
      } else {
        None
      }
    }
  } {
    set_contents (elements, node_id, new_text, None, action_buffer);
  }
  log::trace!("...line_entry");
}

/// Builtin button control ID `FieldBackspace`
pub fn backspace (
  _             : &controls::button::Release,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("backspace...");
  let Widget (_, text, _) = Field::try_get (elements, node_id).unwrap();
  if text.len() > 0 {
    let mut new_text = text.clone();
    let _ = new_text.pop().unwrap();
    set_contents (elements, node_id, new_text, None, action_buffer);
  }
  log::trace!("...backspace");
}

//
//  utils
//

/// Utility function to set the field contents to the given string
pub fn set_contents (
  elements      : &Tree <Element>,
  field_id      : &NodeId,
  contents      : String,
  focused       : Option <bool>,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  let new_body = {
    let field = elements.get_element (field_id);
    let Widget (cursor, _, _) = Field::try_get (elements, field_id).unwrap();
    let Widget (_, _, canvas) =
      Frame::try_get (elements, elements.get_parent_id (field_id)).unwrap();
    let focused = focused
      .unwrap_or (field.controller.state == controller::State::Focused);
    body (canvas, cursor, &contents, focused)
  };
  { // update model
    let update_text = Box::new (|model : &mut Model| model.component = contents.into());
    action_buffer.push ((field_id.clone(), Action::ModifyModel (update_text)));
  }
  { // update view
    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
    action_buffer.push ((field_id.clone(), Action::ModifyView (update_body)));
  }
}

//
//  crate
//

pub (crate) fn body (
  canvas  : &view::component::Canvas,
  cursor  : &controller::component::Cursor,
  text    : &str,
  focused : bool
) -> view::component::Body {
  //use controller::{alignment, Offset};
  log::trace!("body...");
  let body  = {
    let mut body = {
      let (body_width, _body_height) = canvas.body_wh();
      let end   = text.len();
      let begin = {
        let mut begin = text.len().saturating_sub (body_width as usize-1);
        if !focused {
          begin = begin.saturating_sub (1);
        }
        begin
      };
      text[begin..end].to_string()
    };
    if focused {
      let caret = char::try_from (cursor.caret).unwrap();
      body.push (caret);
    }
    view::component::Body (body)
  };
  log::trace!("...body");
  body
}

//
//  builder
//

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

  #[derive(Builder)]
  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
  struct Field <'a, A : Application> {
    elements     : &'a Tree <Element>,
    parent_id    : &'a NodeId,
    #[builder(default)]
    appearances  : Appearances,
    #[builder(default)]
    bindings     : Option <&'a Bindings <A>>,
    #[builder(default)]
    callback_id  : Option <application::CallbackId>,
    #[builder(default)]
    contents     : Option <String>,
    #[builder(default)]
    cursor       : Cursor,
    #[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,
    /// If this is true, overrides free layout options (`frame_anchor`,
    /// `frame_area`, `frame_offset`, `frame_width`)
    #[builder(default)]
    frame_tiled  : bool,
    #[builder(default = "size::Unsigned::Absolute (24)")]
    frame_width  : size::Unsigned,
    #[builder(default)]
    name         : Option <String>,
    // TODO: move this into bindings?
    #[builder(default)]
    text_control : Option <controls::Text>
  }

  impl <'a, A : Application> FieldBuilder <'a, A> {
    pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
      FieldBuilder {
        elements:          Some (elements),
        parent_id:         Some (parent_id),
        appearances:       None,
        bindings:          None,
        callback_id:       None,
        contents:          None,
        cursor:            None,
        frame_anchor:      None,
        frame_appearances: None,
        frame_area:        None,
        frame_bindings:    None,
        frame_border:      None,
        frame_clear_color: None,
        frame_offset:      None,
        frame_tiled:       None,
        frame_width:       None,
        name:              None,
        text_control:      None
      }
    }
  }

  impl <A : Application> BuildActions for FieldBuilder <'_, A> {
    fn build_actions (self) -> Vec<(NodeId, Action)> {
      use std::convert::TryInto;
      use crate::tree::{InsertBehavior, Node};
      use controller::component::layout;
      use view::coordinates;
      log::trace!("build actions...");
      let Field {
        elements, parent_id, appearances, bindings, callback_id, contents, cursor,
        frame_anchor, frame_appearances, frame_area, frame_bindings, frame_border,
        frame_clear_color, frame_offset, frame_tiled, frame_width, name, text_control
      } = self.build()
        .map_err(|err| log::error!("frame builder error: {err:?}")).unwrap();
      let bindings_empty = Bindings::empty();
      let bindings = {
        let mut bindings = bindings.unwrap_or (&bindings_empty)
          .get_bindings (&super::CONTROLS);
        bindings.text = text_control.map (Into::into)
          .or (Some (controls::text::Builtin::FieldLineEntry.into()));
        bindings
      };
      let frame_bindings = frame_bindings.unwrap_or (&bindings_empty);
      { // only allow tile coordinate parents
        let Widget (_, _, canvas) = Frame::try_get (elements, parent_id).unwrap();
        assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
      }
      let mut out = vec![];
      let (mut subtree, order) = {
        let (_, border_h) = frame_border.as_ref().map_or ((0, 0), Border::total_wh);
        let height = border_h as u32 + 1;
        let layout = if frame_tiled {
          layout::Variant::from (layout::Tiled::Absolute (
            std::num::NonZeroU32::new (height).unwrap()))
        } else {
          let anchor = frame_anchor;
          let offset = frame_offset;
          let size   = {
            let width  = frame_width;
            let height = height.into();
            Size { width, height }
          };
          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 field = {
        let contents   = contents.unwrap_or_else (String::new);
        let controller = {
          let component = cursor.clone().into();
          let mut controller     = Controller::with_bindings (&bindings);
          controller.component   = component;
          controller.appearances = appearances;
          controller
        };
        let model = Model { callback_id, component: contents.clone().into() };
        let view = {
          let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id).unwrap();
          view::Component::from (super::body (canvas, &cursor, &contents, false)).into()
        };
        let name = name.unwrap_or ("Field".to_string());
        Element::new (name, controller, model, view)
      };
      let _ = subtree.insert (Node::new (field), InsertBehavior::UnderNode (&frame_id))
        .unwrap();
      out.push ((parent_id.clone(), Action::Create (subtree, order)));
      log::trace!("...build actions");
      out
    }
  }
}