use std::{future::Future, marker::PhantomData, sync::Arc};
use derive_where::derive_where;
use toasty::stmt::Path;
use topcoat::{
Result,
context::Cx,
icon::IconData,
view::{BoxView, View, ViewExt},
};
use crate::{Color, naming::type_stem};
pub trait Widget: Sized + Send + Sync + 'static {
fn declare() -> WidgetDef {
WidgetDef::new()
}
fn render(cx: &Cx) -> impl Future<Output = Result<impl View>> + Send;
}
type AccessFn = dyn Fn(&Cx) -> bool + Send + Sync;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Span {
Columns(u8),
Full,
}
pub struct WidgetDef {
name: Option<&'static str>,
span: Span,
access: Arc<AccessFn>,
}
impl WidgetDef {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub fn span(mut self, columns: u8) -> Self {
self.span = Span::Columns(columns.max(1));
self
}
#[must_use]
pub fn full_width(mut self) -> Self {
self.span = Span::Full;
self
}
#[must_use]
pub fn access(mut self, access: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.access = Arc::new(access);
self
}
}
impl Default for WidgetDef {
fn default() -> Self {
Self {
name: None,
span: Span::Columns(1),
access: Arc::new(|_| true),
}
}
}
impl std::fmt::Debug for WidgetDef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WidgetDef")
.field("name", &self.name)
.field("span", &self.span)
.finish_non_exhaustive()
}
}
pub(crate) struct MountedWidget {
pub(crate) name: String,
pub(crate) span: Span,
access: Arc<AccessFn>,
pub(crate) render: for<'a> fn(&'a Cx) -> crate::BoxFuture<'a, Result<BoxView<'a>>>,
}
impl MountedWidget {
pub(crate) fn new<W: Widget>() -> Self {
let def = W::declare();
Self {
name: def.name.map_or_else(
|| crate::naming::kebab_case(type_stem::<W>("Widget")),
str::to_string,
),
span: def.span,
access: def.access,
render: render::<W>,
}
}
pub(crate) fn admits(&self, cx: &Cx) -> bool {
(self.access)(cx)
}
}
impl std::fmt::Debug for MountedWidget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MountedWidget")
.field("name", &self.name)
.field("span", &self.span)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub(crate) struct WidgetGrid {
pub(crate) widgets: Vec<MountedWidget>,
pub(crate) columns: u8,
}
impl WidgetGrid {
pub(crate) fn push<W: Widget>(&mut self) {
self.widgets.push(MountedWidget::new::<W>());
}
pub(crate) fn set_columns(&mut self, columns: u8) {
self.columns = columns.clamp(1, 4);
}
pub(crate) fn admitted<'a>(&'a self, cx: &'a Cx) -> impl Iterator<Item = &'a MountedWidget> {
self.widgets.iter().filter(|widget| widget.admits(cx))
}
pub(crate) fn check<T>(&self, errors: &mut Vec<crate::DeclarationError>) {
let mut seen = std::collections::HashSet::new();
for widget in &self.widgets {
if !seen.insert(widget.name.as_str()) {
errors.push(crate::DeclarationError::of::<T>(
crate::declaration::Site::Registration,
crate::DeclarationErrorKind::DuplicateWidget {
name: widget.name.clone(),
},
));
}
}
}
}
impl Default for WidgetGrid {
fn default() -> Self {
Self {
widgets: Vec::new(),
columns: 2,
}
}
}
fn render<W: Widget>(cx: &Cx) -> crate::BoxFuture<'_, Result<BoxView<'_>>> {
Box::pin(async move { Ok(W::render(cx).await?.boxed()) })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Trend {
Up,
Down,
Flat,
}
#[derive(Clone, Debug)]
pub struct Stat {
pub(crate) label: crate::Text,
pub(crate) value: String,
pub(crate) detail: Option<crate::Text>,
pub(crate) icon: Option<IconData>,
pub(crate) href: Option<String>,
pub(crate) trend: Option<Trend>,
pub(crate) color: Color,
pub(crate) chart: Vec<f64>,
}
impl Stat {
pub fn new(label: impl Into<crate::Text>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
value: value.into(),
detail: None,
icon: None,
href: None,
trend: None,
color: Color::Gray,
chart: Vec::new(),
}
}
#[must_use]
pub fn detail(mut self, detail: impl Into<crate::Text>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub fn icon(mut self, icon: IconData) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn href(mut self, href: impl Into<String>) -> Self {
self.href = Some(href.into());
self
}
#[must_use]
pub fn trend(mut self, trend: Trend) -> Self {
self.trend = Some(trend);
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
#[must_use]
pub fn chart(mut self, values: impl IntoIterator<Item = f64>) -> Self {
self.chart = values.into_iter().collect();
self
}
}
pub const DEFAULT_RECORD_ROWS: usize = 5;
pub const MAX_RECORD_ROWS: usize = 50;
#[derive_where(Clone, Debug)]
pub struct RecordTable<M> {
pub(crate) heading: Option<crate::Text>,
pub(crate) description: Option<crate::Text>,
pub(crate) columns: Option<Vec<String>>,
pub(crate) filter: Option<toasty::stmt::Expr<bool>>,
pub(crate) order_by: Vec<toasty::stmt::OrderBy>,
pub(crate) limit: usize,
#[derive_where(skip(Debug))]
_model: PhantomData<fn() -> M>,
}
impl<M> RecordTable<M> {
pub fn new() -> Self {
Self {
heading: None,
description: None,
columns: None,
filter: None,
order_by: Vec::new(),
limit: DEFAULT_RECORD_ROWS,
_model: PhantomData,
}
}
#[must_use]
pub fn heading(mut self, heading: impl Into<crate::Text>) -> Self {
self.heading = Some(heading.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<crate::Text>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn column<T>(self, path: impl Into<Path<M, T>>) -> Self
where
M: toasty::schema::Model,
{
let name = crate::schema::form_key(path);
self.column_named(name)
}
#[must_use]
pub fn column_named(mut self, name: impl Into<String>) -> Self {
self.columns.get_or_insert_with(Vec::new).push(name.into());
self
}
#[must_use]
pub fn filter(mut self, filter: toasty::stmt::Expr<bool>) -> Self {
self.filter = Some(match self.filter.take() {
Some(previous) => previous.and(filter),
None => filter,
});
self
}
#[must_use]
pub fn order_by(mut self, order: impl Into<toasty::stmt::OrderBy>) -> Self {
self.order_by.push(order.into());
self
}
#[must_use]
pub fn limit(mut self, rows: usize) -> Self {
self.limit = rows.clamp(1, MAX_RECORD_ROWS);
self
}
}
impl<M> Default for RecordTable<M> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests;