#[allow(unused_imports)]
use crate::{FieldKind, Fill, Region, RowPart};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Width {
Content,
Fixed,
Fill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Priority {
Optional,
Secondary,
Essential,
}
impl Priority {
pub const RAISING: &'static [Self] = &[Self::Optional, Self::Secondary, Self::Essential];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub struct Budget {
pub columns: u32,
pub floors: u32,
}
impl Budget {
#[must_use]
pub fn width(self, ch: f32, gap: f32) -> f32 {
self.floors as f32 * ch + gap * (self.columns + 1) as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub struct Sizing<'a, L> {
pub lengths: &'a [(&'a str, L)],
pub fallback: L,
}
impl<'a, L: Copy> Sizing<'a, L> {
#[must_use]
pub const fn new(lengths: &'a [(&'a str, L)], fallback: L) -> Self {
Self { lengths, fallback }
}
#[must_use]
pub fn length_for(&self, name: &str) -> L {
self.lengths
.iter()
.find(|(column, _)| *column == name)
.map_or(self.fallback, |(_, length)| *length)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Fallback {
Wrap,
Stack,
Shed,
Menu,
}
pub const MIN_CEILING: u16 = 64;
const FILL_FLOOR: u16 = 16;
const fn char_len(s: &str) -> usize {
let bytes = s.as_bytes();
let mut chars = 0;
let mut i = 0;
while i < bytes.len() {
chars += 1;
let byte = bytes[i];
i += if byte < 0x80 {
1
} else if byte < 0xE0 {
2
} else if byte < 0xF0 {
3
} else {
4
};
}
chars
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ColumnKind {
#[default]
Text,
Identifier,
Date,
Number,
Code,
Status,
Actions,
Picture,
}
impl ColumnKind {
#[must_use]
pub const fn floor(self) -> u16 {
match self {
Self::Text | Self::Code | Self::Status | Self::Actions | Self::Picture => 8,
Self::Identifier => 12,
Self::Date => 10,
Self::Number => 6,
}
}
#[must_use]
pub const fn aligns_end(self) -> bool {
matches!(self, Self::Number | Self::Actions)
}
#[must_use]
pub const fn wraps(self) -> bool {
matches!(self, Self::Text | Self::Identifier)
}
#[must_use]
pub const fn monospace(self) -> bool {
matches!(self, Self::Identifier | Self::Code)
}
#[must_use]
pub const fn tabular(self) -> bool {
matches!(self, Self::Date | Self::Number)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Tiling {
#[default]
Narrow,
Never,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Sort {
Ascending,
Descending,
}
impl Sort {
#[must_use]
pub const fn reversed(self) -> Self {
match self {
Self::Ascending => Self::Descending,
Self::Descending => Self::Ascending,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ascending => "ascending",
Self::Descending => "descending",
}
}
#[must_use]
pub const fn glyph(self) -> &'static str {
match self {
Self::Ascending => "\u{25B2}",
Self::Descending => "\u{25BC}",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Caret {
Sorted(Sort),
Idle,
None,
}
impl Caret {
#[must_use]
pub const fn of(sorted: Option<Sort>, caret: bool) -> Self {
match sorted {
Some(sort) => Self::Sorted(sort),
None if caret => Self::Idle,
None => Self::None,
}
}
#[must_use]
pub fn label(self, name: &str, ascending: &str, descending: &str) -> String {
let label = name.to_uppercase();
let glyph = match self {
Self::Sorted(Sort::Descending) => descending,
Self::Sorted(Sort::Ascending) | Self::Idle => ascending,
Self::None => return label,
};
format!("{label} {glyph}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ColumnRoom<'a> {
pub heading: &'a str,
pub width: Width,
pub priority: Priority,
pub kind: ColumnKind,
pub min: Option<u16>,
pub caret: bool,
}
impl<'a> ColumnRoom<'a> {
#[must_use]
pub const fn new(heading: &'a str) -> Self {
Self {
heading,
width: Width::Fill,
priority: Priority::Essential,
kind: ColumnKind::Text,
min: None,
caret: false,
}
}
#[must_use]
pub const fn floor(&self) -> u16 {
let raw = match self.min {
Some(ch) => ch,
None => {
let caret = if self.caret { 2 } else { 0 };
let chars = char_len(self.heading);
let heading = if chars > MIN_CEILING as usize {
MIN_CEILING
} else {
chars as u16 + 2 + caret
};
let kind = self.kind.floor();
let fill = if matches!(self.width, Width::Fill) {
FILL_FLOOR
} else {
0
};
let wider = if heading > kind { heading } else { kind };
if fill > wider { fill } else { wider }
}
};
let raw = if raw > MIN_CEILING { MIN_CEILING } else { raw };
let even = raw + raw % 2;
if even < 2 {
2
} else if even > MIN_CEILING {
MIN_CEILING
} else {
even
}
}
#[must_use]
pub const fn kept_at(&self, cutoff: Priority) -> bool {
(self.priority as u8) >= (cutoff as u8)
}
#[must_use]
pub fn budget(rooms: &[Self], cutoff: Priority) -> Budget {
rooms
.iter()
.filter(|room| room.kept_at(cutoff))
.fold(Budget::default(), |budget, room| Budget {
columns: budget.columns + 1,
floors: budget.floors + u32::from(room.floor()),
})
}
#[must_use]
pub fn cutoff_for(rooms: &[Self], fits: impl Fn(Budget) -> bool) -> Priority {
let strongest = rooms
.iter()
.map(|room| room.priority)
.max()
.unwrap_or(Priority::Essential);
Priority::RAISING
.iter()
.copied()
.take_while(|&cutoff| cutoff <= strongest)
.find(|&cutoff| fits(Self::budget(rooms, cutoff)))
.unwrap_or(strongest)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CellPart {
Value,
Tokens,
Actions,
Link,
}
impl CellPart {
#[must_use]
pub const fn intent(self) -> &'static str {
match self {
Self::Value => "content",
Self::Tokens => "content",
Self::Actions => "content",
Self::Link => "content",
}
}
}