use std::sync::Arc;
use derive_where::derive_where;
use toasty::stmt::{Expr, OrderByExpr};
use topcoat::{context::Cx, icon::IconData, view::*};
use super::{
Column, ColumnBase, ColumnWidth, ReadFn,
cell::{TextCell, text_cell},
sort_order,
};
use crate::{
Lens, Text,
display::{Alignment, Color, Currency, FontWeight, Numeric},
values::FormScalar,
};
#[derive_where(Clone, Debug)]
pub struct TextColumn<M, T> {
lens: Lens<M, T>,
base: ColumnBase,
#[derive_where(skip(Debug))]
format: FormatFn<M, T>,
#[derive_where(skip(Debug))]
search: Option<SearchFn>,
sortable: bool,
#[derive_where(skip(Debug))]
color: ColorFn<T>,
tinted: bool,
badge: bool,
tabular: bool,
weight: FontWeight,
alignment: Alignment,
#[derive_where(skip(Debug))]
description: Option<RowFn<M, String>>,
#[derive_where(skip(Debug))]
icon: ReadFn<T, Option<IconData>>,
#[derive_where(skip(Debug))]
url: Option<RowFn<M, Option<String>>>,
new_tab: bool,
#[derive_where(skip(Debug))]
tooltip: Option<RowFn<M, Option<String>>>,
limit: Option<usize>,
copyable: bool,
placeholder: Option<Text>,
reads_row: bool,
row_reads: Option<crate::lens::Reads<M>>,
}
type SearchFn = Arc<dyn Fn(String) -> Expr<bool> + Send + Sync>;
type ColorFn<T> = ReadFn<T, Option<Color>>;
type RowFn<M, V> = ReadFn<M, V>;
type FormatFn<M, T> = Arc<dyn Fn(&Cx, &M, &T) -> Text + Send + Sync>;
pub(crate) const LIKE_ESCAPE: char = '\\';
pub(crate) fn escape_like(term: &str) -> String {
let mut escaped = String::with_capacity(term.len());
for c in term.chars() {
if c == LIKE_ESCAPE || c == '%' || c == '_' {
escaped.push(LIKE_ESCAPE);
}
escaped.push(c);
}
escaped
}
pub(super) fn escape_like_pattern(term: &str) -> String {
format!("%{}%", escape_like(term))
}
pub fn contains_expr<M, T>(lens: &Lens<M, T>, term: &str) -> Option<Expr<bool>>
where
M: toasty::schema::Model,
T: toasty::schema::Field<Inner = String>,
{
let trimmed = term.trim();
(!trimmed.is_empty()).then(|| {
lens.path()
.clone()
.like_with_escape(escape_like_pattern(trimmed), LIKE_ESCAPE)
})
}
impl<M, T> TextColumn<M, T>
where
M: toasty::schema::Model,
{
pub fn new(lens: Lens<M, T>) -> Self
where
T: FormScalar + Send + Sync + 'static,
{
Self {
base: ColumnBase::new(lens.path(), ColumnWidth::Grow),
lens,
format: Arc::new(|_, _, value: &T| value.to_label()),
search: None,
sortable: false,
color: Arc::new(T::color),
tinted: false,
badge: false,
tabular: false,
weight: FontWeight::Normal,
alignment: Alignment::Start,
description: None,
icon: Arc::new(T::icon),
url: None,
new_tab: false,
tooltip: None,
limit: None,
copyable: false,
placeholder: None,
reads_row: false,
row_reads: None,
}
}
pub fn format<L: Into<Text>>(
mut self,
format: impl Fn(&T) -> L + Send + Sync + 'static,
) -> Self {
self.format = Arc::new(move |_, _, value| format(value).into());
self
}
pub fn badge(mut self) -> Self {
self.badge = true;
self
}
pub fn color(mut self, color: impl Fn(&T) -> Option<Color> + Send + Sync + 'static) -> Self {
self.color = Arc::new(color);
self.tinted = true;
self
}
pub fn description(
mut self,
description: impl Fn(&M) -> String + Send + Sync + 'static,
) -> Self {
self.description = Some(Arc::new(description));
self.reads_row = true;
self
}
pub fn url(mut self, url: impl Fn(&M) -> Option<String> + Send + Sync + 'static) -> Self {
self.url = Some(Arc::new(url));
self.reads_row = true;
self
}
pub fn open_url_in_new_tab(mut self) -> Self {
self.new_tab = true;
self
}
pub fn tooltip(
mut self,
tooltip: impl Fn(&M) -> Option<String> + Send + Sync + 'static,
) -> Self {
self.tooltip = Some(Arc::new(tooltip));
self.reads_row = true;
self
}
pub fn limit(mut self, chars: usize) -> Self {
self.limit = Some(chars);
self
}
pub fn copyable(mut self) -> Self {
self.copyable = true;
self
}
pub fn placeholder(mut self, placeholder: impl Into<Text>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn icon(mut self, icon: impl Fn(&T) -> Option<IconData> + Send + Sync + 'static) -> Self {
self.icon = Arc::new(icon);
self
}
pub fn reading(mut self, reads: crate::lens::Reads<M>) -> Self {
self.row_reads = Some(reads);
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
pub fn align(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
base_builders!(sortable);
}
impl<M, T> TextColumn<M, T>
where
M: toasty::schema::Model,
T: Numeric + 'static,
{
pub fn numeric(mut self, decimals: u8) -> Self {
self.format = Arc::new(move |cx, _, value: &T| {
value
.number()
.map(|number| crate::display::NumberFormat::of(cx).spell(number, decimals))
.unwrap_or_default()
.into()
});
self.amount()
}
pub fn money(mut self, currency: Currency) -> Self {
self.format = Arc::new(move |cx, _, value: &T| currency.format_in(cx, value).into());
self.amount()
}
pub fn money_by(mut self, currency: impl Fn(&M) -> Currency + Send + Sync + 'static) -> Self {
self.format = Arc::new(move |cx, row, value: &T| currency(row).format_in(cx, value).into());
self.reads_row = true;
self.amount()
}
fn amount(mut self) -> Self {
self.alignment = Alignment::End;
self.tabular = true;
self.base.width = ColumnWidth::Auto;
self
}
}
impl<M, T> TextColumn<M, T>
where
M: toasty::schema::Model + Send + Sync + 'static,
T: toasty::schema::Field<Inner = String> + Send + Sync + 'static,
{
pub fn searchable(mut self) -> Self {
let path = self.lens.path().clone();
self.search = Some(Arc::new(move |pattern| {
path.clone().like_with_escape(pattern, LIKE_ESCAPE)
}));
self
}
}
impl<M, T> Column<M> for TextColumn<M, T>
where
M: toasty::schema::Model + Send + Sync + 'static,
T: Send + Sync + 'static,
{
base_column_methods!(reads);
fn reads(&self) -> Option<crate::lens::Reads<M>> {
let own = self.base.reads();
match (self.reads_row, &self.row_reads) {
(false, _) => Some(own),
(true, None) => None,
(true, Some(declared)) => Some(own.union(declared)),
}
}
fn text(&self, cx: &Cx, row: &M) -> String {
(self.format)(cx, row, self.lens.read(row)).localize(cx)
}
fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
let value = self.lens.read(row);
let text = (self.format)(cx, row, value).localize(cx);
let color = (!text.trim().is_empty())
.then(|| (self.color)(value))
.flatten();
let icon = (self.icon)(value).map(|icon| (icon, color));
let color = color.filter(|_| self.badge || self.tinted);
text_cell(
cx,
TextCell {
text,
color,
badge: self.badge,
tabular: self.tabular,
weight: self.weight,
icon,
description: self.description.as_ref().map(|describe| describe(row)),
url: self.url.as_ref().and_then(|url| url(row)),
new_tab: self.new_tab,
tooltip: self.tooltip.as_ref().and_then(|tooltip| tooltip(row)),
limit: self.limit,
copyable: self.copyable,
placeholder: self.placeholder.clone(),
},
)
}
fn alignment(&self) -> Alignment {
self.alignment
}
fn search_expr(&self, term: &str) -> Option<Expr<bool>> {
let t = term.trim();
let search = self.search.as_ref()?;
(!t.is_empty()).then(|| search(escape_like_pattern(t)))
}
fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
self.sortable
.then(|| sort_order(self.lens.path(), descending))
}
}
impl<M, T> crate::column::ColumnInternals<M> for TextColumn<M, T>
where
M: toasty::schema::Model + Send + Sync + 'static,
T: Send + Sync + 'static,
{
base_internals!(binding, related);
fn related(&self) -> crate::column::Related {
if self.reads_row {
crate::column::Related::Row
} else {
crate::column::Related::None
}
}
}