gooey/widget/
field.rs

1//! Text entry widget.
2//!
3//! ```text
4//! +--------+
5//! | frame  |
6//! +---+----+
7//!     |
8//! +---+---+
9//! | field |
10//! +-------+
11//! ```
12
13use std::convert::TryFrom;
14use lazy_static::lazy_static;
15
16use crate::TreeHelper;
17use crate::tree::{Tree, NodeId};
18use crate::interface::{Action, Element, Model, View};
19use crate::interface::view::{self, input};
20use crate::interface::controller::{self, controls, Controls};
21use super::{Widget, Frame};
22
23pub use self::builder::FieldBuilder as Builder;
24
25pub type Field <'element> = Widget <'element,
26  controller::component::Cursor, String, view::component::Body>;
27
28//
29//  controls
30//
31
32lazy_static!{
33  pub static ref CONTROLS : Controls = controls::Builder::new()
34    .buttons (vec![
35      controls::button::Builtin::FieldBackspace,
36      controls::button::Builtin::FormSubmitCallback
37    ].into_iter().map (Into::into).collect::<Vec <_>>().into())
38    .build();
39}
40
41/// Builtin text control ID 'FieldLineEntry'
42///
43/// Note: will ignore `'\t'` and `'\n'` character input, but will allow any
44/// string input
45pub fn line_entry (
46  input         : &input::Text,
47  elements      : &Tree <Element>,
48  node_id       : &NodeId,
49  action_buffer : &mut Vec <(NodeId, Action)>
50) {
51  use std::convert::TryInto;
52  log::trace!("line_entry...");
53  let Widget (cursor, text, _) = Field::try_get (elements, node_id).unwrap();
54  if let Some (new_text)  = match input {
55    input::Text::Char (ch) => match ch {
56      '\t' | '\n' => None,
57      c if c.is_ascii_control() => None,
58      c => if cursor.length.map (|length| (text.len() as u32) < length)
59        .unwrap_or (true)
60      {
61        let pushchar = ||{
62          let mut text = text.clone();
63          text.push (*ch);
64          Some (text)
65        };
66        if let Ok (keycode) = (*c).try_into() {
67          if cursor.ignore.contains (&keycode) {
68            None
69          } else {
70            pushchar()
71          }
72        } else {
73          pushchar()
74        }
75      } else {
76        None
77      }
78    }
79    input::Text::String (string) => {
80      let remaining = cursor.length
81        .map (|length| (length as usize).saturating_sub (text.len()))
82        .unwrap_or (string.len());
83      if remaining > 0 {
84        let mut text = text.clone();
85        text.extend (string.chars().take (remaining));
86        Some (text)
87      } else {
88        None
89      }
90    }
91  } {
92    set_contents (elements, node_id, new_text, None, action_buffer);
93  }
94  log::trace!("...line_entry");
95}
96
97/// Builtin button control ID 'FieldBackspace'
98pub fn backspace (
99  _             : &controls::button::Release,
100  elements      : &Tree <Element>,
101  node_id       : &NodeId,
102  action_buffer : &mut Vec <(NodeId, Action)>
103) {
104  log::trace!("backspace...");
105  let Widget (_, text, _) = Field::try_get (elements, node_id).unwrap();
106  if text.len() > 0 {
107    let mut new_text = text.clone();
108    let _ = new_text.pop().unwrap();
109    set_contents (elements, node_id, new_text, None, action_buffer);
110  }
111  log::trace!("...backspace");
112}
113
114//
115//  utils
116//
117
118/// Utility function to set the field contents to the given string
119pub fn set_contents (
120  elements      : &Tree <Element>,
121  field_id      : &NodeId,
122  contents      : String,
123  focused       : Option <bool>,
124  action_buffer : &mut Vec <(NodeId, Action)>
125) {
126  let new_body = {
127    let field = elements.get_element (field_id);
128    let Widget (cursor, _, _) = Field::try_get (elements, field_id).unwrap();
129    let Widget (_, _, canvas) =
130      Frame::try_get (elements, elements.get_parent_id (field_id)).unwrap();
131    let focused = focused
132      .unwrap_or (field.controller.state == controller::State::Focused);
133    body (canvas, &cursor, &contents, focused)
134  };
135  { // update model
136    let update_text =
137      Box::new (|model : &mut Model| model.component = contents.into());
138    action_buffer.push ((field_id.clone(), Action::ModifyModel (update_text)));
139  }
140  { // update view
141    let update_body =
142      Box::new (|view : &mut View| view.component = new_body.into());
143    action_buffer.push ((field_id.clone(), Action::ModifyView (update_body)));
144  }
145}
146
147//
148//  crate
149//
150
151pub (crate) fn body (
152  canvas  : &view::component::Canvas,
153  cursor  : &controller::component::Cursor,
154  text    : &String,
155  focused : bool
156) -> view::component::Body {
157  //use controller::{alignment, Offset};
158  log::trace!("body...");
159  let body  = {
160    let mut body = {
161      let (body_width, _body_height) = canvas.body_wh();
162      let end   = text.len();
163      let begin = {
164        let mut begin = text.len().saturating_sub (body_width as usize-1);
165        if !focused {
166          begin = begin.saturating_sub (1);
167        }
168        begin
169      };
170      (&text[begin..end]).to_string()
171    };
172    if focused {
173      let caret = char::try_from (cursor.caret).unwrap();
174      body.push (caret);
175    }
176    view::component::Body (body)
177  };
178  log::trace!("...body");
179  body
180}
181
182//
183//  builder
184//
185
186mod builder {
187  use derive_builder::Builder;
188  use crate::prelude::*;
189
190  #[derive(Builder)]
191  #[builder(pattern="owned", build_fn(private), setter(strip_option))]
192  pub struct Field <'a, A : Application> {
193    elements     : &'a Tree <Element>,
194    parent_id    : &'a NodeId,
195    #[builder(default)]
196    appearances  : controller::Appearances,
197    #[builder(default)]
198    bindings     : Option <&'a controller::Bindings <A>>,
199    #[builder(default)]
200    callback_id  : Option <application::CallbackId>,
201    #[builder(default)]
202    contents     : Option <String>,
203    #[builder(default)]
204    cursor       : controller::component::Cursor,
205    #[builder(default)]
206    frame_anchor : controller::Alignment,
207    #[builder(default)]
208    frame_appearances : controller::Appearances,
209    #[builder(default)]
210    frame_area   : controller::Area,
211    #[builder(default)]
212    frame_bindings : Option <&'a controller::Bindings <A>>,
213    #[builder(default)]
214    frame_border : Option <view::Border>,
215    #[builder(default)]
216    frame_clear_color : Option <canvas::ClearColor>,
217    #[builder(default)]
218    frame_offset : controller::Offset,
219    /// If this is true, overrides free layout options (`frame_anchor`,
220    /// `frame_area`, `frame_offset`, `frame_width`)
221    #[builder(default)]
222    frame_tiled  : bool,
223    #[builder(default = "controller::size::Unsigned::Absolute (24)")]
224    frame_width  : controller::size::Unsigned,
225    // TODO: move this into bindings?
226    #[builder(default)]
227    text_control : Option <controls::Text>
228  }
229
230  impl <'a, A : Application> FieldBuilder <'a, A> {
231    pub fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
232      FieldBuilder {
233        elements:          Some (elements),
234        parent_id:         Some (parent_id),
235        appearances:       None,
236        bindings:          None,
237        callback_id:       None,
238        contents:          None,
239        cursor:            None,
240        frame_anchor:      None,
241        frame_appearances: None,
242        frame_area:        None,
243        frame_bindings:    None,
244        frame_border:      None,
245        frame_clear_color: None,
246        frame_offset:      None,
247        frame_tiled:       None,
248        frame_width:       None,
249        text_control:      None
250      }
251    }
252  }
253
254  impl <'a, A : Application> BuildActions for FieldBuilder <'a, A> {
255    fn build_actions (self) -> Vec<(NodeId, Action)> {
256      use std::convert::TryInto;
257      use crate::tree::{InsertBehavior, Node};
258      use controller::component::layout;
259      use view::coordinates;
260      log::trace!("build actions...");
261      let Field {
262        elements, parent_id, appearances, bindings, callback_id, contents,
263        cursor, frame_anchor, frame_appearances, frame_area, frame_bindings,
264        frame_border, frame_clear_color, frame_offset, frame_tiled, frame_width,
265        text_control
266      } = self.build()
267        .map_err(|err| log::error!("frame builder error: {:?}", err)).unwrap();
268      let bindings_empty = Bindings::empty();
269      let bindings = {
270        let mut bindings =
271          bindings.unwrap_or (&bindings_empty).get_bindings (&super::CONTROLS);
272        bindings.text = text_control.map (Into::into)
273          .or (Some (controls::text::Builtin::FieldLineEntry.into()));
274        bindings
275      };
276      let frame_bindings = frame_bindings.unwrap_or (&bindings_empty);
277      { // only allow tile coordinate parents
278        let Widget (_, _, canvas) = Frame::try_get (elements, parent_id)
279          .unwrap();
280        assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
281      }
282      let mut out = vec![];
283      let (mut subtree, order) = {
284        let (_, border_h) = frame_border.as_ref()
285          .map (view::Border::total_wh).unwrap_or ((0, 0));
286        let height = border_h as u32 + 1;
287        let layout = if frame_tiled {
288          layout::Variant::from (layout::Tiled::Absolute (
289            std::num::NonZeroU32::new (height).unwrap()))
290        } else {
291          let anchor = frame_anchor;
292          let offset = frame_offset;
293          let size   = {
294            let width  = frame_width;
295            let height = height.into();
296            controller::Size { width, height }
297          };
298          layout::Variant::from (
299            (layout::Free { anchor, offset, size }, frame_area))
300        };
301        let mut actions = {
302          let mut frame = frame::Builder::new (elements, parent_id)
303            .appearances (frame_appearances)
304            .bindings (frame_bindings)
305            .layout (layout.into());
306          set_option!(frame, border, frame_border);
307          set_option!(frame, clear_color, frame_clear_color);
308          frame.build_actions()
309        };
310        out.extend (actions.drain (1..));
311        debug_assert_eq!(actions.len(), 1);
312        actions.pop().unwrap().1.try_into().unwrap()
313      };
314      let frame_id = subtree.root_node_id().unwrap().clone();
315      let field = {
316        let contents   = contents.unwrap_or_else (|| String::new());
317        let controller = {
318          let component = cursor.clone().into();
319          let mut controller     = Controller::with_bindings (&bindings);
320          controller.component   = component;
321          controller.appearances = appearances;
322          controller
323        };
324        let model = Model {
325          callback_id, component: contents.clone().into()
326        };
327        let view = {
328          let Widget (_, _, canvas) =
329            Frame::try_get (&subtree, &frame_id).unwrap();
330          view::Component::from (super::body (canvas, &cursor, &contents, false))
331            .into()
332        };
333        Element::new ("Field".to_string(), controller, model, view)
334      };
335      let _ = subtree
336        .insert (Node::new (field), InsertBehavior::UnderNode (&frame_id))
337        .unwrap();
338      out.push ((parent_id.clone(), Action::Create (subtree, order)));
339      log::trace!("...build actions");
340      out
341    }
342  }
343}