quvyta-framework 0.1.35

A Rust framework for building terminal applications
//! A `Command::focus` for a widget that is not on screen yet: the request waits for the frame that
//! shows the widget, and a focus the person or a widget moves in the meantime ends the wait.

use std::time::Duration;

use crate::env::Env;
use crate::keymap::{KeyChord, Scope};
use crate::runtime::{App, Command, Harness};
use crate::widget::View;
use crate::widgets::TextInput;

/// The fields only a command can name, which the window draws when its loading ends.
const ARRIVING: [&str; 2] = ["late", "later"];

/// A window with two fields from the start and two that arrive later: a form whose fields come when
/// its loading ends, a terminal whose container takes a moment to start.
struct Late {
    /// Every field the window is made of and what has been typed into it, drawn or not.
    fields: Vec<(&'static str, String)>,
    /// Whether the fields that arrive have come.
    arrived: bool,
}

impl Default for Late {
    fn default() -> Self {
        let fields = ["other", "second"].into_iter().map(|name| (name, String::new())).collect();
        Self { fields, arrived: false }
    }
}

impl Late {
    /// The text of the field named `name`, drawn or not.
    fn text(&self, name: &str) -> Option<&str> {
        self.fields.iter().find(|(known, _)| *known == name).map(|(_, value)| value.as_str())
    }
}

#[derive(Clone)]
enum Msg {
    /// A key asked for a field by name, whether it is drawn or not.
    Ask(&'static str),
    /// The fields the window was waiting for have come.
    Arrive,
    /// A field's text changed.
    Typed(&'static str, String),
}

impl App for Late {
    type Msg = Msg;

    fn update(&mut self, msg: Msg) -> Command<Msg> {
        match msg {
            Msg::Ask(name) => Command::focus(name),
            Msg::Arrive => {
                self.arrived = true;
                for name in ARRIVING {
                    if !self.fields.iter().any(|(known, _)| *known == name) {
                        self.fields.push((name, String::new()));
                    }
                }
                Command::none()
            }
            Msg::Typed(name, text) => {
                if let Some(field) = self.fields.iter_mut().find(|(known, _)| *known == name) {
                    field.1 = text;
                }
                Command::none()
            }
        }
    }

    fn action(&self, name: &str) -> Option<Msg> {
        Some(match name {
            "ask-late" => Msg::Ask("late"),
            "ask-later" => Msg::Ask("later"),
            "arrive" => Msg::Arrive,
            _ => return None,
        })
    }

    fn view(&self, ui: &mut View<'_, Msg>) {
        for (name, value) in &self.fields {
            let name = *name;
            if !self.arrived && ARRIVING.contains(&name) {
                continue;
            }
            ui.add(TextInput::new(value).placeholder(name).on_change(move |text| Msg::Typed(name, text))).id(name);
        }
    }
}

fn chord(text: &str) -> KeyChord {
    text.parse().expect("valid chord")
}

/// The window with the keyboard on the field drawn first, which is where the first Tab puts it, and
/// the keys that ask for a field by name and bring the fields on.
fn window() -> Harness<Late> {
    let mut env = Env::builtin();
    for (text, action) in [("f6", "ask-late"), ("f7", "ask-later"), ("f8", "arrive")] {
        env.keymap_mut().bind(Scope::App, action, &[chord(text)]);
    }
    let mut h = Harness::with_env(Late::default(), env, 40, 8);
    h.press("tab");
    assert!(h.is_focused("other"), "the keyboard starts in the field that is drawn:\n{}", h.screen());
    h
}

/// Frames go by with nothing asked of them, as while a container starts or a list loads.
fn frames_passing(h: &mut Harness<Late>) {
    for _ in 0..3 {
        h.advance(Duration::from_millis(500));
    }
}

#[test]
fn a_focus_asked_before_a_field_is_drawn_reaches_it_when_it_arrives() {
    let mut h = window();
    h.press("f6");
    frames_passing(&mut h);
    assert!(!h.is_focused("late"), "the field is not on screen yet:\n{}", h.screen());
    h.press("f8");
    h.type_text("abc");
    assert!(h.is_focused("late"), "the keys went to the field the command named:\n{}", h.screen());
    assert_eq!(h.app().text("late"), Some("abc"));
}

#[test]
fn a_press_while_a_focus_waits_ends_the_wait() {
    let mut h = window();
    h.press("f6");
    h.click_text("second");
    frames_passing(&mut h);
    h.press("f8");
    h.type_text("abc");
    assert!(h.is_focused("second"), "the press decided where the keyboard goes:\n{}", h.screen());
    assert_eq!(h.app().text("second"), Some("abc"));
    assert!(!h.is_focused("late"), "the field that arrived later took nothing:\n{}", h.screen());
}

#[test]
fn tab_while_a_focus_waits_ends_the_wait() {
    let mut h = window();
    h.press("f6");
    h.press("tab");
    frames_passing(&mut h);
    h.press("f8");
    h.type_text("abc");
    assert!(h.is_focused("second"), "the keys went where Tab put them:\n{}", h.screen());
    assert_eq!(h.app().text("second"), Some("abc"));
}

#[test]
fn a_later_request_replaces_the_one_still_waiting() {
    let mut h = window();
    h.press("f6");
    h.press("f7");
    frames_passing(&mut h);
    h.press("f8");
    h.type_text("abc");
    assert!(h.is_focused("later"), "the last request is the one that waited:\n{}", h.screen());
    assert_eq!(h.app().text("later"), Some("abc"));
    assert!(!h.is_focused("late"), "the request it replaced took nothing:\n{}", h.screen());
}