use std::{future::Future, sync::Arc};
use topcoat::{Result, context::Cx, icon::IconData, view::View};
use crate::{
HeaderAction, HeaderActions,
naming::{kebab_case, sentence_case, type_stem},
navigation::NavigationItem,
};
pub trait Page: Sized + Send + Sync + 'static {
fn declare() -> PageDef {
PageDef::new()
}
fn render(cx: &Cx) -> impl Future<Output = Result<impl View>> + Send;
}
type AccessFn = dyn Fn(&Cx) -> bool + Send + Sync;
pub struct PageDef {
slug: Option<String>,
label: Option<crate::Text>,
icon: Option<IconData>,
navigation_order: Option<i32>,
navigation_group: Option<crate::Text>,
navigation: Option<NavigationItem>,
access: Arc<AccessFn>,
header_actions: HeaderActions,
}
impl PageDef {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn slug(mut self, slug: impl Into<String>) -> Self {
self.slug = Some(slug.into());
self
}
#[must_use]
pub fn label(mut self, label: impl Into<crate::Text>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn icon(mut self, icon: IconData) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn navigation_order(mut self, order: i32) -> Self {
self.navigation_order = Some(order);
self
}
#[must_use]
pub fn navigation_group(mut self, group: impl Into<crate::Text>) -> Self {
self.navigation_group = Some(group.into());
self
}
#[must_use]
pub fn navigation(mut self, item: NavigationItem) -> Self {
self.navigation = Some(item);
self
}
#[must_use]
pub fn access(mut self, access: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.access = Arc::new(access);
self
}
#[must_use]
pub fn header_action<A: HeaderAction>(mut self) -> Self {
self.header_actions = self.header_actions.add::<A>();
self
}
}
impl Default for PageDef {
fn default() -> Self {
Self {
slug: None,
label: None,
icon: None,
navigation_order: None,
navigation_group: None,
navigation: None,
access: Arc::new(|_| true),
header_actions: HeaderActions::new(),
}
}
}
impl std::fmt::Debug for PageDef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PageDef")
.field("slug", &self.slug)
.field("label", &self.label)
.field("navigation_order", &self.navigation_order)
.field("navigation_group", &self.navigation_group)
.field("navigation", &self.navigation)
.field("header_actions", &self.header_actions)
.finish_non_exhaustive()
}
}
pub(crate) struct MountedPage {
pub(crate) slug: String,
pub(crate) navigation: NavigationItem,
access: Arc<AccessFn>,
pub(crate) header_actions: HeaderActions,
}
impl MountedPage {
pub(crate) fn new<P: Page>(def: PageDef) -> Self {
let stem = type_stem::<P>("Page");
let mut navigation = def.navigation.unwrap_or_else(|| NavigationItem {
label: def.label.unwrap_or_else(|| {
let noun = sentence_case(stem);
crate::Text::derived(
crate::naming::derived_id("page", &crate::naming::slug_case(&noun)),
noun,
)
}),
..NavigationItem::default()
});
if let Some(order) = def.navigation_order {
navigation.order = order;
}
if let Some(icon) = def.icon {
navigation.icon = Some(icon);
}
if let Some(group) = def.navigation_group {
navigation.group = Some(group);
}
Self {
slug: def.slug.unwrap_or_else(|| kebab_case(stem)),
navigation,
access: def.access,
header_actions: def.header_actions,
}
}
pub(crate) fn admits(&self, cx: &Cx) -> bool {
(self.access)(cx)
}
pub(crate) fn access(&self) -> Arc<AccessFn> {
Arc::clone(&self.access)
}
}
pub(crate) type PageCheck = Box<dyn Fn(&Cx, &mut Vec<crate::DeclarationError>) + Send>;
pub(crate) fn check_page<P: Page>(page: Arc<MountedPage>) -> PageCheck {
Box::new(move |cx, errors| {
crate::resource::check_inputs::<P>(
cx,
page.header_actions
.entries()
.iter()
.map(|action| (action.name, action.input)),
errors,
);
})
}
#[cfg(test)]
mod tests;