use std::sync::LazyLock;
use crate::{Application, Presentation, TreeHelper};
use crate::tree::{Tree, NodeId};
use crate::interface::{
model, view, Action, Controller, Element, Interface, Model, View
};
use crate::interface::controller::{self, controls, Controls};
use super::{Widget, Frame, build_and_get_node_ids, frame, set_option};
mod make_table;
pub use self::builder::{TextboxBuilder as Builder, FitBuilder, WithFrameBuilder};
pub use self::make_table::{box_vec, make_table, mk_table};
pub type Textbox <'element> =
Widget <'element, controller::component::Scroll, String, view::component::Body>;
pub static CONTROLS : LazyLock <Controls> = LazyLock::new (||
controls::Builder::new()
.buttons (vec![
controls::button::Builtin::TextboxScrollDown,
controls::button::Builtin::TextboxScrollUp,
controls::button::Builtin::TextboxScrollRight,
controls::button::Builtin::TextboxScrollLeft,
controls::button::Builtin::TextboxPageDown,
controls::button::Builtin::TextboxPageUp,
controls::button::Builtin::TextboxPageRight,
controls::button::Builtin::TextboxPageLeft
].into_iter().map (Into::into).collect::<Vec <_>>().into())
.build()
);
pub fn scroll_right (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("scroll_right...");
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_right, 1);
log::trace!("...scroll_right");
}
pub fn scroll_left (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("scroll_left...");
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_left, 1);
log::trace!("...scroll_left");
}
pub fn scroll_up (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("scroll_up...");
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_up, 1);
log::trace!("...scroll_up");
}
pub fn scroll_down (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("scroll_down...");
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_down, 1);
log::trace!("...scroll_down");
}
pub fn page_right (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("page_right...");
let (width, _) = {
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
canvas.body_wh()
};
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_right, width-1);
log::trace!("...page_right");
}
pub fn page_left (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("page_left...");
let (width, _) = {
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
canvas.body_wh()
};
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_left, width-1);
log::trace!("...page_left");
}
pub fn page_up (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("page_up...");
let (_, height) = {
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
canvas.body_wh()
};
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_up, height-1);
log::trace!("...page_up");
}
pub fn page_down (
_ : &controls::button::Release,
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
log::trace!("page_down...");
let (_, height) = {
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
canvas.body_wh()
};
scroll_helper (elements, textbox_id, action_buffer,
controller::component::Scroll::offset_move_down, height-1);
log::trace!("...page_down");
}
pub fn popup <A, P> (
interface : &mut Interface <A, P>,
parent_id : NodeId,
text : String,
appearances : Option <controller::Appearances>,
border : Option <view::Border>
) -> [NodeId; 2] where
A : Application,
P : Presentation
{
let mut builder = FitBuilder::<A>::new (interface.elements(), &parent_id)
.frame_anchor (controller::Alignment::middle_center())
.text (text);
set_option!(builder, frame_appearances, appearances);
set_option!(builder, frame_border, border);
let [frame_id, textbox_id] =
build_and_get_node_ids!(interface, builder, ["Frame", "Textbox"]);
let _ = interface.action (&frame_id, Action::Focus);
[frame_id, textbox_id]
}
pub fn get_contents (elements : &Tree <Element>, textbox_id : &NodeId) -> String {
let Widget (_, contents, _) = Textbox::try_get (elements, textbox_id).unwrap();
contents.clone()
}
pub fn set_contents (
elements : &Tree <Element>,
textbox_id : &NodeId,
contents : String,
action_buffer : &mut Vec <(NodeId, Action)>
) {
let new_body = {
let Widget (scroll, _, _) = Textbox::try_get (elements, textbox_id).unwrap();
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
body (canvas, scroll, &contents)
};
{ let update_text = Box::new (|model : &mut Model| model.component = contents.into());
action_buffer.push ((textbox_id.clone(), Action::ModifyModel (update_text)));
}
{ let update_body = Box::new (|view : &mut View| view.component = new_body.into());
action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
}
}
pub fn fit_to_contents (
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>
) {
use controller::component::layout;
let (width, height) = {
let contents = get_contents (elements, textbox_id);
let lines = contents.lines().map (str::len).collect::<Vec <_>>();
let height = lines.len() as u32;
let width = (*lines.iter().max().unwrap_or (&0)) as u32;
(width, height)
};
let frame_id = elements.get_parent_id (textbox_id);
let mut layout = {
let Widget (layout, _, _) = Frame::try_get (elements, frame_id).unwrap();
layout.clone()
};
match layout.variant {
layout::Variant::Free (ref mut free, _) => {
let old_size = free.size;
free.size.width = width.into();
free.size.height = height.into();
if old_size != free.size {
frame::set_layout (elements, frame_id, layout, None, action_buffer);
}
}
layout::Variant::Tiled (_) =>
log::warn!("fit to contents called on tiled textbox frame")
}
}
pub fn push_line (
elements : &Tree <Element>,
textbox_id : &NodeId,
line : String,
action_buffer : &mut Vec <(NodeId, Action)>
) {
let new_body = {
let Widget (scroll, contents, _) = Textbox::try_get (elements, textbox_id).unwrap();
let mut contents = contents.clone();
contents.push ('\n');
contents.push_str (&line);
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
body (canvas, scroll, &contents)
};
{ let update_text = Box::new (move |model : &mut Model| match model.component {
model::Component::Text (ref mut text) => {
text.push ('\n');
text.push_str (&line);
}
_ => unreachable!()
});
action_buffer.push ((textbox_id.clone(), Action::ModifyModel (update_text)));
}
{ let update_body = Box::new (|view : &mut View| view.component = new_body.into());
action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
}
}
pub (crate) fn body (
canvas : &view::component::Canvas,
scroll : &controller::component::Scroll,
text : &str
) -> view::component::Body {
use controller::{alignment, offset};
log::trace!("body...");
let (body_width, body_height) = canvas.body_wh();
let lines = text.lines().count() as u32;
let longest = text.lines().map (str::len).max().unwrap_or_default() as u32;
let scroll_horizontal = match scroll.offset.horizontal {
offset::Signed::Absolute (horizontal) => horizontal,
offset::Signed::Relative (horizontal) => {
let horizontal_max = longest.saturating_sub (body_width) as f32;
(*horizontal * horizontal_max).round() as i32
}
};
let scroll_vertical = match scroll.offset.vertical {
offset::Signed::Absolute (vertical) => vertical,
offset::Signed::Relative (vertical) => {
let vertical_max = lines.saturating_sub (body_height) as f32;
(*vertical * vertical_max).round() as i32
}
};
let mut body = String::new();
let (skip, body_top) = match scroll.alignment.vertical {
alignment::Vertical::Top => (scroll_vertical as usize, 0),
alignment::Vertical::Bottom => {
for _ in 0..body_height.saturating_sub (lines) {
body.push ('\n');
}
( (lines.saturating_sub (body_height) as i32 - scroll_vertical).max (0)
as usize,
0
)
}
alignment::Vertical::Middle => {
let center = lines as i32 / 2;
let body_top = center - body_height as i32 / 2 - scroll_vertical;
for _ in body_top..0 {
body.push ('\n');
}
(body_top.max (0) as usize, body_top)
}
};
let take = (body_height as i32 + body_top.min (0)).max (0) as usize;
for line in text.lines().skip (skip).take (take) {
let len = line.len();
let (space, start, end) = match scroll.alignment.horizontal {
alignment::Horizontal::Left => (
0,
scroll_horizontal as usize,
(scroll_horizontal as usize + body_width as usize).min (len)
),
alignment::Horizontal::Center => {
let center = longest as i32 / 2;
let body_left = center - body_width as i32 / 2 + scroll_horizontal;
let body_right = center + body_width as i32 / 2 + scroll_horizontal;
let line_start = center - len as i32 / 2;
let line_end = center + len as i32 / 2 - (len as i32 + 1) % 2;
let space = (line_start - body_left).max (0) as usize;
let start = (body_left - line_start).max (0) as usize;
let end = len - (line_end - body_right).max (0) as usize;
(space, start, end)
}
alignment::Horizontal::Right =>
( (body_width as usize + scroll_horizontal as usize).saturating_sub (len),
len.saturating_sub (body_width as usize)
.saturating_sub (scroll_horizontal as usize),
len
)
};
if start < end {
for _ in 0..space {
body.push ('\0');
}
for ch in line.chars().skip (start).take (end - start) {
body.push (ch);
}
body.push ('\n');
} else if start == end {
body.push ('\n');
}
}
log::trace!("...body");
view::component::Body (body)
}
fn scroll_helper (
elements : &Tree <Element>,
textbox_id : &NodeId,
action_buffer : &mut Vec <(NodeId, Action)>,
scroll_fun : fn (&mut controller::component::Scroll, u32),
count : u32
) {
use controller::{alignment, offset};
let Widget (scroll, text, _) = Textbox::try_get (elements, textbox_id).unwrap();
let Widget (_, _, canvas) =
Frame::try_get (elements, elements.get_parent_id (textbox_id)).unwrap();
let lines = text.lines().count() as u32;
let longest = text.lines().map (str::len).max().unwrap() as u32;
let (body_width, body_height) = canvas.body_wh();
let horizontal_max = longest.saturating_sub (body_width) as i32;
let vertical_max = lines.saturating_sub (body_height) as i32;
let new_scroll = {
let mut scroll = scroll.clone();
scroll_fun (&mut scroll, count);
match &mut scroll.offset.horizontal {
offset::Signed::Absolute (horizontal) => {
match scroll.alignment.horizontal {
alignment::Horizontal::Left | alignment::Horizontal::Right => {
*horizontal = (*horizontal).max (0);
*horizontal = (*horizontal).min (horizontal_max);
}
alignment::Horizontal::Center => {
*horizontal = (*horizontal)
.min (horizontal_max / 2 + (body_width+1) as i32 % 2)
.max (-horizontal_max / 2);
}
}
}
offset::Signed::Relative (_) => {} }
match &mut scroll.offset.vertical {
offset::Signed::Absolute (vertical) => {
match scroll.alignment.vertical {
alignment::Vertical::Top | alignment::Vertical::Bottom => {
*vertical = (*vertical).max (0);
*vertical = (*vertical).min (vertical_max);
}
alignment::Vertical::Middle => {
if vertical_max == 0 {
*vertical = 0;
} else {
*vertical = (*vertical)
.min (vertical_max / 2 + body_height as i32 % 2)
.max (-vertical_max / 2);
}
}
}
}
offset::Signed::Relative (_) => {} }
scroll
};
if &new_scroll != scroll {
let new_body = body (canvas, &new_scroll, text);
{ let update_scroll = Box::new (move |controller : &mut Controller|
controller.component = new_scroll.into());
action_buffer.push (
(textbox_id.clone(), Action::ModifyController (update_scroll)));
}
{ let update_body = Box::new (move |view : &mut View|
view.component = new_body.into());
action_buffer.push ((textbox_id.clone(), Action::ModifyView (update_body)));
}
}
}
mod builder {
use derive_builder::Builder;
use crate::prelude::*;
#[derive(Builder)]
#[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
struct Textbox <'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_appearances : Appearances,
#[builder(default)]
frame_border : Option <Border>,
#[builder(default)]
frame_clear_color : Option <canvas::ClearColor>,
#[builder(default)]
frame_disabled : bool,
#[builder(default)]
frame_layout : Layout,
#[builder(default)]
name : Option <String>,
#[builder(default)]
scroll : Option <Scroll>,
#[builder(default)]
text : String
}
#[derive(Builder)]
#[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
struct Fit <'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_border : Option <Border>,
#[builder(default)]
frame_clear_color : Option <canvas::ClearColor>,
#[builder(default)]
frame_disabled : bool,
#[builder(default)]
frame_offset : Offset,
#[builder(default)]
name : Option <String>,
#[builder(default)]
scroll : Option <Scroll>,
#[builder(default)]
text : String
}
#[derive(Builder)]
#[builder(public, pattern="owned", build_fn(private), setter(strip_option))]
struct WithFrame <'a, A : Application> {
elements : &'a Tree <Element>,
frame_id : &'a NodeId,
#[builder(default)]
appearances : Appearances,
#[builder(default)]
bindings : Option <&'a Bindings <A>>,
#[builder(default)]
name : Option <String>,
#[builder(default)]
scroll : Option <Scroll>,
#[builder(default)]
text : String
}
impl <'a, A : Application> TextboxBuilder <'a, A> {
pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
TextboxBuilder {
elements: Some (elements),
parent_id: Some (parent_id),
appearances: None,
bindings: None,
frame_appearances: None,
frame_border: None,
frame_clear_color: None,
frame_disabled: None,
frame_layout: None,
name: None,
scroll: None,
text: None
}
}
}
impl <A : Application> BuildActions for TextboxBuilder <'_, A> {
fn build_actions (self) -> Vec<(NodeId, Action)> {
use std::convert::TryInto;
use crate::tree::{InsertBehavior, Node};
use view::coordinates;
log::trace!("build actions...");
let Textbox {
elements, parent_id, appearances, bindings, frame_appearances, frame_border,
frame_clear_color, frame_disabled, frame_layout, name, scroll, text
} = self.build()
.map_err(|err| log::error!("frame builder error: {err:?}"))
.unwrap();
let bindings_empty = Bindings::empty();
let bindings = bindings.unwrap_or (&bindings_empty);
{ 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 mut actions = {
let mut frame = frame::Builder::new (elements, parent_id)
.appearances (frame_appearances)
.bindings (bindings)
.disabled (frame_disabled)
.layout (frame_layout);
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 textbox = {
let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id).unwrap();
let scroll = scroll.unwrap_or (Scroll::default_tile_absolute());
let body = super::body (canvas, &scroll, &text);
let controller = {
let mut controller =
Controller::with_bindings (&bindings.get_bindings (&super::CONTROLS));
controller.appearances = appearances;
controller.component = scroll.into();
controller
};
let model = Model { component: text.into(), .. Model::default() };
let view = view::Component::from (body).into();
let name = name.unwrap_or ("Textbox".to_string());
Element::new (name, controller, model, view)
};
let _ = subtree
.insert (Node::new (textbox), InsertBehavior::UnderNode (&frame_id))
.unwrap();
out.push ((parent_id.clone(), Action::Create (subtree, order)));
log::trace!("...build actions");
out
}
}
impl <'a, A : Application> FitBuilder <'a, A> {
pub const fn new (elements : &'a Tree <Element>, parent_id : &'a NodeId) -> Self {
FitBuilder {
elements: Some (elements),
parent_id: Some (parent_id),
appearances: None,
bindings: None,
frame_anchor: None,
frame_appearances: None,
frame_area: None,
frame_border: None,
frame_clear_color: None,
frame_disabled: None,
frame_offset: None,
name: None,
scroll: None,
text: None
}
}
}
impl <A : Application> BuildActions for FitBuilder <'_, A> {
fn build_actions (self) -> Vec<(NodeId, Action)> {
use std::convert::TryInto;
use crate::tree::InsertBehavior;
log::trace!("fit build actions...");
let Fit {
elements, parent_id, appearances, bindings, frame_anchor, frame_appearances,
frame_area, frame_border, frame_clear_color, frame_disabled, frame_offset,
name, scroll, text
} = self.build()
.map_err (|err| log::error!("frame builder error: {err:?}"))
.unwrap();
let bindings_empty = Bindings::empty();
let bindings = bindings.unwrap_or (&bindings_empty);
{ 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 size = {
let longest = text.lines().map (str::len).max().unwrap() as u32;
let lines = text.lines().count() as u32;
let (border_w, border_h) = frame_border.as_ref()
.map_or ((0, 0), Border::total_wh);
let width = (longest + border_w as u32).into();
let height = (lines + border_h as u32).into();
Size { width, height }
};
let layout = layout::Variant::from ((
layout::Free { anchor: frame_anchor, offset: frame_offset, size },
frame_area
));
let mut actions = {
let mut frame = frame::Builder::new (elements, parent_id)
.appearances (frame_appearances)
.bindings (bindings)
.disabled (frame_disabled)
.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 textbox = {
let Widget (_, _, canvas) = Frame::try_get (&subtree, &frame_id).unwrap();
let scroll = scroll.unwrap_or (Scroll::default_tile_absolute());
let body = super::body (canvas, &scroll, &text);
let controller = {
let mut controller = Controller::with_bindings (bindings);
controller.appearances = appearances;
controller.component = scroll.into();
controller
};
let model = Model { component: text.into(), .. Model::default() };
let view = view::Component::from (body).into();
let name = name.unwrap_or ("Textbox".to_string());
Element::new (name, controller, model, view)
};
let _ = subtree
.insert (Node::new (textbox), InsertBehavior::UnderNode (&frame_id))
.unwrap();
out.push ((parent_id.clone(), Action::Create (subtree, order)));
log::trace!("...fit build actions");
out
}
}
impl <'a, A : Application> WithFrameBuilder <'a, A> {
pub const fn new (elements : &'a Tree <Element>, frame_id : &'a NodeId) -> Self {
WithFrameBuilder {
elements: Some (elements),
frame_id: Some (frame_id),
bindings: None,
appearances: None,
name: None,
scroll: None,
text: None
}
}
}
impl <A : Application> BuildElement for WithFrameBuilder <'_, A> {
fn build_element (self) -> Element {
use view::coordinates;
log::trace!("with frame build element...");
let WithFrame { elements, frame_id, bindings, appearances, name, scroll, text } =
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 Widget (_, _, canvas) = Frame::try_get (elements, frame_id).unwrap();
assert!(canvas.coordinates.kind() == coordinates::Kind::Tile);
let scroll = scroll.unwrap_or (Scroll::default_tile_absolute());
let body = super::body (canvas, &scroll, &text);
let controller = {
let mut controller = Controller::with_bindings (&bindings);
controller.appearances = appearances;
controller.component = scroll.into();
controller
};
let model = Model { component: text.into(), .. Model::default() };
let view = view::Component::from (body).into();
let name = name.unwrap_or ("Textbox".to_string());
log::trace!("...with frame build element");
Element::new (name, controller, model, view)
}
}
}