use crate::compat::{format, String};
use crate::core::Rect;
pub const DECORATION_GAP: u32 = 4;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DecorationSlots {
pub prefix: String,
pub suffix: String,
pub helper: String,
pub error: String,
pub counter: String,
}
impl DecorationSlots {
pub fn new() -> Self {
Self::default()
}
pub fn has_error(&self) -> bool {
!self.error.is_empty()
}
pub fn is_empty(&self) -> bool {
self.prefix.is_empty()
&& self.suffix.is_empty()
&& self.helper.is_empty()
&& self.error.is_empty()
&& self.counter.is_empty()
}
pub fn support_message(&self) -> &str {
if self.has_error() {
&self.error
} else {
&self.helper
}
}
pub fn needs_support_row(&self) -> bool {
!self.support_message().is_empty() || !self.counter.is_empty()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DecorationMetrics {
pub prefix_width: u32,
pub suffix_width: u32,
}
impl DecorationMetrics {
pub fn measure(slots: &DecorationSlots, measure: impl Fn(&str) -> u32) -> Self {
Self {
prefix_width: if slots.prefix.is_empty() { 0 } else { measure(&slots.prefix) },
suffix_width: if slots.suffix.is_empty() { 0 } else { measure(&slots.suffix) },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DecorationLayout {
pub prefix: Option<Rect>,
pub value: Rect,
pub suffix: Option<Rect>,
pub counter: Option<Rect>,
pub support: Option<Rect>,
pub support_text: Option<Rect>,
}
impl DecorationLayout {
pub fn compute(
field: Rect,
padding_h: u32,
line_height: u32,
gap: u32,
metrics: DecorationMetrics,
counter_width: u32,
slots: &DecorationSlots,
) -> Self {
let padding = padding_h as i32;
let gap = gap as i32;
let inner_left = field.x + padding;
let inner_right = field.x + field.width as i32 - padding;
let mut cursor = inner_left;
let prefix = if metrics.prefix_width > 0 {
let width = metrics.prefix_width as i32;
let box_ = Rect::new(cursor, field.y, metrics.prefix_width, field.height);
cursor += width + gap;
Some(box_)
} else {
None
};
let suffix = if metrics.suffix_width > 0 {
let width = metrics.suffix_width as i32;
let x = inner_right - width;
let box_ = Rect::new(x, field.y, metrics.suffix_width, field.height);
cursor += 0; Some((box_, x - gap))
} else {
None
};
let (suffix_box, value_right) = match suffix {
Some((box_, limit)) => (Some(box_), limit),
None => (None, inner_right),
};
let value_width = (value_right - cursor).max(0) as u32;
let value = Rect::new(cursor, field.y, value_width, field.height);
let has_counter = !slots.counter.is_empty() && counter_width > 0;
let support = if slots.needs_support_row() {
Some(Rect::new(field.x, field.y + field.height as i32 + gap, field.width, line_height))
} else {
None
};
let (support_text, counter_box) = match support {
None => (None, None),
Some(row) => {
let counter_box = if has_counter {
Some(Rect::new(
row.x + row.width as i32 - counter_width as i32,
row.y,
counter_width,
row.height,
))
} else {
None
};
(Some(row), counter_box)
}
};
Self { prefix, value, suffix: suffix_box, counter: counter_box, support, support_text }
}
pub fn total_height(field_height: u32, line_height: u32, gap: u32, has_support: bool) -> u32 {
if has_support {
field_height.saturating_add(gap).saturating_add(line_height)
} else {
field_height
}
}
pub fn counter_text(length: usize, max_length: Option<usize>) -> Option<String> {
max_length.map(|max| format!("{length}/{max}"))
}
pub fn over_limit(length: usize, max_length: Option<usize>) -> usize {
match max_length {
Some(max) => length.saturating_sub(max),
None => 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn measure(text: &str) -> u32 {
text.chars().count() as u32 * 8
}
#[test]
fn an_empty_decoration_set_costs_nothing() {
let slots = DecorationSlots::new();
assert!(slots.is_empty());
assert!(!slots.needs_support_row());
assert!(!slots.has_error());
assert_eq!(slots.support_message(), "");
let metrics = DecorationMetrics::measure(&slots, |_| 999);
assert_eq!(metrics.prefix_width, 0, "an unset prefix must not be measured");
assert_eq!(metrics.suffix_width, 0, "an unset suffix must not be measured");
let field = Rect::new(10, 20, 200, 24);
let layout = DecorationLayout::compute(field, 6, 16, 2, metrics, 0, &slots);
assert_eq!(layout.prefix, None);
assert_eq!(layout.suffix, None);
assert_eq!(layout.support, None);
assert_eq!(layout.counter, None);
assert_eq!(layout.value, Rect::new(16, 20, 188, 24));
}
#[test]
fn the_prefix_shifts_the_value_and_not_the_other_way_round() {
let slots = DecorationSlots { prefix: "$".into(), ..Default::default() };
let metrics = DecorationMetrics::measure(&slots, measure);
assert_eq!(metrics.prefix_width, 8);
let field = Rect::new(10, 20, 200, 24);
let layout = DecorationLayout::compute(field, 6, 16, 2, metrics, 0, &slots);
assert_eq!(layout.prefix, Some(Rect::new(16, 20, 8, 24)));
assert_eq!(layout.value.x, 16 + 8 + 2, "the value begins after the prefix and its gap");
assert_eq!(layout.suffix, None);
assert!(layout.prefix.unwrap().right() <= layout.value.x, "the boxes must not overlap");
assert_eq!(layout.value.width, 188 - 8 - 2);
}
#[test]
fn the_suffix_is_anchored_to_the_trailing_edge() {
let slots = DecorationSlots { suffix: "%".into(), ..Default::default() };
let metrics = DecorationMetrics::measure(&slots, measure);
assert_eq!(metrics.suffix_width, 8);
let field = Rect::new(10, 20, 200, 24);
let layout = DecorationLayout::compute(field, 6, 16, 2, metrics, 0, &slots);
let suffix = layout.suffix.expect("a suffix is set");
assert_eq!(suffix, Rect::new(10 + 200 - 6 - 8, 20, 8, 24));
assert!(
layout.value.right() <= suffix.x,
"the value {:?} runs under the suffix {suffix:?}",
layout.value
);
assert_eq!(layout.value.x, 16);
}
#[test]
fn both_slots_bracket_the_value_without_overlapping_it() {
let slots =
DecorationSlots { prefix: "$".into(), suffix: "%".into(), ..Default::default() };
let metrics = DecorationMetrics::measure(&slots, measure);
let layout =
DecorationLayout::compute(Rect::new(0, 0, 120, 24), 6, 16, 2, metrics, 0, &slots);
let prefix = layout.prefix.expect("a prefix is set");
let suffix = layout.suffix.expect("a suffix is set");
assert!(prefix.right() <= layout.value.x, "{prefix:?} overlaps {:?}", layout.value);
assert!(layout.value.right() <= suffix.x, "{:?} overlaps {suffix:?}", layout.value);
assert!(layout.value.width > 0, "the value must keep room in a 120px field");
}
#[test]
fn a_field_narrower_than_its_slots_clamps_the_value_to_zero() {
let slots =
DecorationSlots { prefix: "aa".into(), suffix: "bb".into(), ..Default::default() };
let metrics = DecorationMetrics::measure(&slots, measure);
assert_eq!(metrics.prefix_width, 16);
let layout =
DecorationLayout::compute(Rect::new(0, 0, 40, 24), 6, 16, 2, metrics, 0, &slots);
assert_eq!(layout.value.width, 0, "a negative width must clamp, not underflow");
}
#[test]
fn an_error_displaces_the_helper_but_not_the_counter() {
let slots = DecorationSlots {
helper: "Enter a URL".into(),
error: "Not a URL".into(),
counter: "12/40".into(),
..Default::default()
};
assert!(slots.has_error());
assert_eq!(slots.support_message(), "Not a URL", "the error wins the row");
let counter_width = measure("12/40");
let layout = DecorationLayout::compute(
Rect::new(0, 0, 200, 24),
6,
16,
2,
DecorationMetrics::default(),
counter_width,
&slots,
);
let row = layout.support.expect("there is a message and a counter");
assert_eq!(row.y, 24 + 2, "the row sits below the field, past the gap");
assert_eq!(row.height, 16);
assert_eq!(row.x, 0, "the row starts at the field's own leading edge");
assert_eq!(row.width, 200);
let counter = layout.counter.expect("the counter shares the row, not displaced");
assert_eq!(counter.width, counter_width);
assert_eq!(counter.right(), row.right(), "the counter is anchored to the trailing edge");
assert_eq!(layout.support_text.expect("a message box"), row);
}
#[test]
fn a_counter_alone_still_occupies_the_row() {
let slots = DecorationSlots { counter: "3/10".into(), ..Default::default() };
assert!(slots.needs_support_row());
assert_eq!(slots.support_message(), "", "there is no message to show");
let layout = DecorationLayout::compute(
Rect::new(0, 0, 100, 20),
4,
12,
3,
DecorationMetrics::default(),
measure("3/10"),
&slots,
);
assert!(layout.support.is_some(), "the counter alone reserves the row");
assert!(layout.counter.is_some());
}
#[test]
fn the_support_row_follows_what_is_actually_set() {
let mut slots = DecorationSlots { helper: "Hint".into(), ..Default::default() };
assert!(slots.needs_support_row());
assert_eq!(slots.support_message(), "Hint");
slots.error = "Bad".into();
assert_eq!(slots.support_message(), "Bad");
slots.error.clear();
assert_eq!(slots.support_message(), "Hint", "clearing the error restores the helper");
slots.helper.clear();
assert!(!slots.needs_support_row(), "no message and no counter means no row");
}
#[test]
fn the_reserved_height_follows_the_support_row() {
assert_eq!(DecorationLayout::total_height(24, 16, 2, false), 24);
assert_eq!(DecorationLayout::total_height(24, 16, 2, true), 42, "field + gap + line");
}
#[test]
fn the_support_row_never_overlaps_the_field() {
let slots = DecorationSlots { helper: "Hint".into(), ..Default::default() };
let field = Rect::new(5, 5, 100, 20);
let layout =
DecorationLayout::compute(field, 4, 12, 3, DecorationMetrics::default(), 0, &slots);
let row = layout.support.unwrap();
assert!(
row.y >= field.y + field.height as i32,
"the row at {row:?} overlaps the field {field:?}"
);
}
#[test]
fn the_counter_text_comes_from_the_length_and_the_limit() {
assert_eq!(DecorationLayout::counter_text(0, None), None, "no limit means no counter");
assert_eq!(DecorationLayout::counter_text(12, Some(40)).as_deref(), Some("12/40"));
assert_eq!(DecorationLayout::counter_text(0, Some(5)).as_deref(), Some("0/5"));
}
#[test]
fn an_over_long_value_reports_how_far_over_it_is() {
assert_eq!(DecorationLayout::over_limit(3, Some(5)), 0, "within the limit");
assert_eq!(DecorationLayout::over_limit(5, Some(5)), 0, "exactly at the limit is not over");
assert_eq!(DecorationLayout::over_limit(8, Some(5)), 3, "three characters too long");
assert_eq!(DecorationLayout::over_limit(8, None), 0, "no limit can be exceeded");
}
}