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