gooey/widget/textbox/
mod.rs

1//! Display text data.
2//!
3//! ```text
4//! +--------+
5//! | frame  |
6//! +---+----+
7//!     |
8//! +---+-----+
9//! | textbox |
10//! +---------+
11//! ```
12
13use std::sync::LazyLock;
14
15use crate::{Application, Presentation, TreeHelper};
16use crate::tree::{Tree, NodeId};
17use crate::interface::{
18  model, view, Action, Controller, Element, Interface, Model, View
19};
20use crate::interface::controller::{self, controls, Controls};
21use super::{Widget, Frame, build_and_get_node_ids, frame, set_option};
22
23mod make_table;
24
25pub use self::builder::{TextboxBuilder as Builder, FitBuilder, WithFrameBuilder};
26pub use self::make_table::{box_vec, make_table, mk_table};
27
28pub type Textbox <'element> = Widget <'element,
29  controller::component::Scroll, String, view::component::Body>;
30
31//
32//  controls
33//
34
35pub static CONTROLS : LazyLock <Controls> = LazyLock::new (||
36  controls::Builder::new()
37    .buttons (vec![
38      controls::button::Builtin::TextboxScrollDown,
39      controls::button::Builtin::TextboxScrollUp,
40      controls::button::Builtin::TextboxScrollRight,
41      controls::button::Builtin::TextboxScrollLeft,
42      controls::button::Builtin::TextboxPageDown,
43      controls::button::Builtin::TextboxPageUp,
44      controls::button::Builtin::TextboxPageRight,
45      controls::button::Builtin::TextboxPageLeft
46    ].into_iter().map (Into::into).collect::<Vec <_>>().into())
47    .build()
48);
49
50/// Builtin textbox control ID `TextboxScrollRight`
51pub fn scroll_right (
52  _             : &controls::button::Release,
53  elements      : &Tree <Element>,
54  textbox_id    : &NodeId,
55  action_buffer : &mut Vec <(NodeId, Action)>
56) {
57  log::trace!("scroll_right...");
58  scroll_helper (elements, textbox_id, action_buffer,
59    controller::component::Scroll::offset_move_right, 1);
60  log::trace!("...scroll_right");
61}
62
63/// Builtin textbox control ID `TextboxScrollLeft`
64pub fn scroll_left (
65  _             : &controls::button::Release,
66  elements      : &Tree <Element>,
67  textbox_id    : &NodeId,
68  action_buffer : &mut Vec <(NodeId, Action)>
69) {
70  log::trace!("scroll_left...");
71  scroll_helper (elements, textbox_id, action_buffer,
72    controller::component::Scroll::offset_move_left, 1);
73  log::trace!("...scroll_left");
74}
75
76/// Builtin textbox control ID `TextboxScrollUp`
77pub fn scroll_up (
78  _             : &controls::button::Release,
79  elements      : &Tree <Element>,
80  textbox_id    : &NodeId,
81  action_buffer : &mut Vec <(NodeId, Action)>
82) {
83  log::trace!("scroll_up...");
84  scroll_helper (elements, textbox_id, action_buffer,
85    controller::component::Scroll::offset_move_up, 1);
86  log::trace!("...scroll_up");
87}
88
89/// Builtin textbox control ID `TextboxScrollDown`
90pub fn scroll_down (
91  _             : &controls::button::Release,
92  elements      : &Tree <Element>,
93  textbox_id    : &NodeId,
94  action_buffer : &mut Vec <(NodeId, Action)>
95) {
96  log::trace!("scroll_down...");
97  scroll_helper (elements, textbox_id, action_buffer,
98    controller::component::Scroll::offset_move_down, 1);
99  log::trace!("...scroll_down");
100}
101
102/// Builtin textbox control ID `TextboxPageRight`
103pub fn page_right (
104  _             : &controls::button::Release,
105  elements      : &Tree <Element>,
106  textbox_id    : &NodeId,
107  action_buffer : &mut Vec <(NodeId, Action)>
108) {
109  log::trace!("page_right...");
110  // scroll by width - 1
111  let (width, _) = {
112    let Widget (_, _, canvas) =
113      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
114    canvas.body_wh()
115  };
116  scroll_helper (elements, textbox_id, action_buffer,
117    controller::component::Scroll::offset_move_right, width-1);
118  log::trace!("...page_right");
119}
120
121/// Builtin textbox control ID `TextboxPageLeft`
122pub fn page_left (
123  _             : &controls::button::Release,
124  elements      : &Tree <Element>,
125  textbox_id    : &NodeId,
126  action_buffer : &mut Vec <(NodeId, Action)>
127) {
128  log::trace!("page_left...");
129  // scroll by width - 1
130  let (width, _) = {
131    let Widget (_, _, canvas) =
132      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
133    canvas.body_wh()
134  };
135  scroll_helper (elements, textbox_id, action_buffer,
136    controller::component::Scroll::offset_move_left, width-1);
137  log::trace!("...page_left");
138}
139
140/// Builtin textbox control ID `TextboxPageUp`
141pub fn page_up (
142  _             : &controls::button::Release,
143  elements      : &Tree <Element>,
144  textbox_id    : &NodeId,
145  action_buffer : &mut Vec <(NodeId, Action)>
146) {
147  log::trace!("page_up...");
148  // scroll by height - 1
149  let (_, height) = {
150    let Widget (_, _, canvas) =
151      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
152    canvas.body_wh()
153  };
154  scroll_helper (elements, textbox_id, action_buffer,
155    controller::component::Scroll::offset_move_up, height-1);
156  log::trace!("...page_up");
157}
158
159/// Builtin textbox control ID `TextboxPageDown`
160pub fn page_down (
161  _             : &controls::button::Release,
162  elements      : &Tree <Element>,
163  textbox_id    : &NodeId,
164  action_buffer : &mut Vec <(NodeId, Action)>
165) {
166  log::trace!("page_down...");
167  // scroll by height - 1
168  let (_, height) = {
169    let Widget (_, _, canvas) =
170      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
171    canvas.body_wh()
172  };
173  scroll_helper (elements, textbox_id, action_buffer,
174    controller::component::Scroll::offset_move_down, height-1);
175  log::trace!("...page_down");
176}
177
178//
179//  utils
180//
181
182/// Create a new textbox in the middle of the parent element and focus
183pub fn popup <A, P> (
184  interface   : &mut Interface <A, P>,
185  parent_id   : NodeId,
186  text        : String,
187  appearances : Option <controller::Appearances>,
188  border      : Option <view::Border>
189) -> [NodeId; 2] where
190  A : Application,
191  P : Presentation
192{
193  let mut builder = FitBuilder::<A>::new (interface.elements(), &parent_id)
194    .frame_anchor (controller::Alignment::middle_center())
195    .text (text);
196  set_option!(builder, frame_appearances, appearances);
197  set_option!(builder, frame_border, border);
198  let [frame_id, textbox_id] =
199    build_and_get_node_ids!(interface, builder, ["Frame", "Textbox"]);
200  let _ = interface.action (&frame_id, Action::Focus);
201  [frame_id, textbox_id]
202}
203
204/// Utility function to get the textbox contents
205pub fn get_contents (elements : &Tree <Element>, textbox_id : &NodeId)
206  -> String
207{
208  let Widget (_, contents, _) = Textbox::try_get (elements, textbox_id).unwrap();
209  contents.clone()
210}
211
212/// Utility function to set the textbox contents to the given string
213pub fn set_contents (
214  elements      : &Tree <Element>,
215  textbox_id    : &NodeId,
216  contents      : String,
217  action_buffer : &mut Vec <(NodeId, Action)>
218) {
219  let new_body = {
220    // TODO: we may need to update scroll here
221    let Widget (scroll, _, _) = Textbox::try_get (elements, textbox_id).unwrap();
222    let Widget (_, _, canvas) =
223      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
224    body (canvas, scroll, &contents)
225  };
226  { // update model
227    let update_text =
228      Box::new (|model : &mut Model| model.component = contents.into());
229    action_buffer.push ((textbox_id.clone(), Action::ModifyModel (update_text)));
230  }
231  { // update view
232    let update_body =
233      Box::new (|view : &mut View| view.component = new_body.into());
234    action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
235  }
236}
237
238/// Utility function to resize the textbox to the current contents
239pub fn fit_to_contents (
240  elements      : &Tree <Element>,
241  textbox_id    : &NodeId,
242  action_buffer : &mut Vec <(NodeId, Action)>
243) {
244  use controller::component::layout;
245  let (width, height) = {
246    let contents = get_contents (elements, textbox_id);
247    let lines    = contents.lines().map (str::len).collect::<Vec <_>>();
248    let height   = lines.len() as u32;
249    let width    = (*lines.iter().max().unwrap_or (&0)) as u32;
250    (width, height)
251  };
252  let frame_id = elements.get_parent_id (textbox_id);
253  let mut layout = {
254    let Widget (layout, _, _) = Frame::try_get (elements, frame_id).unwrap();
255    layout.clone()
256  };
257  match layout.variant {
258    layout::Variant::Free (ref mut free, _) => {
259      let old_size = free.size;
260      free.size.width  = width.into();
261      free.size.height = height.into();
262      if old_size != free.size {
263        frame::set_layout (elements, frame_id, layout, None, action_buffer);
264      }
265    }
266    layout::Variant::Tiled (_) =>
267      log::warn!("fit to contents called on tiled textbox frame")
268  }
269}
270
271/// Utility function to push a newline followed by the given string onto the textbox
272/// contents
273pub fn push_line (
274  elements      : &Tree <Element>,
275  textbox_id    : &NodeId,
276  line          : String,
277  action_buffer : &mut Vec <(NodeId, Action)>
278) {
279  let new_body = {
280    // TODO: we may need to update scroll here
281    let Widget (scroll, contents, _) = Textbox::try_get (elements, textbox_id).unwrap();
282    let mut contents = contents.clone();
283    contents.push ('\n');
284    contents.push_str (&line);
285    let Widget (_, _, canvas) =
286      Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
287    body (canvas, scroll, &contents)
288  };
289  { // update model
290    let update_text = Box::new (move |model : &mut Model| match model.component {
291      model::Component::Text (ref mut text) => {
292        text.push ('\n');
293        text.push_str (&line);
294      }
295      _ => unreachable!()
296    });
297    action_buffer.push ((textbox_id.clone(), Action::ModifyModel (update_text)));
298  }
299  { // update view
300    let update_body = Box::new (|view : &mut View| view.component = new_body.into());
301    action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
302  }
303}
304
305//
306//  crate
307//
308
309pub (crate) fn body (
310  canvas : &view::component::Canvas,
311  scroll : &controller::component::Scroll,
312  text   : &str
313) -> view::component::Body {
314  use controller::{alignment, offset};
315  log::trace!("body...");
316  let (body_width, body_height) = canvas.body_wh();
317  let lines   = text.lines().count() as u32;
318  let longest = text.lines().map (str::len).max().unwrap_or_default() as u32;
319  let scroll_horizontal = match scroll.offset.horizontal {
320    offset::Signed::Absolute (horizontal) => horizontal,
321    offset::Signed::Relative (horizontal) => {
322      let horizontal_max = longest.saturating_sub (body_width) as f32;
323      (*horizontal * horizontal_max).round() as i32
324    }
325  };
326  let scroll_vertical = match scroll.offset.vertical {
327    offset::Signed::Absolute (vertical) => vertical,
328    offset::Signed::Relative (vertical) => {
329      let vertical_max = lines.saturating_sub (body_height) as f32;
330      (*vertical * vertical_max).round() as i32
331    }
332  };
333  let mut body = String::new();
334  let (skip, body_top) = match scroll.alignment.vertical {
335    alignment::Vertical::Top    => (scroll_vertical as usize, 0),
336    alignment::Vertical::Bottom => {
337      for _ in 0..body_height.saturating_sub (lines) {
338        body.push ('\n');
339      }
340      ( (lines.saturating_sub (body_height) as i32 - scroll_vertical).max (0)
341          as usize,
342        0
343      )
344    }
345    alignment::Vertical::Middle => {
346      let center   = lines as i32 / 2;
347      let body_top = center - body_height as i32 / 2 - scroll_vertical;
348      for _ in body_top..0 {
349        body.push ('\n');
350      }
351      (body_top.max (0) as usize, body_top)
352    }
353  };
354  // NOTE: take at most body_height lines, but stops at the last line if too few; for
355  // middle scroll alignment subtract negative body_top from body_height
356  let take = (body_height as i32 + body_top.min (0)).max (0) as usize;
357  for line in text.lines().skip (skip).take (take) {
358    let len = line.len();
359    let (space, start, end) = match scroll.alignment.horizontal {
360      alignment::Horizontal::Left   => (
361        0,
362        scroll_horizontal  as usize,
363        (scroll_horizontal as usize + body_width as usize).min (len)
364      ),
365      alignment::Horizontal::Center => {
366        let center     = longest as i32 / 2;
367        let body_left  = center - body_width as i32 / 2 + scroll_horizontal;
368        let body_right = center + body_width as i32 / 2 + scroll_horizontal;
369        let line_start = center - len as i32 / 2;
370        let line_end   = center + len as i32 / 2 - (len as i32 + 1) % 2;
371        let space      = (line_start - body_left).max (0)      as usize;
372        let start      = (body_left - line_start).max (0)      as usize;
373        let end        = len - (line_end - body_right).max (0) as usize;
374        (space, start, end)
375      }
376      alignment::Horizontal::Right  =>
377        ( (body_width as usize + scroll_horizontal as usize)
378            .saturating_sub (len),
379          len.saturating_sub (body_width as usize)
380            .saturating_sub (scroll_horizontal as usize),
381          len
382        )
383    };
384    if start < end {
385      for _ in 0..space {
386        body.push ('\0');
387      }
388      // NOTE: previously we sliced here but if the string contains non-ascii
389      // characters the byte boundary will not be aligned
390      for ch in line.chars().skip (start).take (end - start) {
391        body.push (ch);
392      }
393      body.push ('\n');
394    } else if start == end {
395      body.push ('\n');
396    }
397  }
398  log::trace!("...body");
399  view::component::Body (body)
400}
401
402//
403//  private
404//
405
406fn scroll_helper (
407  elements      : &Tree <Element>,
408  textbox_id    : &NodeId,
409  action_buffer : &mut Vec <(NodeId, Action)>,
410  scroll_fun    : fn (&mut controller::component::Scroll, u32),
411  count         : u32
412) {
413  use controller::{alignment, offset};
414  let Widget (scroll, text, _) = Textbox::try_get (elements, textbox_id)
415    .unwrap();
416  let Widget (_, _, canvas) =
417    Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
418  let lines           = text.lines().count() as u32;
419  let longest         = text.lines().map (str::len).max().unwrap() as u32;
420  let (body_width, body_height) = canvas.body_wh();
421  let horizontal_max  = longest.saturating_sub (body_width) as i32;
422  let vertical_max    = lines.saturating_sub (body_height)  as i32;
423  let new_scroll      = {
424    let mut scroll = scroll.clone();
425    scroll_fun (&mut scroll, count);
426    match &mut scroll.offset.horizontal {
427      offset::Signed::Absolute (horizontal) => {
428        match scroll.alignment.horizontal {
429          alignment::Horizontal::Left | alignment::Horizontal::Right => {
430            *horizontal = (*horizontal).max (0);
431            *horizontal = (*horizontal).min (horizontal_max);
432          }
433          alignment::Horizontal::Center => {
434            *horizontal = (*horizontal)
435              .min (horizontal_max / 2 + (body_width+1) as i32 % 2)
436              .max (-horizontal_max / 2);
437          }
438        }
439      }
440      offset::Signed::Relative (_) => {} // TODO: scroll relative ?
441    }
442    match &mut scroll.offset.vertical {
443      offset::Signed::Absolute (vertical) => {
444        match scroll.alignment.vertical {
445          alignment::Vertical::Top | alignment::Vertical::Bottom => {
446            *vertical   = (*vertical).max (0);
447            *vertical   = (*vertical).min (vertical_max);
448          }
449          alignment::Vertical::Middle => {
450            if vertical_max == 0 {
451              *vertical = 0;
452            } else {
453              *vertical   = (*vertical)
454                .min (vertical_max / 2 + body_height as i32 % 2)
455                .max (-vertical_max / 2);
456            }
457          }
458        }
459      }
460      offset::Signed::Relative (_) => {} // TODO: scroll relative ?
461    }
462    scroll
463  };
464  if &new_scroll != scroll {
465    let new_body = body (canvas, &new_scroll, text);
466    { // update scroll
467      let update_scroll = Box::new (move |controller : &mut Controller|
468        controller.component = new_scroll.into());
469      action_buffer.push (
470        (textbox_id.clone(), Action::ModifyController (update_scroll)));
471    }
472    { // update body
473      let update_body = Box::new (move |view : &mut View|
474        view.component = new_body.into());
475      action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
476    }
477  }
478}
479
480//
481//  builder
482//
483
484mod builder {
485  use derive_builder::Builder;
486  use crate::prelude::*;
487  /*
488  use crate::Application;
489  use crate::interface::{controller, view, Action, Controller, Element, Model};
490  use crate::interface::widget::{self, set_option, BuildActions, BuildElement,
491    Widget};
492  use crate::tree::{Tree, NodeId};
493  */
494
495  #[derive(Builder)]
496  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
497  struct Textbox <'a, A : Application> {
498    elements          : &'a Tree <Element>,
499    parent_id         : &'a NodeId,
500    #[builder(default)]
501    appearances       : Appearances,
502    #[builder(default)]
503    bindings          : Option <&'a Bindings <A>>,
504    #[builder(default)]
505    frame_appearances : Appearances,
506    #[builder(default)]
507    frame_border      : Option <Border>,
508    #[builder(default)]
509    frame_clear_color : Option <canvas::ClearColor>,
510    #[builder(default)]
511    frame_disabled    : bool,
512    #[builder(default)]
513    frame_layout      : Layout,
514    #[builder(default)]
515    text              : String,
516    #[builder(default)]
517    scroll            : Option <Scroll>
518  }
519
520  #[derive(Builder)]
521  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
522  struct Fit <'a, A : Application> {
523    elements          : &'a Tree <Element>,
524    parent_id         : &'a NodeId,
525    #[builder(default)]
526    appearances       : Appearances,
527    #[builder(default)]
528    bindings          : Option <&'a Bindings <A>>,
529    #[builder(default)]
530    frame_anchor      : Alignment,
531    #[builder(default)]
532    frame_appearances : Appearances,
533    #[builder(default)]
534    frame_area        : Area,
535    #[builder(default)]
536    frame_border      : Option <Border>,
537    #[builder(default)]
538    frame_clear_color : Option <canvas::ClearColor>,
539    #[builder(default)]
540    frame_disabled    : bool,
541    #[builder(default)]
542    frame_offset      : Offset,
543    #[builder(default)]
544    scroll            : Option <Scroll>,
545    #[builder(default)]
546    text              : String
547  }
548
549  #[derive(Builder)]
550  #[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
551  struct WithFrame <'a, A : Application> {
552    elements     : &'a Tree <Element>,
553    frame_id     : &'a NodeId,
554    #[builder(default)]
555    appearances  : Appearances,
556    #[builder(default)]
557    bindings     : Option <&'a Bindings <A>>,
558    #[builder(default)]
559    scroll       : Option <Scroll>,
560    #[builder(default)]
561    text         : String
562  }
563
564  impl <'a, A : Application> TextboxBuilder <'a, A> {
565    pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
566      TextboxBuilder {
567        elements:          Some (elements),
568        parent_id:         Some (parent_id),
569        appearances:       None,
570        bindings:          None,
571        frame_appearances: None,
572        frame_border:      None,
573        frame_clear_color: None,
574        frame_disabled:    None,
575        frame_layout:      None,
576        scroll:            None,
577        text:              None
578      }
579    }
580  }
581
582  impl <A : Application> BuildActions for TextboxBuilder <'_, A> {
583    fn build_actions (self) -> Vec<(NodeId, Action)> {
584      use std::convert::TryInto;
585      use crate::tree::{InsertBehavior, Node};
586      use view::coordinates;
587      log::trace!("build actions...");
588      let Textbox {
589        elements, parent_id, appearances, bindings, frame_appearances,
590        frame_border, frame_clear_color, frame_disabled, frame_layout, text,
591        scroll
592      } = self.build()
593        .map_err(|err| log::error!("frame builder error: {err:?}"))
594        .unwrap();
595      let bindings_empty = Bindings::empty();
596      let bindings = bindings.unwrap_or (&bindings_empty);
597      { // only allow tile coordinate parents
598        let Widget (_, _, canvas) = Frame::try_get (elements, parent_id)
599          .unwrap();
600        assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
601      }
602      let mut out = vec![];
603      let (mut subtree, order) = {
604        let mut actions = {
605          let mut frame = frame::Builder::new (elements, parent_id)
606            .appearances (frame_appearances)
607            .bindings (bindings)
608            .disabled (frame_disabled)
609            .layout (frame_layout);
610          set_option!(frame, border, frame_border);
611          set_option!(frame, clear_color, frame_clear_color);
612          frame.build_actions()
613        };
614        out.extend (actions.drain (1..));
615        debug_assert_eq!(actions.len(), 1);
616        actions.pop().unwrap().1.try_into().unwrap()
617      };
618      let frame_id = subtree.root_node_id().unwrap().clone();
619      let textbox = {
620        let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id)
621          .unwrap();
622        let scroll     = scroll.unwrap_or (Scroll::default_tile_absolute());
623        let body       = super::body (canvas, &scroll, &text);
624        let controller = {
625          let mut controller = Controller::with_bindings (
626            &bindings.get_bindings (&super::CONTROLS));
627          controller.appearances = appearances;
628          controller.component   = scroll.into();
629          controller
630        };
631        let model = Model { component: text.into(), .. Model::default() };
632        let view = view::Component::from (body).into();
633        Element::new ("Textbox".to_string(), controller, model, view)
634      };
635      let _ = subtree
636        .insert (Node::new (textbox), InsertBehavior::UnderNode (&frame_id))
637        .unwrap();
638      out.push ((parent_id.clone(), Action::Create (subtree, order)));
639      log::trace!("...build actions");
640      out
641    }
642  }
643
644  impl <'a, A : Application> FitBuilder <'a, A> {
645    pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
646      FitBuilder {
647        elements:          Some (elements),
648        parent_id:         Some (parent_id),
649        appearances:       None,
650        bindings:          None,
651        frame_anchor:      None,
652        frame_appearances: None,
653        frame_area:        None,
654        frame_border:      None,
655        frame_clear_color: None,
656        frame_disabled:    None,
657        frame_offset:      None,
658        text:              None,
659        scroll:            None
660      }
661    }
662  }
663
664  impl <A : Application> BuildActions for FitBuilder <'_, A> {
665    /// Create a parent frame with body size determined by the given text
666    fn build_actions (self) -> Vec<(NodeId, Action)> {
667      use std::convert::TryInto;
668      use crate::tree::InsertBehavior;
669      log::trace!("fit build actions...");
670      let Fit {
671        elements, parent_id, appearances, bindings, frame_anchor,
672        frame_appearances, frame_area, frame_border, frame_clear_color,
673        frame_disabled, frame_offset, scroll, text
674      } = self.build()
675        .map_err (|err| log::error!("frame builder error: {err:?}"))
676        .unwrap();
677      let bindings_empty = Bindings::empty();
678      let bindings = bindings.unwrap_or (&bindings_empty);
679      { // only allow tile coordinate parents
680        let Widget (_, _, canvas) = Frame::try_get (elements, parent_id).unwrap();
681        assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
682      }
683      let mut out = vec![];
684      let (mut subtree, order) = {
685        let size   = {
686          let longest = text.lines().map (str::len).max().unwrap() as u32;
687          let lines   = text.lines().count() as u32;
688          let (border_w, border_h) = frame_border.as_ref()
689            .map_or ((0, 0), Border::total_wh);
690          let width   = (longest + border_w as u32).into();
691          let height  = (lines   + border_h as u32).into();
692          Size { width, height }
693        };
694        let layout = layout::Variant::from ((
695          layout::Free { anchor: frame_anchor, offset: frame_offset, size },
696          frame_area
697        ));
698        let mut actions = {
699          let mut frame = frame::Builder::new (elements, parent_id)
700            .appearances (frame_appearances)
701            .bindings (bindings)
702            .disabled (frame_disabled)
703            .layout (layout.into());
704          set_option!(frame, border, frame_border);
705          set_option!(frame, clear_color, frame_clear_color);
706          frame.build_actions()
707        };
708        out.extend (actions.drain (1..));
709        debug_assert_eq!(actions.len(), 1);
710        actions.pop().unwrap().1.try_into().unwrap()
711      };
712      let frame_id = subtree.root_node_id().unwrap().clone();
713      let textbox = {
714        let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id)
715          .unwrap();
716        let scroll = scroll.unwrap_or (Scroll::default_tile_absolute());
717        let body   = super::body (canvas, &scroll, &text);
718        let controller = {
719          let mut controller     = Controller::with_bindings (bindings);
720          controller.appearances = appearances;
721          controller.component   = scroll.into();
722          controller
723        };
724        let model  = Model {
725          component: text.into(), .. Model::default()
726        };
727        let view   = view::Component::from (body).into();
728        Element::new ("Textbox".to_string(), controller, model, view)
729      };
730      let _ = subtree
731        .insert (Node::new (textbox), InsertBehavior::UnderNode (&frame_id))
732        .unwrap();
733      out.push ((parent_id.clone(), Action::Create (subtree, order)));
734      log::trace!("...fit build actions");
735      out
736    }
737  }
738
739  impl <'a, A : Application> WithFrameBuilder <'a, A> {
740    pub const fn new (elements : &'a Tree <Element>, frame_id : &'a NodeId) -> Self {
741      WithFrameBuilder {
742        elements:    Some (elements),
743        frame_id:    Some (frame_id),
744        bindings:    None,
745        appearances: None,
746        text:        None,
747        scroll:      None
748      }
749    }
750  }
751
752  impl <A : Application> BuildElement for WithFrameBuilder <'_, A> {
753    /// Build Textbox element targeting an existing Frame node; Frame must have
754    /// tile coordinates.
755    fn build_element (self) -> Element {
756      use view::coordinates;
757      log::trace!("with frame build element...");
758      let WithFrame {
759        elements, frame_id, bindings, appearances, text, scroll
760      } = self.build()
761        .map_err(|err| log::error!("frame builder error: {err:?}")).unwrap();
762      let bindings_empty = Bindings::empty();
763      let bindings = bindings.unwrap_or (&bindings_empty)
764        .get_bindings (&super::CONTROLS);
765      let Widget (_, _, canvas) = Frame::try_get (elements, frame_id)
766        .unwrap();
767      assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
768      let scroll = scroll.unwrap_or (Scroll::default_tile_absolute());
769      let body   = super::body (canvas, &scroll, &text);
770      let controller = {
771        let mut controller     = Controller::with_bindings (&bindings);
772        controller.appearances = appearances;
773        controller.component   = scroll.into();
774        controller
775      };
776      let model = Model { component: text.into(), .. Model::default() };
777      let view  = view::Component::from (body).into();
778      log::trace!("...with frame build element");
779      Element::new ("Textbox".to_string(), controller, model, view)
780    }
781  }
782}