use crate::i18n::Text;
use crate::screen::{Icon, ScreenDef};
pub struct Shell;
impl Shell {
pub fn site() -> SiteShell {
SiteShell::default()
}
}
#[derive(Default)]
pub struct SiteShell {
pub(crate) header: SiteHeader,
pub(crate) footer: SiteFooter,
pub(crate) bottom: Vec<BottomNavItem>,
}
impl SiteShell {
pub fn header(mut self, build: impl FnOnce(SiteHeader) -> SiteHeader) -> Self {
self.header = build(self.header);
self
}
pub fn footer(mut self, build: impl FnOnce(SiteFooter) -> SiteFooter) -> Self {
self.footer = build(self.footer);
self
}
pub fn bottom_nav<const N: usize>(mut self, items: [BottomNavItem; N]) -> Self {
const {
assert!(
N <= 5,
"bottom_nav holds at most five items; put the others in the feature's menu"
)
};
self.bottom = items.into();
self
}
pub(crate) fn linked_screens(&self) -> impl Iterator<Item = &'static str> + '_ {
self.footer
.columns
.iter()
.flat_map(|c| &c.links)
.filter_map(|l| match l.target {
FooterTarget::Screen(s) => Some(s),
FooterTarget::Url(_) => None,
})
.chain(self.bottom.iter().map(|b| b.screen))
}
pub(crate) fn bad_urls(&self) -> impl Iterator<Item = &'static str> + '_ {
self.footer
.columns
.iter()
.flat_map(|c| &c.links)
.filter_map(|l| match l.target {
FooterTarget::Url(u) if !safe_url(u) => Some(u),
_ => None,
})
}
}
fn safe_url(url: &str) -> bool {
(url.starts_with('/') && !url.starts_with("//"))
|| ["https://", "http://", "mailto:"]
.iter()
.any(|p| url.starts_with(p))
}
#[derive(Default)]
pub struct SiteHeader {
pub(crate) sign_in: bool,
pub(crate) language: bool,
pub(crate) theme: bool,
}
impl SiteHeader {
pub fn sign_in(mut self) -> Self {
self.sign_in = true;
self
}
pub fn language(mut self) -> Self {
self.language = true;
self
}
pub fn theme(mut self) -> Self {
self.theme = true;
self
}
}
#[derive(Default)]
pub struct SiteFooter {
pub(crate) columns: Vec<FooterColumn>,
pub(crate) text: Option<Text>,
}
impl SiteFooter {
pub fn column(
mut self,
title: impl Into<Text>,
build: impl FnOnce(FooterColumn) -> FooterColumn,
) -> Self {
self.columns.push(build(FooterColumn {
title: title.into(),
links: Vec::new(),
}));
self
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.text = Some(text.into());
self
}
}
pub(crate) enum FooterTarget {
Screen(&'static str),
Url(&'static str),
}
pub(crate) struct FooterLink {
pub(crate) label: Text,
pub(crate) target: FooterTarget,
}
pub struct FooterColumn {
pub(crate) title: Text,
pub(crate) links: Vec<FooterLink>,
}
impl FooterColumn {
pub fn link<S: ScreenDef>(mut self, label: impl Into<Text>, _screen: S) -> Self {
const {
assert!(
S::META.route.is_some(),
"a footer link must point to a screen that has a route"
)
};
self.links.push(FooterLink {
label: label.into(),
target: FooterTarget::Screen(S::META.name),
});
self
}
pub fn url(mut self, label: impl Into<Text>, href: &'static str) -> Self {
self.links.push(FooterLink {
label: label.into(),
target: FooterTarget::Url(href),
});
self
}
}
pub struct BottomNavItem {
pub(crate) label: Text,
pub(crate) screen: &'static str,
pub(crate) icon: Option<Icon>,
}
impl BottomNavItem {
pub fn new<S: ScreenDef>(label: impl Into<Text>, _screen: S) -> Self {
const {
assert!(
S::META.route.is_some(),
"a bottom bar link must point to a screen that has a route"
)
};
BottomNavItem {
label: label.into(),
screen: S::META.name,
icon: None,
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
}