use roost_ui::{emitters, drawables, Component, Context, IS_MOBILE, IS_WEB};
use roost_ui::events::{Event, OnEvent, MouseEvent, MouseState};
use roost_ui::drawable::{Drawable, Align};
use roost_ui::layouts::{AdjustScrollEvent, Column, Stack, Row, Padding, Offset, Size, Scroll, ScrollAnchor, ScrollDirection, Opt};
use crate::components::{Rectangle, TextStyle, TextSize, ExpandableText};
use crate::components::button::{GhostIconButton, PrimaryButton, SecondaryButton};
use crate::components::interface::navigation::{AppPage, NavigateEvent, RootInfo, NavigatorEvent, PageBuilder};
use crate::components::interface::{desktop::DesktopInterface, mobile::MobileInterface, web::WebInterface};
use crate::pages::Error;
use crate::plugin::PelicanUI;
#[derive(Component)]
pub struct Interface(Stack, Rectangle, Box<dyn InterfaceTrait>, #[skip] Option<Vec<PageBuilder>>);
impl Interface {
pub fn new(ctx: &mut Context,
mut navigation: (Vec<RootInfo>, Option<Vec<RootInfo>>),
) -> Self {
let mut pages: Vec<PageBuilder> = navigation.0
.iter_mut().chain(navigation.1.iter_mut().flatten())
.map(|nav| nav.get_page.take().unwrap()).collect();
let navigation = match navigation.0.len() <= 1 && navigation.1.is_none()
|| navigation.0.is_empty() && navigation.1.as_ref().map(|n| n.len() <= 1).unwrap_or(false) {
true => None,
false => Some(navigation)
};
if pages.is_empty() {panic!("You must provide at least one RootInfo to the Interface")}
let start_page = pages[0](ctx);
let visual: Box<dyn InterfaceTrait> = match IS_WEB {
true => Box::new(WebInterface::new(ctx, start_page, navigation, None)),
false if IS_MOBILE => Box::new(MobileInterface::new(ctx, start_page, navigation)),
false => Box::new(DesktopInterface::new(ctx, start_page, navigation)),
};
let color = ctx.get::<PelicanUI>().get().0.theme().colors.background.primary;
Interface(Stack::default(), Rectangle::new(color, 0.0, None), visual, Some(pages))
}
}
impl std::fmt::Debug for Interface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Interface")
}
}
impl OnEvent for Interface {
fn on_event(&mut self, ctx: &mut Context, mut event: Box<dyn Event>) -> Vec<Box<dyn Event>> {
if let Some(NavigateEvent(index)) = event.downcast_mut::<NavigateEvent>() {
let page = self.2.app_page().take().unwrap();
let new_page: Box<dyn AppPage> = page.navigate(ctx, *index)
.unwrap_or_else(|e| Box::new(Error::new(ctx, e)));
*self.2.app_page() = Some(new_page);
let display = self.2.app_page().as_ref().map(|s| s.has_navigator()).unwrap_or(false);
if let Some(navigator) = self.2.navigator() { navigator.display(display); }
} else if let Some(NavigatorEvent(index)) = event.downcast_mut::<NavigatorEvent>() {
if let Some(pages) = self.3.as_mut() {
*self.2.app_page() = Some(pages[*index](ctx));
}
}
vec![event]
}
}
pub trait InterfaceTrait: Drawable + std::fmt::Debug + 'static {
fn app_page(&mut self) -> &mut Option<Box<dyn AppPage>>;
fn navigator(&mut self) -> &mut Option<Opt<Box<dyn Drawable>>>;
}
#[derive(Debug, Component)]
pub struct Page(Column, Header, Content, Option<Bumper>);
impl OnEvent for Page {}
impl Page {
pub fn new(header: Header, content: Content, bumper: Option<Bumper>) -> Self {
let width = Size::custom(move |widths: Vec<(f32, f32)>|(widths[0].0, f32::MAX));
Page(
Column::new(12.0, Offset::Center, width, Padding::default()),
header,
content,
bumper,
)
}
pub fn header(&mut self) -> &mut Header {&mut self.1}
pub fn content(&mut self) -> &mut Content {&mut self.2}
pub fn bumper(&mut self) -> &mut Option<Bumper> {&mut self.3}
}
#[derive(Debug, Component)]
pub struct Content (Scroll, emitters::Scrollable<ContentChildren>);
impl Content {
pub fn new(ctx: &mut Context, offset: Offset, content: Vec<Box<dyn Drawable>>) -> Self {
let layout = ctx.get::<PelicanUI>().get().0.theme().layout.clone();
let max = if roost_ui::IS_WEB {1200.0} else {layout.content_max};
let width = Size::custom(move |widths: Vec<(f32, f32)>|(widths[0].0.min(max), max));
let height = Size::custom(move |_: Vec<(f32, f32)>|(0.0, f32::MAX));
let anchor = if offset == Offset::End { ScrollAnchor::End } else { ScrollAnchor::Start };
let scroll = Scroll::new(Offset::Center, offset, width, height, Padding::default(), anchor, ScrollDirection::Vertical);
Content(scroll, emitters::Scrollable::new(ContentChildren::new(content, layout.content_padding)))
}
pub fn find<T: std::any::Any>(&mut self) -> Option<&mut T> {
self.items().iter_mut().find_map(|item| (**item).as_any_mut().downcast_mut::<T>())
}
pub fn find_at<T: std::any::Any>(&mut self, i: usize) -> Option<&mut T> {
self.items().get_mut(i).and_then(|item| (**item).as_any_mut().downcast_mut::<T>())
}
pub fn remove<T: std::any::Any>(&mut self) -> Option<T> {
if let Some(pos) = self.items().iter().position(|item| (**item).as_any().is::<T>()) {
let boxed = self.items().remove(pos);
boxed.into_any().downcast::<T>().ok().map(|b| *b)
} else {
None
}
}
pub fn items(&mut self) -> &mut Vec<Box<dyn Drawable>> {&mut self.1.1.inner.1}
pub fn offset(&mut self) -> &mut Offset {self.0.offset()}
}
impl OnEvent for Content {
fn on_event(&mut self, _ctx: &mut Context, event: Box<dyn Event>) -> Vec<Box<dyn Event>> {
if let Some(AdjustScrollEvent::Vertical(a)) = event.downcast_ref::<AdjustScrollEvent>() {
self.0.adjust_scroll(*a);
} else if let Some(MouseEvent { state: MouseState::Scroll(_, y), position: Some(_) }) = event.downcast_ref::<MouseEvent>() {
self.0.adjust_scroll(*y);
}
vec![event]
}
}
#[derive(Debug, Component)]
struct ContentChildren (Column, Vec<Box<dyn Drawable>>);
impl OnEvent for ContentChildren {}
impl ContentChildren {
pub fn new(content: Vec<Box<dyn Drawable>>, padding: f32) -> Self {
ContentChildren(Column::new(24.0, Offset::Center, Size::Fit, Padding::new(padding)), content)
}
}
#[derive(Debug, Component)]
pub struct Header(Row, HeaderIcon, Box<dyn Drawable>, HeaderIcon);
impl OnEvent for Header {}
impl Header {
pub fn home(ctx: &mut Context, title: &str, icon: Option<(&'static str, usize)>) -> Self {
Self::_new(ctx, title, None, icon, TextSize::H3)
}
pub fn stack(ctx: &mut Context, title: &str) -> Self {
Self::_new(ctx, title, Some(("left", 0)), None, TextSize::H4)
}
pub fn stack_end(ctx: &mut Context, title: &str) -> Self {
Self::_new(ctx, title, Some(("close", 0)), None, TextSize::H4)
}
fn _new(
ctx: &mut Context,
title: &str,
left_icon: Option<(&'static str, usize)>,
right_icon: Option<(&'static str, usize)>,
size: TextSize,
) -> Self {
let clean: String = title.chars().filter(|c| c.is_alphanumeric() || c.is_whitespace()).collect();
let title = clean[..1].to_uppercase() + &clean[1..].to_lowercase();
let text = ExpandableText::new(ctx, &title, size, TextStyle::Heading, Align::Center, Some(1));
let mut make_icon = |icon: Option<(&'static str, usize)>| icon.map(|(name, idx)| {
let cb = move |ctx: &mut Context| ctx.trigger_event(NavigateEvent(idx));
HeaderIcon::new(ctx, name, cb)
}).unwrap_or_default();
let layout = Row::new(16.0, Offset::Center, Size::Fit, Padding(24.0, 16.0, 24.0, 16.0));
Header(layout, make_icon(left_icon), Box::new(text), make_icon(right_icon))
}
}
#[derive(Debug, Component)]
pub struct HeaderIcon(Stack, Option<GhostIconButton>);
impl OnEvent for HeaderIcon {}
impl Default for HeaderIcon {fn default() -> Self {Self::none()}}
impl HeaderIcon {
pub fn new(ctx: &mut Context, icon: &'static str, closure: impl FnMut(&mut Context) + 'static) -> Self {
let layout = Stack(Offset::Center, Offset::Center, Size::Static(48.0), Size::Static(48.0), Padding::default());
HeaderIcon(layout, Some(GhostIconButton::new(ctx, icon, closure)))
}
pub fn none() -> Self {
let layout = Stack(Offset::Center, Offset::Center, Size::Static(48.0), Size::Static(48.0), Padding::default());
HeaderIcon(layout, None)
}
}
#[derive(Debug, Component)]
pub struct Bumper (Stack, Rectangle, BumperContent);
impl OnEvent for Bumper {}
impl Bumper {
pub fn home(ctx: &mut Context, first: &str, second: Option<&str>) -> Self {
let first = PrimaryButton::new(ctx, first, |ctx: &mut Context| ctx.trigger_event(NavigateEvent(1)), false);
let second = second.map(|l| PrimaryButton::new(ctx, l, |ctx: &mut Context| ctx.trigger_event(NavigateEvent(2)), false));
Self::new(ctx, drawables![first, second])
}
pub fn stack(ctx: &mut Context, is_disabled: bool) -> Self {
let button = PrimaryButton::new(ctx, "Continue", |ctx: &mut Context| ctx.trigger_event(NavigateEvent(1)), is_disabled);
Self::new(ctx, drawables![button])
}
pub fn stack_end(ctx: &mut Context) -> Self {
let button = SecondaryButton::large(ctx, "Done", |ctx: &mut Context| ctx.trigger_event(NavigateEvent(1)));
Self::new(ctx, drawables![button])
}
pub fn new(ctx: &mut Context, content: Vec<Box<dyn Drawable>>) -> Self {
let background = ctx.get::<PelicanUI>().get().0.theme().colors.background.primary;
let max = ctx.get::<PelicanUI>().get().0.theme().layout.bumper_max;
let max = if roost_ui::IS_WEB {1200.0} else {max};
let width = Size::custom(move |widths: Vec<(f32, f32)>|(widths[0].0.min(max), max));
let height = Size::custom(move |heights: Vec<(f32, f32)>|(heights[1].0, heights[1].1));
let layout = Stack(Offset::Center, Offset::Start, width, height, Padding::default());
Bumper(layout, Rectangle::new(background, 0.0, None), BumperContent::new(content))
}
pub fn items(&mut self) -> &mut Vec<Box<dyn Drawable>> { &mut self.2.1 }
pub fn find<T: std::any::Any>(&mut self) -> Option<&mut T> {
self.items().iter_mut().find_map(|item| (**item).as_any_mut().downcast_mut::<T>())
}
pub fn find_at<T: std::any::Any>(&mut self, i: usize) -> Option<&mut T> {
self.items().get_mut(i).and_then(|item| (**item).as_any_mut().downcast_mut::<T>())
}
}
#[derive(Debug, Component)]
struct BumperContent (Row, Vec<Box<dyn Drawable>>);
impl OnEvent for BumperContent {}
impl BumperContent {
fn new(content: Vec<Box<dyn Drawable>>) -> Self {
BumperContent(Row::new(16.0, Offset::Center, Size::Fit, Padding(24.0, 16.0, 24.0, 16.0)), content)
}
}