use std::fmt;
use std::str::FromStr;
use binrw::binrw;
use super::id::{ResourceId, ResourceRef};
#[binrw]
#[brw(little)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RawValue {
#[br(temp)]
#[bw(calc = 8)]
_size: u16,
#[br(temp)]
#[bw(calc = 0)]
_reserved: u8,
pub data_type: u8,
pub data: u32,
}
impl RawValue {
pub const SIZE: usize = 8;
pub const fn new(data_type: u8, data: u32) -> Self {
Self { data_type, data }
}
pub fn read(bytes: &[u8]) -> Option<Self> {
let bytes = bytes.get(..Self::SIZE)?;
Some(Self::new(bytes[3], u32::from_le_bytes(bytes[4..8].try_into().ok()?)))
}
}
pub(crate) mod data_type {
pub const NULL: u8 = 0x00;
pub const REFERENCE: u8 = 0x01;
pub const ATTRIBUTE: u8 = 0x02;
pub const STRING: u8 = 0x03;
pub const FLOAT: u8 = 0x04;
pub const DIMENSION: u8 = 0x05;
pub const FRACTION: u8 = 0x06;
pub const INT_DEC: u8 = 0x10;
pub const INT_HEX: u8 = 0x11;
pub const INT_BOOLEAN: u8 = 0x12;
pub const INT_COLOR_ARGB8: u8 = 0x1c;
pub const INT_COLOR_RGB8: u8 = 0x1d;
pub const INT_COLOR_ARGB4: u8 = 0x1e;
pub const INT_COLOR_RGB4: u8 = 0x1f;
pub const DATA_NULL_EMPTY: u32 = 1;
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Value {
String(String),
Bool(bool),
Int(i32),
Hex(u32),
Float(f32),
Dimension(Dimension),
Fraction(Fraction),
Color(Color),
Reference(ResourceRef),
Attribute(ResourceRef),
Null,
Empty,
Raw {
data_type: u8,
data: u32,
},
}
impl Value {
pub(crate) fn from_raw<E>(raw: RawValue, string: impl FnOnce(u32) -> Result<String, E>) -> Result<Self, E> {
use data_type::*;
let data = raw.data;
Ok(match raw.data_type {
NULL if data == DATA_NULL_EMPTY => Self::Empty,
REFERENCE if data == 0 => Self::Null,
REFERENCE => Self::Reference(ResourceRef::Id(ResourceId(data))),
ATTRIBUTE => Self::Attribute(ResourceRef::Id(ResourceId(data))),
STRING => Self::String(string(data)?),
FLOAT => Self::Float(f32::from_bits(data)),
DIMENSION => Self::Dimension(Dimension(data)),
FRACTION => Self::Fraction(Fraction(data)),
INT_DEC => Self::Int(data as i32),
INT_HEX => Self::Hex(data),
INT_BOOLEAN => Self::Bool(data != 0),
INT_COLOR_ARGB8 => Self::Color(Color { argb: data, format: ColorFormat::Argb8 }),
INT_COLOR_RGB8 => Self::Color(Color { argb: data, format: ColorFormat::Rgb8 }),
INT_COLOR_ARGB4 => Self::Color(Color { argb: data, format: ColorFormat::Argb4 }),
INT_COLOR_RGB4 => Self::Color(Color { argb: data, format: ColorFormat::Rgb4 }),
data_type => Self::Raw { data_type, data },
})
}
pub(crate) fn to_raw<'v, E>(
&'v self,
string: impl FnOnce(&'v str) -> u32,
resolve: impl FnOnce(&ResourceRef) -> Result<ResourceId, E>,
) -> Result<RawValue, E> {
use data_type::*;
Ok(match self {
Self::String(s) => RawValue::new(STRING, string(s)),
Self::Bool(b) => RawValue::new(INT_BOOLEAN, if *b { u32::MAX } else { 0 }),
Self::Int(i) => RawValue::new(INT_DEC, *i as u32),
Self::Hex(h) => RawValue::new(INT_HEX, *h),
Self::Float(f) => RawValue::new(FLOAT, f.to_bits()),
Self::Dimension(d) => RawValue::new(DIMENSION, d.0),
Self::Fraction(f) => RawValue::new(FRACTION, f.0),
Self::Color(c) => RawValue::new(c.format.data_type(), c.argb),
Self::Reference(r) => RawValue::new(REFERENCE, resolve(r)?.0),
Self::Attribute(r) => RawValue::new(ATTRIBUTE, resolve(r)?.0),
Self::Null => RawValue::new(REFERENCE, 0),
Self::Empty => RawValue::new(NULL, DATA_NULL_EMPTY),
Self::Raw { data_type, data } => RawValue::new(*data_type, *data),
})
}
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match *self {
Self::Int(i) => Some(i.into()),
Self::Hex(h) => Some(h.into()),
Self::Bool(b) => Some(b.into()),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match *self {
Self::Bool(b) => Some(b),
_ => None,
}
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Self::String(s.to_owned())
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Self::Bool(b)
}
}
impl From<i32> for Value {
fn from(i: i32) -> Self {
Self::Int(i)
}
}
impl From<ResourceRef> for Value {
fn from(r: ResourceRef) -> Self {
Self::Reference(r)
}
}
impl From<Color> for Value {
fn from(c: Color) -> Self {
Self::Color(c)
}
}
impl From<Dimension> for Value {
fn from(d: Dimension) -> Self {
Self::Dimension(d)
}
}
impl From<Fraction> for Value {
fn from(f: Fraction) -> Self {
Self::Fraction(f)
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::String(s) => f.write_str(s),
Self::Bool(b) => write!(f, "{b}"),
Self::Int(i) => write!(f, "{i}"),
Self::Hex(h) => write!(f, "0x{h:08x}"),
Self::Float(x) => write!(f, "{x:?}"),
Self::Dimension(d) => write!(f, "{d}"),
Self::Fraction(x) => write!(f, "{x}"),
Self::Color(c) => write!(f, "{c}"),
Self::Reference(r) => write!(f, "{r}"),
Self::Attribute(r) => write!(f, "?{}", r.to_string().trim_start_matches('@')),
Self::Null => f.write_str("@null"),
Self::Empty => f.write_str("@empty"),
Self::Raw { data_type, data } => write!(f, "(type 0x{data_type:02x})0x{data:08x}"),
}
}
}
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum LiteralError {
#[error("{text:?} is not a color (#RGB, #ARGB, #RRGGBB, or #AARRGGBB)")]
Color { text: String },
#[error("{text:?} is not a dimension such as `16dp`")]
Dimension { text: String },
#[error("{text:?} is not a fraction such as `50%` or `50%p`")]
Fraction { text: String },
#[error("{value} is not finite with a magnitude below 2^23, so it has no complex encoding")]
OutOfRange { value: f32 },
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum ColorFormat {
#[default]
Argb8,
Rgb8,
Argb4,
Rgb4,
}
impl ColorFormat {
const fn data_type(self) -> u8 {
match self {
Self::Argb8 => data_type::INT_COLOR_ARGB8,
Self::Rgb8 => data_type::INT_COLOR_RGB8,
Self::Argb4 => data_type::INT_COLOR_ARGB4,
Self::Rgb4 => data_type::INT_COLOR_RGB4,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Color {
pub argb: u32,
pub format: ColorFormat,
}
impl Color {
pub const fn argb(argb: u32) -> Self {
Self { argb, format: ColorFormat::Argb8 }
}
pub const fn rgb(rgb: u32) -> Self {
Self { argb: 0xff00_0000 | (rgb & 0x00ff_ffff), format: ColorFormat::Rgb8 }
}
}
impl FromStr for Color {
type Err = LiteralError;
fn from_str(text: &str) -> Result<Self, LiteralError> {
let invalid = || LiteralError::Color { text: text.to_owned() };
let hex =
text.strip_prefix('#').filter(|hex| hex.bytes().all(|b| b.is_ascii_hexdigit())).ok_or_else(invalid)?;
let value = u32::from_str_radix(hex, 16).map_err(|_| invalid())?;
let widen = |v: u32| (0..4).rev().fold(0, |acc, i| (acc << 8) | (((v >> (i * 4)) & 0xf) * 0x11));
Ok(match hex.len() {
3 => Self { argb: 0xff00_0000 | widen(value), format: ColorFormat::Rgb4 },
4 => Self { argb: widen(value), format: ColorFormat::Argb4 },
6 => Self { argb: 0xff00_0000 | value, format: ColorFormat::Rgb8 },
8 => Self { argb: value, format: ColorFormat::Argb8 },
_ => return Err(invalid()),
})
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let opaque = self.argb >> 24 == 0xff;
let short = (self.argb & 0x0f0f_0f0f) == ((self.argb >> 4) & 0x0f0f_0f0f);
match self.format {
ColorFormat::Rgb8 if opaque => write!(f, "#{:06x}", self.argb & 0x00ff_ffff),
ColorFormat::Argb4 | ColorFormat::Rgb4 if short && (self.format == ColorFormat::Argb4 || opaque) => {
let short = (0..4).rev().fold(0, |value, i| (value << 4) | ((self.argb >> (i * 8 + 4)) & 0xf));
match self.format {
ColorFormat::Argb4 => write!(f, "#{short:04x}"),
_ => write!(f, "#{:03x}", short & 0xfff),
}
}
_ => write!(f, "#{:08x}", self.argb),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum DimensionUnit {
Px,
Dp,
Sp,
Pt,
In,
Mm,
}
impl DimensionUnit {
const ALL: [(Self, &'static str); 6] =
[(Self::Px, "px"), (Self::Dp, "dp"), (Self::Sp, "sp"), (Self::Pt, "pt"), (Self::In, "in"), (Self::Mm, "mm")];
fn suffix(self) -> &'static str {
Self::ALL[self as usize].1
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Dimension(u32);
impl Dimension {
pub fn new(value: f32, unit: DimensionUnit) -> Result<Self, LiteralError> {
Ok(Self(complex(value)? | unit as u32))
}
pub fn value(self) -> f32 {
complex_value(self.0)
}
pub fn unit(self) -> Option<DimensionUnit> {
DimensionUnit::ALL.get((self.0 & UNIT_MASK) as usize).map(|(unit, _)| *unit)
}
}
impl FromStr for Dimension {
type Err = LiteralError;
fn from_str(text: &str) -> Result<Self, LiteralError> {
let invalid = || LiteralError::Dimension { text: text.to_owned() };
let (unit, number) = DimensionUnit::ALL
.into_iter()
.chain([(DimensionUnit::Dp, "dip")])
.find_map(|(unit, suffix)| text.strip_suffix(suffix).map(|number| (unit, number)))
.ok_or_else(invalid)?;
Self::new(number.trim().parse().map_err(|_| invalid())?, unit)
}
}
impl fmt::Display for Dimension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.unit() {
Some(unit) => write!(f, "{}{}", self.value(), unit.suffix()),
None => write!(f, "{}(unit {})", self.value(), self.0 & UNIT_MASK),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Fraction(u32);
impl Fraction {
pub fn new(value: f32, relative_to_parent: bool) -> Result<Self, LiteralError> {
Ok(Self(complex(value)? | u32::from(relative_to_parent)))
}
pub fn value(self) -> f32 {
complex_value(self.0)
}
pub fn relative_to_parent(self) -> bool {
self.0 & UNIT_MASK == 1
}
}
impl FromStr for Fraction {
type Err = LiteralError;
fn from_str(text: &str) -> Result<Self, LiteralError> {
let invalid = || LiteralError::Fraction { text: text.to_owned() };
let (number, relative_to_parent) = match text.strip_suffix("%p") {
Some(number) => (number, true),
None => (text.strip_suffix('%').ok_or_else(invalid)?, false),
};
let percent: f32 = number.trim().parse().map_err(|_| invalid())?;
Self::new(percent / 100.0, relative_to_parent)
}
}
impl fmt::Display for Fraction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}%{}", self.value() * 100.0, if self.relative_to_parent() { "p" } else { "" })
}
}
const UNIT_MASK: u32 = 0xf;
const RADIX_SHIFT: u32 = 4;
const MANTISSA_SHIFT: u32 = 8;
const MANTISSA_MASK: u32 = 0x00ff_ffff;
const COMPLEX_LIMIT: f32 = (1 << 23) as f32;
fn complex_value(data: u32) -> f32 {
const MANTISSA_MULT: f32 = 1.0 / (1 << MANTISSA_SHIFT) as f32;
const RADIX_MULTS: [f32; 4] = [
MANTISSA_MULT,
MANTISSA_MULT / (1 << 7) as f32,
MANTISSA_MULT / (1 << 15) as f32,
MANTISSA_MULT / (1 << 23) as f32,
];
let mantissa = (data & (MANTISSA_MASK << MANTISSA_SHIFT)) as i32;
mantissa as f32 * RADIX_MULTS[((data >> RADIX_SHIFT) & 0x3) as usize]
}
fn complex(value: f32) -> Result<u32, LiteralError> {
if !value.is_finite() || value.abs() >= COMPLEX_LIMIT {
return Err(LiteralError::OutOfRange { value });
}
let bits = (value.abs() * (1u32 << 23) as f32 + 0.5) as u64;
let (radix, shift) = match bits {
_ if bits & 0x7f_ffff == 0 => (0, 23),
_ if bits & !0x7f_ffff == 0 => (3, 0),
_ if bits & !0x7fff_ffff == 0 => (2, 8),
_ if bits & !0x7f_ffff_ffff == 0 => (1, 16),
_ => (0, 23),
};
let mut mantissa = ((bits >> shift) as u32) & MANTISSA_MASK;
if value < 0.0 {
mantissa = mantissa.wrapping_neg() & MANTISSA_MASK;
}
Ok((radix << RADIX_SHIFT) | (mantissa << MANTISSA_SHIFT))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn complex_numbers_round_trip() {
for value in [0.0, 1.0, 16.0, 0.5, -3.25, 1234.5, 0.001, 8_388_607.0, -8_388_607.0] {
let dimension = Dimension::new(value, DimensionUnit::Dp).unwrap();
assert!((dimension.value() - value).abs() < 1e-3, "{value} decoded as {}", dimension.value());
assert_eq!(dimension.unit(), Some(DimensionUnit::Dp));
}
assert_eq!(Dimension::new(16.0, DimensionUnit::Dp).unwrap().0, 0x0000_1001);
}
#[test]
fn unrepresentable_numbers_are_rejected() {
for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 8_388_608.0, -8_388_608.0, 1e30] {
assert!(
matches!(Dimension::new(value, DimensionUnit::Dp), Err(LiteralError::OutOfRange { .. })),
"{value}"
);
}
assert!(matches!("1000000000%".parse::<Fraction>(), Err(LiteralError::OutOfRange { .. })));
}
#[test]
fn decoded_values_re_encode_exactly() {
let raw = RawValue::new(data_type::DIMENSION, 0x0000_100f);
let value = Value::from_raw::<()>(raw, |_| unreachable!()).unwrap();
assert_eq!(value.to_raw::<()>(|_| unreachable!(), |_| unreachable!()).unwrap(), raw);
}
#[test]
fn parses_literals() {
let argb = |text: &str| text.parse::<Color>().unwrap().argb;
assert_eq!(argb("#f00"), 0xffff_0000);
assert_eq!(argb("#8f00"), 0x88ff_0000);
assert_eq!(argb("#123456"), 0xff12_3456);
assert_eq!(argb("#80123456"), 0x8012_3456);
for format in [ColorFormat::Rgb4, ColorFormat::Argb4, ColorFormat::Rgb8] {
let color = Color { argb: 0x7f12_3456, format };
assert_eq!(color.to_string(), "#7f123456", "unrepresentable formats must preserve the ARGB value");
}
for invalid in ["#12345", "#+1234", "123456"] {
assert!(matches!(invalid.parse::<Color>(), Err(LiteralError::Color { .. })), "{invalid}");
}
assert_eq!("4.5dip".parse::<Dimension>().unwrap(), Dimension::new(4.5, DimensionUnit::Dp).unwrap());
for (text, value) in [("1e3dp", 1000.0), ("-2.5E-1dp", -0.25), ("1e+2dip", 100.0)] {
assert_eq!(text.parse::<Dimension>().unwrap(), Dimension::new(value, DimensionUnit::Dp).unwrap());
}
assert!(matches!("4.5parsecs".parse::<Dimension>(), Err(LiteralError::Dimension { .. })));
let fraction: Fraction = "50%p".parse().unwrap();
assert_eq!((fraction.value(), fraction.relative_to_parent()), (0.5, true));
}
#[test]
fn null_empty_and_undefined_are_distinct() {
for value in [Value::Empty, Value::Null] {
let raw = value.to_raw::<()>(|_| 0, |_| unreachable!()).unwrap();
assert_eq!(Value::from_raw::<()>(raw, |_| unreachable!()).unwrap(), value);
}
assert_eq!(
Value::Null.to_raw::<()>(|_| 0, |_| unreachable!()).unwrap(),
RawValue::new(data_type::REFERENCE, 0)
);
let undefined = RawValue::new(data_type::NULL, 0);
assert_eq!(Value::from_raw::<()>(undefined, |_| unreachable!()).unwrap(), Value::Raw { data_type: 0, data: 0 });
}
}