use prism::{event, event::{Event, OnEvent, Key, NamedKey}, display::{Opt, EitherOr}, Context, canvas::Align, layout::{Stack, Row, Padding, Size, Offset, Column}, drawable::{Component, SizedTree}, emitters};
use crate::components::text::{Text, ExpandableText, TextStyle};
use crate::theme::Theme;
use ptsd::{colors, TextSize};
#[derive(Debug, Clone, Component)]
pub struct NumericalInput(Stack, _NumericalInput);
impl OnEvent for NumericalInput {}
impl NumericalInput {
pub fn numerical(theme: &Theme, instructions: &str) -> Self {
NumericalInput::new(theme, instructions, SlotDisplay::numerical(theme))
}
pub fn date(theme: &Theme, instructions: &str) -> Self {
NumericalInput::new(theme, instructions, SlotDisplay::date(theme))
}
pub fn time(theme: &Theme, instructions: &str) -> Self {
NumericalInput::new(theme, instructions, SlotDisplay::time(theme))
}
pub fn display(theme: &Theme, amount: f32, instructions: &str) -> Self {
NumericalInput::new(theme, instructions, SlotDisplay::display(theme, amount))
}
pub fn value(&self) -> String {
let mut out = String::new();
for slot in &self.1.1.1.1 {
let s = match slot.2.get_visual() {
SlotVisual::Primary(s) | SlotVisual::Ghost(s) => s,
SlotVisual::None => continue,
};
for ch in s.chars() {
match ch.is_ascii_digit() || matches!(ch, '$' | '.' | ',' | '-' | ':' | '/') {
true => out.push(ch),
false => out.push('0'),
}
}
}
out
}
fn new(theme: &Theme, instructions: &str, input: SlotDisplay) -> Self {
let input = _NumericalInput::new(theme, instructions, input);
let layout = Stack(Offset::Center, Offset::Center, Size::Fill, Size::Fill, Padding::default());
NumericalInput(layout, input)
}
}
#[derive(Clone, Debug, Component)]
pub struct _NumericalInput(Column, emitters::TextInput<SlotDisplay>, ExpandableText);
impl OnEvent for _NumericalInput {}
impl _NumericalInput {
fn new(theme: &Theme, instructions: &str, input: SlotDisplay) -> Self {
let layout = Column::new(24.0, Offset::Center, Size::Fit, Padding::new(64.0), None);
let text = ExpandableText::new(theme, instructions, TextSize::Lg, TextStyle::Secondary, Align::Center, None);
_NumericalInput(layout, emitters::TextInput::new(input, false), text)
}
}
#[derive(Clone, Debug, Component)]
pub struct SlotDisplay(Row, Vec<Slot>, #[skip] bool);
impl SlotDisplay {
pub fn numerical(theme: &Theme) -> Self {
let slots = vec![
Slot::new(theme, SlotType::Fixed('$')),
Slot::new(theme, SlotType::InputWithDefault(String::new(), 6, '0', InputFormat::Numerical)),
Slot::new(theme, SlotType::TriggersGhost('.', false)),
Slot::new(theme, SlotType::TriggeredGhostInputWithDefault(String::new(), 1, '0', false)),
Slot::new(theme, SlotType::TriggeredGhostInputWithDefault(String::new(), 1, '0', false)),
];
SlotDisplay(Row::center(0.0), slots, true)
}
pub fn display(theme: &Theme, amount: f32) -> Self {
let chars = format!("{:.2}", amount).chars().collect::<Vec<char>>();
let mut slots = vec![Slot::new(theme, SlotType::Fixed('$'))];
chars.into_iter().for_each(|c| slots.push(Slot::new(theme, SlotType::Fixed(c))));
SlotDisplay(Row::center(0.0), slots, false)
}
pub fn date(theme: &Theme) -> Self {
let slots = vec![
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, 'D')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, 'D')),
Slot::new(theme, SlotType::Fixed('-')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, 'M')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, 'M')),
];
SlotDisplay(Row::center(0.0), slots, true)
}
pub fn time(theme: &Theme) -> Self {
let slots = vec![
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, '0')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, '0')),
Slot::new(theme, SlotType::Fixed(':')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, '0')),
Slot::new(theme, SlotType::GhostInputWithDefault(String::new(), 1, '0')),
];
SlotDisplay(Row::center(0.0), slots, true)
}
}
impl OnEvent for SlotDisplay {
fn on_event(&mut self, _ctx: &mut Context, _sized: &SizedTree, event: Box<dyn Event>) -> Vec<Box<dyn Event>> {
if let Some(e) = event.downcast_ref::<event::TextInput>() && self.2 && let event::TextInput::Edited(key) = e {
let mut reversed = false;
let mut slots: Vec<Slot> = vec![];
let mut start = 0;
match key {
Key::Named(NamedKey::Delete) => {
reversed = true;
slots = self.1.clone().into_iter().rev().collect::<Vec<_>>();
},
_ => {
slots = self.1.clone();
for (i, slot) in slots.iter_mut().enumerate() {
if let SlotType::TriggersGhost(_, is_on) = slot.2 && is_on {
start = i;
}
}
}
}
for (i, slot) in slots.iter_mut().enumerate() {
if start != 0 && i < start {continue;}
let mut edited = false;
match key {
Key::Named(NamedKey::Delete) => {
match &mut slot.2 {
SlotType::TriggersGhost(_, is_on) if *is_on => {
*is_on = false;
edited = true;
},
SlotType::InputWithDefault(inputs, _, _, _) => {if inputs.pop().is_some() { edited = true; }},
SlotType::GhostInputWithDefault(inputs, _, _) => {if inputs.pop().is_some() { edited = true; }},
SlotType::TriggeredGhostInputWithDefault(inputs, _, _, _) => {if inputs.pop().is_some() { edited = true; }}
_ => {} }
},
Key::Character(character) => {
let character = character.chars().next().unwrap();
if matches!(character, '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') {
match &mut slot.2 {
SlotType::InputWithDefault(inputs, limit, default, _) => {
if inputs.len() < *limit && (!inputs.is_empty() || *default != character) {
inputs.push(character);
edited = true;
}
},
SlotType::GhostInputWithDefault(inputs, limit, _) => {
if inputs.len() < *limit {
inputs.push(character);
edited = true;
}
},
SlotType::TriggeredGhostInputWithDefault(inputs, limit, default, is_on) if *is_on => {
if inputs.len() < *limit && (!inputs.is_empty() || *default != character) {
inputs.push(character);
edited = true;
}
},
_ => {} }
} else if let SlotType::TriggersGhost(trigger, is_on) = &mut slot.2 && *trigger == character {
*is_on = true;
edited = true;
}
},
_ => {} }
if edited {
match slot.2.get_visual() {
SlotVisual::Primary(v) => {
slot.1.inner().left().spans[0] = v;
slot.1.inner().display_left(true);
slot.1.display(true);
},
SlotVisual::Ghost(v) => {
slot.1.inner().right().spans[0] = v;
slot.1.inner().display_left(false);
slot.1.display(true);
},
SlotVisual::None => slot.1.display(false),
}
}
if edited {break;}
}
if reversed {slots = slots.into_iter().rev().collect::<Vec<_>>();}
self.1 = slots;
let mut triggered = false;
for slot in &mut self.1 {
if let SlotType::TriggersGhost(_, is_on) = slot.2 {
triggered = is_on;
} else if let SlotType::TriggeredGhostInputWithDefault(_, _, _, is_on) = &mut slot.2 {
*is_on = triggered;
match slot.2.get_visual() {
SlotVisual::Primary(v) => {
slot.1.inner().left().spans[0] = v;
slot.1.inner().display_left(true);
slot.1.display(true);
},
SlotVisual::Ghost(v) => {
slot.1.inner().right().spans[0] = v;
slot.1.inner().display_left(false);
slot.1.display(true);
},
SlotVisual::None => {
slot.1.display(false);
}
}
}
}
}
vec![event]
}
}
#[derive(Clone, Debug, Component)]
pub struct Slot(Stack, Opt<EitherOr<Text, Text>>, #[skip] SlotType); impl OnEvent for Slot {}
impl Slot {
pub fn new(theme: &Theme, ty: SlotType) -> Self {
let ghost = theme.colors().get(colors::Text::Secondary);
let (display_left, show_opt, text) = match ty.get_visual() {
SlotVisual::Primary(s) => (true, true, s.to_string()),
SlotVisual::Ghost(s) => (false, true, s.to_string()),
SlotVisual::None => (true, false, String::new()),
};
let mut eo = EitherOr::new(
Text::new(theme, &text, TextSize::H1, TextStyle::Heading, Align::Left, None),
Text::new(theme, &text, TextSize::H1, TextStyle::Label(ghost), Align::Left, None),
);
eo.display_left(display_left);
Slot(Stack::default(), Opt::new(eo, show_opt), ty)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum SlotType {
Fixed(char), TriggersGhost(char, bool), GhostInputWithDefault(String, usize, char), TriggeredGhostInputWithDefault(String, usize, char, bool), ConditionalVisual(char), InputWithDefault(String, usize, char, InputFormat), ConditionalInput(Option<char>), }
impl SlotType {
pub fn get_visual(&self) -> SlotVisual {
match self {
SlotType::Fixed(c) => SlotVisual::Primary(c.to_string()),
SlotType::TriggersGhost(c, i) => match i {
true => SlotVisual::Primary(c.to_string()),
false => SlotVisual::None,
},
SlotType::TriggeredGhostInputWithDefault(chars, _, default, is_on) => {
match is_on {
true => match chars.to_string().is_empty() {
true => SlotVisual::Ghost(default.to_string()),
false => SlotVisual::Primary(chars.to_string()),
},
false => SlotVisual::None,
}
},
SlotType::GhostInputWithDefault(chars, _, default) => {
match chars.to_string().is_empty() {
true => SlotVisual::Ghost(default.to_string()),
false => SlotVisual::Primary(chars.to_string()),
}
},
SlotType::ConditionalVisual(c) => SlotVisual::Primary(c.to_string()), SlotType::InputWithDefault(chars, _, default, format) => {
SlotVisual::Primary(match chars.to_string().is_empty() {
true => default.to_string(),
false => {
match format {
InputFormat::Numerical => chars.chars().rev().collect::<Vec<char>>().chunks(3).map(|c| c.iter().collect::<String>()).collect::<Vec<_>>().join(",").chars().rev().collect::<String>(),
InputFormat::Date => chars.chars().rev().collect::<Vec<char>>().chunks(2).map(|c| c.iter().collect::<String>()).collect::<Vec<_>>().join("/").chars().rev().collect::<String>(),
_ => chars.to_string()
}
}
})
},
SlotType::ConditionalInput(c) => match c {
Some(character) => SlotVisual::Primary(character.to_string()),
None => SlotVisual::None,
},
}
}
}
pub enum SlotVisual {
Ghost(String),
Primary(String),
None,
}
#[derive(Eq, Clone, Debug, PartialEq)]
pub enum InputFormat {
Numerical,
Date,
Time,
}