use pelican_ui::{drawables, colors, Context, Callback, Request, Hardware, IS_MOBILE};
use pelican_ui::drawable::Drawable;
use pelican_ui::canvas::{Align, RgbaImage, ShapeType, Image};
use pelican_ui::theme::{Theme, Icons};
use pelican_ui::layout::Offset;
use pelican_ui::utils::{TitleSubtitle};
use pelican_ui::components::list_item::{ListItemSection, ListItemInfoLeft, ListItem as PelicanListItem};
use pelican_ui::components::{QRCodeScannedEvent, QRCodeScanner, TextInput, RadioSelector, Icon, DataItem, QRCode, NumericalInput};
use pelican_ui::components::text::{ExpandableText, TextStyle, TextSize};
use pelican_ui::components::avatar::{Avatar, AvatarSize};
pub use pelican_ui::components::avatar::{AvatarContent, AvatarIconStyle};
use pelican_ui::components::button::{SecondaryButton, QuickActions};
use pelican_ui::components::{SearchBar, Keypad};
use pelican_ui::navigation::NavigationEvent;
use std::sync::Arc;
use crate::flow::{Flow, State};
#[derive(Debug, Clone, PartialEq)]
pub enum Input {
Text {label: String, actions: Option<Vec<(String, Icons, Action)>>, show_label: bool, preset: Option<String>},
Currency {instructions: String}, Date {instructions: String}, Time {instructions: String}, Enumerator {items: Vec<EnumItem>},
Avatar {content: AvatarContent, flair: Option<(Icons, AvatarIconStyle)>, action: Option<Action>},
Search {items: Vec<ListItem>},
QRCodeScanner,
}
impl Input {
pub fn currency(instructions: &str) -> Self { Input::Currency {instructions: instructions.to_string()} }
pub fn date(instructions: &str) -> Self { Input::Date {instructions: instructions.to_string()} }
pub fn time(instructions: &str) -> Self { Input::Time {instructions: instructions.to_string()} }
pub fn enumerator(items: Vec<EnumItem>) -> Self {
Input::Enumerator {items}
}
pub fn text(label: &str, show_label: bool, preset: Option<String>, actions: Option<Vec<(String, Icons, Action)>>) -> Self {
Input::Text {label: label.to_string(), show_label, preset, actions}
}
pub fn avatar(content: AvatarContent, flair: Option<(Icons, AvatarIconStyle)>, action: Option<Action>) -> Self {
Input::Avatar {content, flair, action}
}
pub fn search(items: Vec<ListItem>) -> Self {
Input::Search {items}
}
pub fn qr_code_scanner() -> Self {
Input::QRCodeScanner
}
pub fn build(&self, theme: &Theme) -> Option<Vec<Box<dyn Drawable>>> {
Some(match self {
Input::Text {show_label, label, preset, actions} => {
let mut items = drawables![TextInput::new(theme, preset.as_deref(), show_label.then_some(label), Some(&format!("Enter {}...", label.to_lowercase())), None, None)];
if let Some(a) = actions.as_ref() {
items.push(Box::new(QuickActions::new(theme, a.iter().map(|(label, icon, action)| {
let action: Box<dyn Callback> = action.get();
(label.to_string(), *icon, action)
}).collect::<Vec<(String, Icons, Box<dyn Callback>)>>())));
}
items
},
Input::Enumerator {items} => drawables![RadioSelector::new(theme, 0, items.iter().map(|item| item.get()).collect::<Vec<_>>())],
Input::Currency {instructions} => {
let mut drawables = drawables![NumericalInput::numerical(theme, instructions)];
if IS_MOBILE {drawables.push(Box::new(Keypad::new(theme))); }
drawables
},
Input::Date {instructions} => drawables![NumericalInput::date(theme, instructions)],
Input::Time {instructions} => drawables![NumericalInput::time(theme, instructions)],
Input::Avatar {content, flair, action} => drawables![Avatar::new(theme, content.clone(), *flair, flair.is_some(), AvatarSize::Xxl, action.as_ref().map(|a| a.get()))],
Input::Search {items} => drawables![SearchBar::new(theme, items.iter().map(|item| item.build(theme)).collect::<Vec<_>>())],
Input::QRCodeScanner => drawables![QRCodeScanner::new(theme, Box::new(|ctx: &mut Context, d: String| {ctx.send(Request::event(NavigationEvent::Pop)); ctx.send(Request::event(QRCodeScannedEvent(d)));}))]
})
}
pub fn offset(&self) -> Offset {
match self {
Input::Text {..} |
Input::Enumerator {..} |
Input::Currency {..} |
Input::Date {..} |
Input::Time {..} |
Input::Avatar {..} |
Input::Search {..} => Offset::Start,
Input::QRCodeScanner => Offset::Center
}
}
#[allow(clippy::borrowed_box)]
pub fn store_in(child: &Box<dyn Drawable>, state: &mut Vec<State>) {
if let Some(input) = child.downcast_ref::<TextInput>() {
state.push(State::Text(input.value()));
} else if let Some(selector) = child.downcast_ref::<RadioSelector>() {
state.push(State::Enumerator(selector.value()));
} else if let Some(input) = child.downcast_ref::<NumericalInput>() {
state.push(State::Number(input.value()));
} else if let Some(avatar) = child.downcast_ref::<Avatar>() {
state.push(State::Avatar(avatar.content.clone()))
} else if let Some(searchbar) = child.downcast_ref::<SearchBar>() {
state.push(State::Search(searchbar.results()))
} else if let Some(scanner) = child.downcast_ref::<QRCodeScanner>() {
state.push(State::ScanCode(scanner.found()))
}
}
}
#[derive(Debug, Clone)]
pub enum Display {
Text {text: String, size: TextSize, style: TextStyle, align: Align},
Icon {icon: Icons},
Image {image: Arc<RgbaImage>, size: (f32, f32)},
Review {label: String, data: String, instructions: String},
Table {label: String, items: Vec<TableItem>},
Currency {amount: f32, instructions: String},
List {label: Option<String>, items: Vec<ListItem>, instructions: Option<String>},
QRCode {data: String, instructions: String},
Avatar {content: AvatarContent},
Actions {actions: Vec<ActionItem>}
}
impl Display {
pub fn text(text: &str) -> Self {
Display::Text {text: text.to_string(), size: TextSize::Md, style: TextStyle::Primary, align: Align::Left}
}
pub fn instructions(text: &str) -> Self {
Display::Text {text: text.to_string(), size: TextSize::Md, style: TextStyle::Secondary, align: Align::Center}
}
pub fn label(text: &str) -> Self {
Display::Text {text: text.to_string(), size: TextSize::H5, style: TextStyle::Heading, align: Align::Left}
}
pub fn icon(icon: Icons) -> Self {
Display::Icon {icon}
}
pub fn image(image: Arc<RgbaImage>, size: (f32, f32)) -> Self {
Display::Image {image, size}
}
pub fn review(label: &str, data: &str, instructions: &str) -> Self {
Display::Review {label: label.to_string(), data: data.to_string(), instructions: instructions.to_string()}
}
pub fn table(label: &str, items: Vec<TableItem>) -> Self {
Display::Table {label: label.to_string(), items}
}
pub fn qr_code(data: &str, instructions: &str) -> Self {
Display::QRCode {data: data.to_string(), instructions: instructions.to_string()}
}
pub fn list(label: Option<&str>, items: Vec<ListItem>, instructions: Option<&str>) -> Self {
Display::List{label: label.map(|i| i.to_string()), items, instructions: instructions.map(|i| i.to_string())}
}
pub fn currency(amount: f32, instructions: &str) -> Self {
Display::Currency {amount, instructions: instructions.to_string()}
}
pub fn avatar(content: AvatarContent) -> Self {
Display::Avatar {content}
}
pub fn actions(actions: Vec<ActionItem>) -> Self {
Display::Actions {actions}
}
pub fn build(&mut self, theme: &Theme) -> Option<Vec<Box<dyn Drawable>>> {
Some(match self {
Display::Icon {icon} => drawables![Icon::new(theme, *icon, Some(theme.colors().get(colors::Text::Heading)), 128.0)],
Display::Image {image, size} => drawables![Image{shape: ShapeType::Rectangle(0.0, *size, 0.0), image: image.clone(), color: None}],
Display::Text {text, size, style, align} if !text.is_empty() => drawables![ExpandableText::new(theme, text, *size, *style, *align, None)],
Display::Review {label, data, instructions} => drawables![DataItem::text(theme, label, data, instructions, Some(Vec::<(String, Icons, Box<dyn Callback>)>::new()))],
Display::Table {label, items} => drawables![DataItem::table(theme, label, items.iter().map(|TableItem{title, data}| (title.clone(), data.clone())).collect(), Some(Vec::<(String, Icons, Box<dyn Callback>)>::new()))],
Display::Currency {amount, instructions} => drawables![NumericalInput::display(theme, *amount, instructions)],
Display::List {items, instructions, ..} if items.is_empty() => drawables![ExpandableText::new(theme, instructions.as_ref()?, TextSize::Md, TextStyle::Secondary, Align::Center, None)],
Display::List {label, items, ..} => drawables![ListItemSection::new(theme, label.clone(), items.iter_mut().map(|item| item.build(theme)).collect::<Vec<_>>())],
Display::QRCode {data, instructions} => drawables![QRCode::new(theme, data), ExpandableText::new(theme, instructions, TextSize::Md, TextStyle::Secondary, Align::Center, None)],
Display::Avatar {content} => drawables![Avatar::new(theme, content.clone(), None, false, AvatarSize::Xxl, None)],
Display::Actions {actions} => actions.iter_mut().map(|ActionItem(a, l, i)| Box::new(SecondaryButton::medium(theme, *i, l, None, a.get())) as Box<dyn Drawable>).collect::<Vec<_>>(),
_ => return None
})
}
}
#[derive(Debug, Clone)]
pub struct ListItem {
avatar: Option<AvatarContent>,
title: String,
subtitle: String,
secondary: Option<String>,
flow: Option<Flow>,
}
impl PartialEq for ListItem {
fn eq(&self, other: &Self) -> bool {
self.avatar == other.avatar &&
self.title == other.title &&
self.subtitle == other.subtitle &&
self.secondary == other.secondary
}
}
impl ListItem {
pub fn plain(title: &str, subtitle: &str, secondary: Option<&str>, flow: Option<Flow>) -> Self {
ListItem {
avatar: None,
title: title.to_string(),
subtitle: subtitle.to_string(),
secondary: secondary.map(|s| s.to_string()),
flow,
}
}
pub fn avatar(avatar: AvatarContent, title: &str, subtitle: &str, secondary: Option<&str>, flow: Option<Flow>) -> Self {
ListItem {
avatar: Some(avatar),
title: title.to_string(),
subtitle: subtitle.to_string(),
secondary: secondary.map(|s| s.to_string()),
flow,
}
}
pub(crate) fn build(&self, theme: &Theme) -> PelicanListItem {
let ListItem {avatar, title, subtitle, secondary, flow} = self.clone();
let has_flow = flow.is_some();
let closure = Box::new(move |ctx: &mut Context, theme: &Theme| {
if let Some(mut f) = flow.clone() {(f.build(ctx))(ctx, theme);}
});
PelicanListItem::new(theme, avatar.clone(),
ListItemInfoLeft::new(&title, Some(&subtitle), None, None),
secondary.as_ref().map(|s| TitleSubtitle::new(s, Some("Details"))),
None, has_flow.then_some(Icons::Forward), closure
)
}
}
#[derive(Debug, Clone)]
pub enum Action {
Share {data: String},
SelectImage,
Custom {action: Box<dyn Callback>},
None,
Flow {flow: Flow},
Paste,
Copy {data: String},
}
impl PartialEq for Action {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Action::Share { data: a }, Action::Share { data: b }) => a == b,
(Action::SelectImage, Action::SelectImage) => true,
(Action::None, Action::None) => true,
(Action::Paste, Action::Paste) => true,
(Action::Copy { data: a }, Action::Copy { data: b }) => a == b,
(Action::Custom { .. }, Action::Custom { .. }) => false,
(Action::Flow { .. }, Action::Flow { .. }) => false,
_ => false,
}
}
}
impl Action {
pub fn share(data: &str) -> Self {
Action::Share {data: data.to_string()}
}
pub fn copy(data: &str) -> Self {
Action::Copy {data: data.to_string()}
}
pub fn select_image() -> Self {
Action::SelectImage
}
pub fn scan_qr(theme: &Theme) -> Self {
Action::Flow {
flow: Flow::new(vec![crate::Screen::new_builder(theme, Box::new(move |_: &Theme| {
crate::PageType::input("Scan QR code", Input::qr_code_scanner(), None, crate::Bumper::None)
}))]),
}
}
pub fn custom(action: impl Callback + 'static) -> Self {
Action::Custom {action: Box::new(action)}
}
pub fn flow(flow: Flow) -> Self {
Action::Flow {flow}
}
pub fn get(&self) -> Box<dyn Callback> {
match self {
Action::Share {data} => {
let data = data.clone();
Box::new(move |ctx: &mut Context, _: &Theme| ctx.send(Request::Hardware(Hardware::Share(data.clone()))))
}
Action::SelectImage => {
Box::new(move |_ctx: &mut Context, _: &Theme| println!("Selecting image"))
}
Action::Custom {action} => {
let mut action = action.clone();
Box::new(move |ctx: &mut Context, theme: &Theme| (action)(ctx, theme))
}
Action::Flow {flow} => {
let mut flow = flow.clone();
Box::new(move |ctx: &mut Context, theme: &Theme| {flow.build(ctx)(ctx, theme);})
}
Action::Paste => {
Box::new(move |ctx: &mut Context, _: &Theme| {ctx.send(Request::Hardware(Hardware::GetClipboard))})
}
Action::Copy {data} => {
let data = data.to_string();
Box::new(move |ctx: &mut Context, _: &Theme| {ctx.send(Request::Hardware(Hardware::SetClipboard(data.to_string())))})
}
_ => Box::new(move |_ctx: &mut Context, _: &Theme| {})
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ActionItem(Action, String, Icons);
impl ActionItem {
pub fn new(action: Action, label: &str, icon: Icons) -> Self {
ActionItem(action, label.to_string(), icon)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableItem {title: String, data: String}
impl TableItem {
pub fn new(title: &str, data: &str) -> Self {
TableItem { title: title.to_string(), data: data.to_string() }
}
}
#[derive(Clone)]
pub struct EnumItem {title: String, data: String} impl std::fmt::Debug for EnumItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnumItem").field("title", &self.title).field("data", &self.data).finish()
}
}
impl EnumItem {
pub fn new(title: &str, data: &str) -> Self { EnumItem {title: title.to_string(), data: data.to_string(), } }
fn get(&self) -> (&str, &str, Box<dyn Callback>) {
(&self.title as &str, &self.data as &str, Box::new(|_, _|{})) }
}
impl PartialEq for EnumItem {
fn eq(&self, other: &Self) -> bool {
self.title == other.title &&
self.data == other.data
}
}