use velox_dom::layout::{DEFAULT_ROOT_FONT_SIZE, Rect};
use velox_dom::style::ComputedStyle;
pub const DEFAULT_INPUT_TEXT_PADDING: f32 = 4.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InputTextMetrics {
pub pad_left: f32,
pub pad_top: f32,
pub pad_right: f32,
pub pad_bottom: f32,
pub text_left: f32,
pub content_right: f32,
pub border_left: f32,
pub border_top: f32,
pub border_right: f32,
pub border_bottom: f32,
pub padding_left: f32,
pub padding_right: f32,
pub padding_top: f32,
pub padding_bottom: f32,
pub font_size: f32,
}
impl InputTextMetrics {
pub fn text_width(&self) -> f32 {
(self.content_right - self.text_left).max(0.0)
}
pub fn content_height(&self) -> f32 {
(self.pad_bottom - self.pad_top).max(0.0)
}
pub fn padding_box(&self) -> (f32, f32, f32, f32) {
(self.pad_left, self.pad_top, self.pad_right, self.pad_bottom)
}
}
pub fn input_text_metrics(
style: Option<&str>,
border_box: Rect,
viewport: (f32, f32),
inherited_font_size: f32,
) -> InputTextMetrics {
let mut computed = ComputedStyle::new();
if let Some(s) = style
&& !s.trim().is_empty()
{
computed.apply_inline_style(s);
}
let font_size = resolve_font_size(style, &computed, inherited_font_size, viewport);
let px = |len: &velox_dom::style::Length| -> f32 {
let v = len.to_px(font_size, DEFAULT_ROOT_FONT_SIZE, viewport);
if v.is_finite() && v > 0.0 { v } else { 0.0 }
};
let mut padding_left = px(&computed.padding.left);
let mut padding_right = px(&computed.padding.right);
let padding_top = px(&computed.padding.top);
let padding_bottom = px(&computed.padding.bottom);
if !declares_padding(style) {
padding_left = DEFAULT_INPUT_TEXT_PADDING;
padding_right = DEFAULT_INPUT_TEXT_PADDING;
}
let bw = |len: &velox_dom::style::Length, style_side: velox_dom::style::BorderStyle| -> f32 {
if matches!(
style_side,
velox_dom::style::BorderStyle::None | velox_dom::style::BorderStyle::Hidden
) {
return 0.0;
}
px(len)
};
let border_left = bw(&computed.border.width.left, computed.border.style.left);
let border_right = bw(&computed.border.width.right, computed.border.style.right);
let border_top = bw(&computed.border.width.top, computed.border.style.top);
let border_bottom = bw(&computed.border.width.bottom, computed.border.style.bottom);
let left = border_box.x as f32;
let top = border_box.y as f32;
let right = left + border_box.w.max(0) as f32;
let bottom = top + border_box.h.max(0) as f32;
let pad_left = (left + border_left).min(right);
let pad_right = (right - border_right).max(pad_left);
let pad_top = (top + border_top).min(bottom);
let pad_bottom = (bottom - border_bottom).max(pad_top);
let text_left = (pad_left + padding_left).min(pad_right);
let text_right = (pad_right - padding_right).max(text_left);
InputTextMetrics {
pad_left,
pad_top,
pad_right,
pad_bottom,
text_left,
content_right: text_right,
border_left,
border_top,
border_right,
border_bottom,
padding_left,
padding_right,
padding_top,
padding_bottom,
font_size,
}
}
fn resolve_font_size(
style: Option<&str>,
computed: &ComputedStyle,
inherited_font_size: f32,
viewport: (f32, f32),
) -> f32 {
if !declares_font_size(style) {
return inherited_font_size;
}
let own = computed
.font_size
.to_px(inherited_font_size, DEFAULT_ROOT_FONT_SIZE, viewport);
if own.is_finite() && own > 0.0 {
own.max(1.0)
} else {
inherited_font_size
}
}
fn declares_font_size(style: Option<&str>) -> bool {
declared_keys(style).any(|k| k.eq_ignore_ascii_case("font-size"))
}
fn declares_padding(style: Option<&str>) -> bool {
declared_keys(style).any(|k| {
k.eq_ignore_ascii_case("padding")
|| k.eq_ignore_ascii_case("padding-top")
|| k.eq_ignore_ascii_case("padding-right")
|| k.eq_ignore_ascii_case("padding-bottom")
|| k.eq_ignore_ascii_case("padding-left")
})
}
fn declared_keys(style: Option<&str>) -> impl Iterator<Item = String> + '_ {
style
.into_iter()
.flat_map(|s| s.split(';'))
.filter_map(|d| {
d.trim()
.split_once(':')
.map(|(k, _)| k.trim().to_ascii_lowercase())
})
}
#[cfg(test)]
mod tests {
use super::*;
const BOX: Rect = Rect {
x: 20,
y: 30,
w: 200,
h: 40,
};
const VIEW: (f32, f32) = (800.0, 600.0);
fn m(style: Option<&str>) -> InputTextMetrics {
input_text_metrics(style, BOX, VIEW, 14.0)
}
#[test]
fn no_style_falls_back_to_the_documented_padding() {
let r = m(None);
assert_eq!(r.text_left, 20.0 + DEFAULT_INPUT_TEXT_PADDING);
assert_eq!(r.content_right, 220.0 - DEFAULT_INPUT_TEXT_PADDING);
assert_eq!(r.text_width(), 200.0 - 2.0 * DEFAULT_INPUT_TEXT_PADDING);
assert_eq!(r.font_size, 14.0);
}
#[test]
fn border_width_deflates_the_padding_box() {
let r = m(Some("border: 2px solid red"));
assert_eq!(r.border_left, 2.0);
assert_eq!(r.pad_left, 22.0);
assert_eq!(r.pad_right, 218.0);
assert_eq!(r.text_left, 26.0);
assert_eq!(r.pad_bottom, 68.0);
}
#[test]
fn none_border_style_occupies_no_space() {
let r = m(Some("border: 3px none red"));
assert_eq!(r.border_left, 0.0);
assert_eq!(r.pad_left, 20.0);
}
#[test]
fn shorthand_and_longhand_padding_agree() {
let shorthand = m(Some("border: 1px solid #333; padding: 10px 12px"));
let longhand = m(Some(
"border: 1px solid #333; padding-left: 12px; padding-right: 12px",
));
let inline_left_only = m(Some("border: 1px solid #333; padding-left: 12px"));
assert_eq!(shorthand.text_left, longhand.text_left);
assert_eq!(shorthand.text_left, inline_left_only.text_left);
assert_eq!(shorthand.content_right, longhand.content_right);
assert_eq!(shorthand.padding_left, inline_left_only.padding_left);
assert_eq!(inline_left_only.padding_right, 0.0);
assert_eq!(shorthand.padding_right, 12.0);
}
#[test]
fn the_shorthand_is_what_the_old_lookup_missed() {
let r = m(Some("border: 1px solid #333; padding: 10px 12px"));
assert_eq!(r.padding_left, 12.0);
assert_eq!(r.padding_right, 12.0);
assert_eq!(r.padding_top, 10.0);
assert_eq!(r.padding_bottom, 10.0);
assert_eq!(r.text_left, 21.0 + 12.0);
}
#[test]
fn four_value_shorthand() {
let r = m(Some("padding: 1px 2px 3px 4px"));
assert_eq!(r.padding_top, 1.0);
assert_eq!(r.padding_right, 2.0);
assert_eq!(r.padding_bottom, 3.0);
assert_eq!(r.padding_left, 4.0);
assert_eq!(r.text_left, BOX.x as f32 + 4.0);
assert_eq!(r.content_right, BOX.x as f32 + BOX.w as f32 - 2.0);
}
#[test]
fn explicit_zero_padding_is_honoured() {
let r = m(Some("padding: 0"));
assert_eq!(r.text_left, 20.0);
assert_eq!(r.content_right, 220.0);
}
#[test]
fn em_padding_resolves_against_the_elements_own_font_size() {
let r = input_text_metrics(Some("font-size: 20px; padding: 0 1em"), BOX, VIEW, 14.0);
assert_eq!(r.font_size, 20.0);
assert_eq!(r.padding_left, 20.0);
assert_eq!(r.text_left, 40.0);
}
#[test]
fn font_size_declared_in_em_resolves_against_the_parent() {
let r = input_text_metrics(Some("font-size: 2em"), BOX, VIEW, 10.0);
assert_eq!(r.font_size, 20.0);
}
#[test]
fn degenerate_font_size_falls_back_to_the_inherited_one() {
for bad in ["font-size: 0", "font-size: -4px", "font-size: auto"] {
let r = m(Some(bad));
assert_eq!(r.font_size, 14.0, "{bad} must not change the font size");
}
}
#[test]
fn content_right_never_falls_left_of_text_left() {
let r = m(Some("padding: 40px 120px"));
assert!(r.content_right >= r.text_left, "{r:?}");
assert_eq!(r.text_width(), 0.0);
assert_eq!(r.text_left, BOX.x as f32 + 120.0);
assert_eq!(r.content_right, r.text_left);
let roomy = m(Some("padding: 40px 60px"));
assert_eq!(roomy.text_width(), 200.0 - 120.0);
}
#[test]
fn degenerate_box_does_not_produce_nan() {
let zero = Rect {
x: 0,
y: 0,
w: 0,
h: 0,
};
let r = input_text_metrics(
Some("border: 2px solid red; padding: 4px"),
zero,
VIEW,
14.0,
);
assert!(r.text_left.is_finite());
assert!(r.content_right.is_finite());
assert_eq!(r.text_left, r.content_right);
}
}