use std::borrow::Cow;
use super::{ColumnHead, Frame};
use crate::table::column::{ColumnWidth, NARROW_DEFAULT_PERCENT};
pub(super) const BULK_COLUMN_PERCENT: u8 = 5;
pub(super) struct ColumnWidths {
pub(super) cells: Vec<Option<Cow<'static, str>>>,
pub(super) bulk: Option<Cow<'static, str>>,
pub(super) actions: Option<Cow<'static, str>>,
pub(super) table_min_width: Option<Cow<'static, str>>,
}
type Units = u32;
const UNITS_PER_REM: Units = 16;
const ICON_BUTTON: Units = 36;
const LABEL_PADDING: Units = 26;
const GAP: Units = 4;
const CELL_PADDING: Units = 24;
const LABEL_CHAR: Units = 7;
const HEAD_CHAR: Units = 8;
const HEAD_PADDING: Units = 24;
const SORT_ICON: Units = 20;
const WIDE_COLUMN_MIN: Units = 96;
impl Frame<'_> {
pub(super) fn with_actions(&self) -> bool {
self.delete_prefix.is_some()
|| self.edit_prefix.is_some()
|| self.view_prefix.is_some()
|| !self.row_actions.is_empty()
}
fn action_icon_count(&self) -> Units {
let buttons = usize::from(self.view_prefix.is_some())
+ usize::from(self.edit_prefix.is_some())
+ usize::from(self.delete_prefix.is_some());
Units::try_from(buttons).unwrap_or(Units::MAX)
}
fn action_link_count(&self) -> usize {
usize::from(self.view_prefix.is_some())
+ usize::from(self.edit_prefix.is_some())
+ usize::from(self.delete_prefix.is_some())
+ 2 * self.row_actions.len()
}
fn actions_percent(&self) -> u8 {
match self.action_link_count() {
2 => 12,
3.. => 15,
_ => 8,
}
}
fn actions_width(&self) -> Units {
let labels: Units = self
.row_actions
.iter()
.map(|label| LABEL_PADDING + chars(label, LABEL_CHAR))
.sum();
let icons = self.action_icon_count() * ICON_BUTTON;
let controls = Units::try_from(self.row_actions.len())
.unwrap_or(Units::MAX)
.saturating_add(self.action_icon_count());
CELL_PADDING + labels + icons + GAP * controls.saturating_sub(1)
}
pub(super) fn column_widths(&self) -> ColumnWidths {
let bulk = self.bulk_enabled().then_some(BULK_COLUMN_PERCENT);
let actions = self.with_actions().then(|| self.actions_width());
let total: u32 = bulk
.into_iter()
.chain(
self.columns
.iter()
.filter_map(|col| col.width.default_percent()),
)
.chain(self.with_actions().then(|| self.actions_percent()))
.map(u32::from)
.sum();
let cells = self
.columns
.iter()
.map(|col| column_width_style(col, total))
.collect();
let mut min_width = MinWidth::default();
if let Some(share) = bulk {
min_width.share(scaled_default_percent(share, total));
}
for col in &self.columns {
min_width.column(col, total);
}
let actions_style = actions.map(|width| {
min_width.length(width);
Cow::Owned(format!("width: {}", rem(width)))
});
ColumnWidths {
cells,
bulk: bulk.map(|share| default_width_style(scaled_default_percent(share, total))),
actions: actions_style,
table_min_width: min_width.style(),
}
}
}
fn chars(text: &str, per_char: Units) -> Units {
text.chars()
.map(|c| if c.is_ascii() { per_char } else { per_char * 2 })
.sum()
}
fn wide_floor(head: &ColumnHead<'_>) -> Units {
let header =
chars(head.label, HEAD_CHAR) + HEAD_PADDING + if head.sortable { SORT_ICON } else { 0 };
header.max(WIDE_COLUMN_MIN)
}
pub(super) const DEFAULT_WIDTH_BUDGET_PERCENT: u8 = 60;
fn scaled_default_percent(nominal: u8, total: u32) -> u8 {
if total <= u32::from(DEFAULT_WIDTH_BUDGET_PERCENT) {
return nominal;
}
let scaled = u32::from(nominal) * u32::from(DEFAULT_WIDTH_BUDGET_PERCENT) / total;
u8::try_from(scaled).unwrap_or(DEFAULT_WIDTH_BUDGET_PERCENT)
}
fn default_width_style(percent: u8) -> Cow<'static, str> {
Cow::Owned(format!("width: {percent}%"))
}
fn column_width_style(head: &ColumnHead<'_>, total: u32) -> Option<Cow<'static, str>> {
head.width.explicit_css().or_else(|| match head.width {
ColumnWidth::Wide => Some(Cow::Owned(format!("width: {}", rem(wide_floor(head))))),
_ => head
.width
.default_percent()
.map(|nominal| default_width_style(scaled_default_percent(nominal, total))),
})
}
fn rem(units: Units) -> String {
let fraction = units % UNITS_PER_REM;
if fraction == 0 {
return format!("{}rem", units / UNITS_PER_REM);
}
let fraction = format!("{:04}", fraction * 625);
let fraction = fraction.trim_end_matches('0');
format!("{}.{fraction}rem", units / UNITS_PER_REM)
}
#[derive(Default)]
struct MinWidth {
percent: Vec<u8>,
units: Units,
}
impl MinWidth {
fn share(&mut self, percent: u8) {
self.percent.push(percent);
}
fn length(&mut self, units: Units) {
self.units += units;
}
fn column(&mut self, head: &ColumnHead<'_>, total: u32) {
match head.width {
ColumnWidth::Wide => self.length(wide_floor(head)),
ColumnWidth::Narrow => {
self.share(scaled_default_percent(NARROW_DEFAULT_PERCENT, total))
}
ColumnWidth::Rem(rem) => self.length(Units::from(rem) * UNITS_PER_REM),
ColumnWidth::Percent(share) => self.share(share),
}
}
fn style(&self) -> Option<Cow<'static, str>> {
(self.units > 0).then(|| {
let mut parts: Vec<String> = self
.percent
.iter()
.map(|share| format!("{share}%"))
.collect();
parts.push(rem(self.units));
if parts.len() == 1 {
Cow::Owned(format!("min-width: {}", parts[0]))
} else {
Cow::Owned(format!("min-width: calc({})", parts.join(" + ")))
}
})
}
}
#[cfg(test)]
mod tests;