1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
//! How a cell presents its value: its [`Color`], its [`Alignment`], and the number and money
//! spellings of a [`Numeric`] value.
/// A semantic color for a badge, a status dot or a cell's text.
///
/// Each maps to a theme token (`--success`, `--warning`, `--info`, `--destructive`,
/// `--primary`, `--muted-foreground`), so a theme that redeclares the token recolors every cell
/// that uses it.
///
/// ```rust
/// #[derive(tablo_core::Options)]
/// enum Status {
/// #[option(color = "warning")]
/// Pending,
/// #[option(color = "success")]
/// Paid,
/// #[option(color = "danger")]
/// Refunded,
/// }
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Color {
/// A neutral, muted status.
#[default]
Gray,
/// The theme's primary color.
Primary,
/// Done, paid, active.
Success,
/// Pending, expiring, low.
Warning,
/// Failed, refunded, overdue.
Danger,
/// Informational, in progress.
Info,
}
impl Color {
/// The color a `#[option(color = "…")]` attribute names.
#[doc(hidden)]
pub const fn from_name(name: &str) -> Option<Self> {
Some(match name.as_bytes() {
b"gray" => Self::Gray,
b"primary" => Self::Primary,
b"success" => Self::Success,
b"warning" => Self::Warning,
b"danger" => Self::Danger,
b"info" => Self::Info,
_ => return None,
})
}
/// The name a `#[option(color = "…")]` attribute gives this color.
pub(crate) const fn name(self) -> &'static str {
match self {
Self::Gray => "gray",
Self::Primary => "primary",
Self::Success => "success",
Self::Warning => "warning",
Self::Danger => "danger",
Self::Info => "info",
}
}
/// The class of a badge's status dot in this color.
pub(crate) const fn dot_class(self) -> &'static str {
match self {
Self::Gray => "size-1.5 shrink-0 rounded-full bg-muted-foreground/60",
Self::Primary => "size-1.5 shrink-0 rounded-full bg-primary",
Self::Success => "size-1.5 shrink-0 rounded-full bg-success",
Self::Warning => "size-1.5 shrink-0 rounded-full bg-warning",
Self::Danger => "size-1.5 shrink-0 rounded-full bg-destructive",
Self::Info => "size-1.5 shrink-0 rounded-full bg-info",
}
}
/// The class of a surface filled with this color, such as an environment's stripe.
pub(crate) const fn fill_class(self) -> &'static str {
match self {
Self::Gray => "bg-muted-foreground",
Self::Primary => "bg-primary",
Self::Success => "bg-success",
Self::Warning => "bg-warning",
Self::Danger => "bg-destructive",
Self::Info => "bg-info",
}
}
/// The class of a table row tinted in this color.
pub(crate) const fn row_class(self) -> &'static str {
match self {
Self::Gray => "bg-muted/50",
Self::Primary => "bg-primary/5",
Self::Success => "bg-success/10",
Self::Warning => "bg-warning/10",
Self::Danger => "bg-destructive/10",
Self::Info => "bg-info/10",
}
}
/// The classes that tint a button in this color, `None` for the neutral button. They are
/// marked important to win over the outline button's own colors.
pub(crate) const fn button_tone(self) -> Option<&'static str> {
match self {
Self::Gray => None,
Self::Primary => Some("text-primary! hover:bg-primary/5! active:bg-primary/10!"),
Self::Success => Some("text-success! hover:bg-success/5! active:bg-success/10!"),
Self::Warning => Some("text-warning! hover:bg-warning/5! active:bg-warning/10!"),
Self::Danger => {
Some("text-destructive! hover:bg-destructive/5! active:bg-destructive/10!")
}
Self::Info => Some("text-info! hover:bg-info/5! active:bg-info/10!"),
}
}
/// The class of a cell's text in this color.
pub(crate) const fn text_class(self) -> &'static str {
match self {
Self::Gray => "text-muted-foreground",
Self::Primary => "text-primary",
Self::Success => "text-success",
Self::Warning => "text-warning",
Self::Danger => "text-destructive",
Self::Info => "text-info",
}
}
}
/// How heavy a column's text renders.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FontWeight {
/// Lighter than the body text.
Light,
/// The body text's weight, the default.
#[default]
Normal,
/// Between normal and semibold.
Medium,
/// Between medium and bold.
Semibold,
/// The heaviest.
Bold,
}
impl FontWeight {
/// The font-weight class of this weight, empty at the body text's.
pub(crate) const fn class(self) -> &'static str {
match self {
Self::Light => "font-light",
Self::Normal => "",
Self::Medium => "font-medium",
Self::Semibold => "font-semibold",
Self::Bold => "font-bold",
}
}
}
/// Where a column's header and cells align their content.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Alignment {
/// The reading direction's start, the default.
#[default]
Start,
/// The middle of the column.
Center,
/// The reading direction's end: numbers and money, so their digits line up.
End,
}
impl Alignment {
/// The text-alignment class of this alignment, empty at the start.
pub(crate) const fn class(self) -> &'static str {
match self {
Self::Start => "",
Self::Center => "text-center",
Self::End => "text-end",
}
}
}
/// A number as a cell reads it: an integer and a decimal stay exact, a float is rounded when
/// spelled.
///
/// Equality compares the representation, not the amount: `Decimal { mantissa: 1250, scale: 2 }`
/// differs from `Decimal { mantissa: 125, scale: 1 }`, and `Int(12)` from
/// `Decimal { mantissa: 12, scale: 0 }`.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Number {
/// Any integer type, widened.
Int(i128),
/// `f32` or `f64`.
Float(f64),
/// `mantissa / 10^scale`, exactly: a decimal type's value. Spelled with fewer digits than
/// `scale`, it rounds half to even, as a float's spelling does.
Decimal {
/// The digits, with the sign.
mantissa: i128,
/// How many of the digits follow the point.
scale: u32,
},
}
/// A field a [`TextColumn`](crate::TextColumn) can spell as a number or an amount of money:
/// every integer and float type, `rust_decimal::Decimal` and `bigdecimal::BigDecimal` with the
/// features of those names, and an `Option` of one, which reads as the table's dash when `None`.
pub trait Numeric {
/// The value, `None` when it holds none.
fn number(&self) -> Option<Number>;
}
impl<T: Numeric + ?Sized> Numeric for &T {
fn number(&self) -> Option<Number> {
(**self).number()
}
}
macro_rules! numeric_ints {
($($ty:ty),+) => {
$(
impl Numeric for $ty {
fn number(&self) -> Option<Number> {
Some(Number::Int(i128::from(*self)))
}
}
)+
};
}
numeric_ints!(i8, i16, i32, i64, u8, u16, u32, u64);
impl Numeric for f32 {
fn number(&self) -> Option<Number> {
Some(Number::Float(f64::from(*self)))
}
}
impl Numeric for f64 {
fn number(&self) -> Option<Number> {
Some(Number::Float(*self))
}
}
#[cfg(feature = "rust_decimal")]
impl Numeric for rust_decimal::Decimal {
fn number(&self) -> Option<Number> {
Some(Number::Decimal {
mantissa: self.mantissa(),
scale: self.scale(),
})
}
}
/// A value of more than 38 significant digits, which no `i128` mantissa holds, is rounded to 38,
/// half to even; one whose integer part alone outgrows them reads as a float.
#[cfg(feature = "bigdecimal")]
impl Numeric for bigdecimal::BigDecimal {
fn number(&self) -> Option<Number> {
use bigdecimal::{RoundingMode, ToPrimitive};
const DIGITS: u64 = 38;
let exact = || {
let (mantissa, scale) = match self.digits().checked_sub(DIGITS) {
Some(excess @ 1..) => {
let (_, scale) = self.as_bigint_and_scale();
let scale = scale.checked_sub(i64::try_from(excess).ok()?)?;
self.with_scale_round(scale, RoundingMode::HalfEven)
.into_bigint_and_scale()
}
_ => self.clone().into_bigint_and_scale(),
};
let mantissa = mantissa.to_i128()?;
match u32::try_from(scale) {
Ok(scale) => Some(Number::Decimal { mantissa, scale }),
Err(_) if scale < 0 => {
let shift = 10i128.checked_pow(u32::try_from(scale.unsigned_abs()).ok()?)?;
Some(Number::Decimal {
mantissa: mantissa.checked_mul(shift)?,
scale: 0,
})
}
Err(_) => None,
}
};
exact().or_else(|| {
self.to_f64()
.filter(|float| float.is_finite())
.map(Number::Float)
})
}
}
impl<T: Numeric> Numeric for Option<T> {
fn number(&self) -> Option<Number> {
self.as_ref().and_then(Numeric::number)
}
}
mod number;
pub use number::Currency;
pub(crate) use number::NumberFormat;
#[cfg(test)]
use number::format_number;
#[cfg(test)]
mod tests;