use super::*;
use crate::dom::node::Kind;
use crate::dom::{Arena, apply};
fn enc_u32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_le_bytes());
}
fn enc_f32(out: &mut Vec<u8>, v: f32) {
out.extend_from_slice(&v.to_le_bytes());
}
fn enc_f64(out: &mut Vec<u8>, v: f64) {
out.extend_from_slice(&v.to_le_bytes());
}
fn enc_bool(out: &mut Vec<u8>, v: bool) {
out.push(u8::from(v));
}
fn enc_str(out: &mut Vec<u8>, s: &str) {
enc_u32(out, s.len() as u32);
out.extend_from_slice(s.as_bytes());
}
fn enc_kind(out: &mut Vec<u8>, k: Kind) {
out.push(match k {
Kind::Root => 0,
Kind::Box => 1,
Kind::Text => 2,
Kind::VirtualText => 3,
});
}
fn enc_attr(out: &mut Vec<u8>, v: &AttrValue) {
match v {
AttrValue::Bool(b) => {
out.push(0);
enc_bool(out, *b);
}
AttrValue::Str(s) => {
out.push(1);
enc_str(out, s);
}
AttrValue::Number(n) => {
out.push(2);
enc_f64(out, *n);
}
}
}
fn enc_dim(out: &mut Vec<u8>, d: &Dim) {
match d {
Dim::Points(p) => {
out.push(0);
enc_f32(out, *p);
}
Dim::Percent(p) => {
out.push(1);
enc_f32(out, *p);
}
Dim::Auto => out.push(2),
}
}
fn enc_lp(out: &mut Vec<u8>, l: &Lp) {
match l {
Lp::Points(p) => {
out.push(0);
enc_f32(out, *p);
}
Lp::Percent(p) => {
out.push(1);
enc_f32(out, *p);
}
}
}
fn enc_position(out: &mut Vec<u8>, p: Position) {
out.push(match p {
Position::Relative => 0,
Position::Absolute => 1,
Position::Static => 2,
});
}
fn enc_flex_dir(out: &mut Vec<u8>, v: FlexDir) {
out.push(match v {
FlexDir::Row => 0,
FlexDir::Column => 1,
FlexDir::RowReverse => 2,
FlexDir::ColumnReverse => 3,
});
}
fn enc_flex_wrap(out: &mut Vec<u8>, v: FlexWrap) {
out.push(match v {
FlexWrap::NoWrap => 0,
FlexWrap::Wrap => 1,
FlexWrap::WrapReverse => 2,
});
}
fn enc_align(out: &mut Vec<u8>, v: Align) {
out.push(match v {
Align::Stretch => 0,
Align::FlexStart => 1,
Align::Center => 2,
Align::FlexEnd => 3,
Align::Baseline => 4,
});
}
fn enc_content_align(out: &mut Vec<u8>, v: ContentAlign) {
out.push(match v {
ContentAlign::FlexStart => 0,
ContentAlign::Center => 1,
ContentAlign::FlexEnd => 2,
ContentAlign::SpaceBetween => 3,
ContentAlign::SpaceAround => 4,
ContentAlign::SpaceEvenly => 5,
ContentAlign::Stretch => 6,
});
}
fn enc_display(out: &mut Vec<u8>, v: Display) {
out.push(match v {
Display::Flex => 0,
Display::None => 1,
});
}
fn enc_text_wrap(out: &mut Vec<u8>, v: TextWrap) {
out.push(match v {
TextWrap::Wrap => 0,
TextWrap::Hard => 1,
TextWrap::TruncateEnd => 2,
TextWrap::TruncateMiddle => 3,
TextWrap::TruncateStart => 4,
});
}
fn enc_overflow(out: &mut Vec<u8>, v: Overflow) {
out.push(match v {
Overflow::Visible => 0,
Overflow::Hidden => 1,
});
}
fn enc_border_style(out: &mut Vec<u8>, b: &BorderStyle) {
match b {
BorderStyle::Named(s) => {
out.push(0);
enc_str(out, s);
}
BorderStyle::Custom {
top_left,
top,
top_right,
right,
bottom_right,
bottom,
bottom_left,
left,
} => {
out.push(1);
for s in [
top_left,
top,
top_right,
right,
bottom_right,
bottom,
bottom_left,
left,
] {
enc_str(out, s);
}
}
}
}
#[allow(clippy::too_many_lines)]
fn enc_style(out: &mut Vec<u8>, s: &Style) {
let mut body = Vec::new();
let mut n: u32 = 0;
macro_rules! dim_field {
($id:expr, $opt:expr) => {
if let Some(v) = &$opt {
body.push($id);
enc_dim(&mut body, v);
n += 1;
}
};
}
macro_rules! lp_field {
($id:expr, $opt:expr) => {
if let Some(v) = &$opt {
body.push($id);
enc_lp(&mut body, v);
n += 1;
}
};
}
macro_rules! f32_field {
($id:expr, $opt:expr) => {
if let Some(v) = $opt {
body.push($id);
enc_f32(&mut body, v);
n += 1;
}
};
}
macro_rules! bool_field {
($id:expr, $opt:expr) => {
if let Some(v) = $opt {
body.push($id);
enc_bool(&mut body, v);
n += 1;
}
};
}
macro_rules! str_field {
($id:expr, $opt:expr) => {
if let Some(v) = &$opt {
body.push($id);
enc_str(&mut body, v);
n += 1;
}
};
}
macro_rules! tag_field {
($id:expr, $opt:expr, $f:ident) => {
if let Some(v) = $opt {
body.push($id);
$f(&mut body, v);
n += 1;
}
};
}
tag_field!(0, s.position, enc_position);
dim_field!(1, s.top);
dim_field!(2, s.right);
dim_field!(3, s.bottom);
dim_field!(4, s.left);
lp_field!(5, s.margin);
lp_field!(6, s.margin_x);
lp_field!(7, s.margin_y);
lp_field!(8, s.margin_top);
lp_field!(9, s.margin_right);
lp_field!(10, s.margin_bottom);
lp_field!(11, s.margin_left);
lp_field!(12, s.padding);
lp_field!(13, s.padding_x);
lp_field!(14, s.padding_y);
lp_field!(15, s.padding_top);
lp_field!(16, s.padding_right);
lp_field!(17, s.padding_bottom);
lp_field!(18, s.padding_left);
tag_field!(19, s.flex_direction, enc_flex_dir);
tag_field!(20, s.flex_wrap, enc_flex_wrap);
f32_field!(21, s.flex_grow);
f32_field!(22, s.flex_shrink);
dim_field!(23, s.flex_basis);
tag_field!(24, s.align_items, enc_align);
tag_field!(25, s.align_self, enc_align);
tag_field!(26, s.align_content, enc_content_align);
tag_field!(27, s.justify_content, enc_content_align);
dim_field!(28, s.width);
dim_field!(29, s.height);
dim_field!(30, s.min_width);
dim_field!(31, s.min_height);
dim_field!(32, s.max_width);
dim_field!(33, s.max_height);
f32_field!(34, s.aspect_ratio);
tag_field!(35, s.display, enc_display);
if let Some(v) = &s.border_style {
body.push(36);
enc_border_style(&mut body, v);
n += 1;
}
bool_field!(37, s.border_top);
bool_field!(38, s.border_right);
bool_field!(39, s.border_bottom);
bool_field!(40, s.border_left);
f32_field!(41, s.gap);
f32_field!(42, s.column_gap);
f32_field!(43, s.row_gap);
tag_field!(44, s.text_wrap, enc_text_wrap);
tag_field!(45, s.overflow_x, enc_overflow);
tag_field!(46, s.overflow_y, enc_overflow);
str_field!(47, s.background_color);
str_field!(48, s.border_color);
str_field!(49, s.border_top_color);
str_field!(50, s.border_right_color);
str_field!(51, s.border_bottom_color);
str_field!(52, s.border_left_color);
str_field!(53, s.border_background_color);
str_field!(54, s.border_top_background_color);
str_field!(55, s.border_right_background_color);
str_field!(56, s.border_bottom_background_color);
str_field!(57, s.border_left_background_color);
bool_field!(58, s.border_dim_color);
bool_field!(59, s.border_top_dim_color);
bool_field!(60, s.border_right_dim_color);
bool_field!(61, s.border_bottom_dim_color);
bool_field!(62, s.border_left_dim_color);
enc_u32(out, n);
out.extend_from_slice(&body);
}
fn enc_op(op: &Op) -> Vec<u8> {
let mut out = Vec::new();
match op {
Op::Create { id, kind } => {
out.push(0x00);
enc_u32(&mut out, *id);
enc_kind(&mut out, *kind);
}
Op::AppendChild { parent, child } => {
out.push(0x01);
enc_u32(&mut out, *parent);
enc_u32(&mut out, *child);
}
Op::InsertBefore {
parent,
child,
before,
} => {
out.push(0x02);
enc_u32(&mut out, *parent);
enc_u32(&mut out, *child);
enc_u32(&mut out, *before);
}
Op::RemoveChild { parent, child } => {
out.push(0x03);
enc_u32(&mut out, *parent);
enc_u32(&mut out, *child);
}
Op::SetText { id, text } => {
out.push(0x04);
enc_u32(&mut out, *id);
enc_str(&mut out, text);
}
Op::SetStyle { id, style } => {
out.push(0x05);
enc_u32(&mut out, *id);
enc_style(&mut out, style);
}
Op::SetAttribute { id, key, value } => {
out.push(0x06);
enc_u32(&mut out, *id);
enc_str(&mut out, key);
enc_attr(&mut out, value);
}
Op::SetTransform { id, has } => {
out.push(0x07);
enc_u32(&mut out, *id);
enc_bool(&mut out, *has);
}
Op::SetStatic { id, value } => {
out.push(0x08);
enc_u32(&mut out, *id);
enc_bool(&mut out, *value);
}
Op::Hide { id } => {
out.push(0x09);
enc_u32(&mut out, *id);
}
Op::Unhide { id } => {
out.push(0x0A);
enc_u32(&mut out, *id);
}
Op::Free { id } => {
out.push(0x0B);
enc_u32(&mut out, *id);
}
Op::SetTextStyle { id, style } => {
out.push(0x0C);
enc_u32(&mut out, *id);
enc_text_style(&mut out, style);
}
Op::ClearTextStyle { id } => {
out.push(0x0D);
enc_u32(&mut out, *id);
}
}
out
}
fn enc_text_style(out: &mut Vec<u8>, ts: &TextStyle) {
let mut fields: Vec<(u8, Vec<u8>)> = Vec::new();
if let Some(c) = &ts.color {
let mut v = Vec::new();
enc_str(&mut v, c);
fields.push((0, v));
}
if let Some(c) = &ts.background_color {
let mut v = Vec::new();
enc_str(&mut v, c);
fields.push((1, v));
}
let mut bool_field = |id: u8, b: bool| {
let mut v = Vec::new();
enc_bool(&mut v, b);
fields.push((id, v));
};
bool_field(2, ts.bold);
bool_field(3, ts.italic);
bool_field(4, ts.underline);
bool_field(5, ts.strikethrough);
bool_field(6, ts.inverse);
bool_field(7, ts.dim_color);
enc_u32(out, fields.len() as u32);
for (id, v) in fields {
out.push(id);
out.extend_from_slice(&v);
}
}
fn enc_ops(ops: &[Op]) -> Vec<u8> {
let mut out = Vec::new();
for op in ops {
out.extend_from_slice(&enc_op(op));
}
out
}
fn dense_style() -> Style {
Style {
position: Some(Position::Static),
top: Some(Dim::Points(1.5)),
right: Some(Dim::Percent(50.0)),
bottom: Some(Dim::Auto),
left: Some(Dim::Points(-2.0)),
margin: Some(Lp::Points(1.0)),
margin_x: Some(Lp::Percent(10.0)),
margin_y: Some(Lp::Points(2.0)),
margin_top: Some(Lp::Points(3.0)),
margin_right: Some(Lp::Points(4.0)),
margin_bottom: Some(Lp::Points(5.0)),
margin_left: Some(Lp::Percent(25.0)),
padding: Some(Lp::Points(6.0)),
padding_x: Some(Lp::Points(7.0)),
padding_y: Some(Lp::Points(8.0)),
padding_top: Some(Lp::Points(9.0)),
padding_right: Some(Lp::Points(10.0)),
padding_bottom: Some(Lp::Points(11.0)),
padding_left: Some(Lp::Percent(33.0)),
flex_direction: Some(FlexDir::ColumnReverse),
flex_wrap: Some(FlexWrap::WrapReverse),
flex_grow: Some(2.5),
flex_shrink: Some(0.5),
flex_basis: Some(Dim::Percent(75.0)),
align_items: Some(Align::Baseline),
align_self: Some(Align::Center),
align_content: Some(ContentAlign::SpaceEvenly),
justify_content: Some(ContentAlign::SpaceAround),
width: Some(Dim::Points(80.0)),
height: Some(Dim::Percent(100.0)),
min_width: Some(Dim::Points(10.0)),
min_height: Some(Dim::Auto),
max_width: Some(Dim::Points(120.0)),
max_height: Some(Dim::Percent(90.0)),
aspect_ratio: Some(1.777),
display: Some(Display::None),
border_style: Some(BorderStyle::Custom {
top_left: "╔".into(),
top: "═".into(),
top_right: "╗".into(),
right: "║".into(),
bottom_right: "╝".into(),
bottom: "═".into(),
bottom_left: "╚".into(),
left: "║".into(),
}),
border_top: Some(true),
border_right: Some(false),
border_bottom: Some(true),
border_left: Some(false),
gap: Some(1.0),
column_gap: Some(2.0),
row_gap: Some(3.0),
text_wrap: Some(TextWrap::TruncateMiddle),
overflow_x: Some(Overflow::Hidden),
overflow_y: Some(Overflow::Visible),
background_color: Some("#001122".into()),
border_color: Some("red".into()),
border_top_color: Some("green".into()),
border_right_color: Some("blue".into()),
border_bottom_color: Some("yellow".into()),
border_left_color: Some("magenta".into()),
border_background_color: Some("cyan".into()),
border_top_background_color: Some("red".into()),
border_right_background_color: Some("green".into()),
border_bottom_background_color: Some("blue".into()),
border_left_background_color: Some("yellow".into()),
border_dim_color: Some(true),
border_top_dim_color: Some(false),
border_right_dim_color: Some(true),
border_bottom_dim_color: Some(false),
border_left_dim_color: Some(true),
}
}
fn all_op_variants() -> Vec<Op> {
vec![
Op::Create {
id: 0,
kind: Kind::Root,
},
Op::Create {
id: 1,
kind: Kind::Box,
},
Op::Create {
id: 2,
kind: Kind::Text,
},
Op::Create {
id: 3,
kind: Kind::VirtualText,
},
Op::AppendChild {
parent: 0,
child: 1,
},
Op::InsertBefore {
parent: 0,
child: 2,
before: 1,
},
Op::RemoveChild {
parent: 0,
child: 2,
},
Op::SetText {
id: 2,
text: "héllo \u{1F600} wörld".into(),
},
Op::SetText {
id: 2,
text: String::new(),
},
Op::SetStyle {
id: 1,
style: Box::new(dense_style()),
},
Op::SetStyle {
id: 1,
style: Box::new(Style::default()),
},
Op::SetStyle {
id: 1,
style: Box::new(Style {
border_style: Some(BorderStyle::Named("round".into())),
width: Some(Dim::Points(40.0)),
..Style::default()
}),
},
Op::SetAttribute {
id: 1,
key: "onClick".into(),
value: AttrValue::Bool(true),
},
Op::SetAttribute {
id: 1,
key: "label".into(),
value: AttrValue::Str("press me".into()),
},
Op::SetAttribute {
id: 1,
key: "count".into(),
value: AttrValue::Number(42.5),
},
Op::SetAttribute {
id: 1,
key: "neg".into(),
value: AttrValue::Number(-0.0),
},
Op::SetTransform { id: 1, has: true },
Op::SetTransform { id: 1, has: false },
Op::SetStatic { id: 1, value: true },
Op::SetStatic {
id: 1,
value: false,
},
Op::Hide { id: 1 },
Op::Unhide { id: 1 },
Op::Free { id: 3 },
]
}
#[test]
fn round_trip_every_op_variant_individually() {
for op in all_op_variants() {
let bytes = enc_op(&op);
let decoded = decode_ops(&bytes).expect("valid single-op buffer decodes");
assert_eq!(decoded, vec![op.clone()], "round-trip mismatch for {op:?}");
}
}
#[test]
fn round_trip_full_batch() {
let ops = all_op_variants();
let bytes = enc_ops(&ops);
let decoded = decode_ops(&bytes).expect("valid batch decodes");
assert_eq!(decoded, ops);
}
#[test]
fn empty_buffer_decodes_to_empty_vec() {
assert_eq!(decode_ops(&[]).unwrap(), Vec::<Op>::new());
}
#[test]
fn dense_style_round_trips_all_fields() {
let op = Op::SetStyle {
id: 7,
style: Box::new(dense_style()),
};
let decoded = decode_ops(&enc_op(&op)).unwrap();
assert_eq!(decoded, vec![op]);
}
#[test]
fn decoded_ops_apply_to_arena() {
let ops = all_op_variants();
let decoded = decode_ops(&enc_ops(&ops)).unwrap();
let mut arena = Arena::new();
apply(&mut arena, &decoded);
assert!(arena.get(0).is_some());
assert!(arena.get(1).is_some());
assert!(arena.get(3).is_none(), "id 3 was Freed");
}
#[test]
fn truncation_at_every_offset_never_panics() {
let bytes = enc_ops(&all_op_variants());
for len in 0..bytes.len() {
let prefix = &bytes[..len];
let _ = decode_ops(prefix);
}
assert!(decode_ops(&bytes).is_ok());
}
#[test]
fn truncated_mid_primitive_is_unexpected_eof() {
let op = Op::Create {
id: 0x0102_0304,
kind: Kind::Box,
};
let bytes = enc_op(&op);
assert_eq!(decode_ops(&bytes[..3]), Err(DecodeError::UnexpectedEof));
}
#[test]
fn truncated_string_length_overruns_to_eof() {
let mut bytes = vec![0x04];
bytes.extend_from_slice(&7u32.to_le_bytes()); bytes.extend_from_slice(&100u32.to_le_bytes()); bytes.extend_from_slice(b"short"); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnexpectedEof));
}
#[test]
fn unknown_opcode_rejected() {
assert_eq!(decode_ops(&[0xFF]), Err(DecodeError::UnknownOpcode(0xFF)));
let mut bytes = enc_op(&Op::Hide { id: 1 });
bytes.push(0x7E);
assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownOpcode(0x7E)));
}
#[test]
fn unknown_kind_tag_rejected() {
let mut bytes = vec![0x00]; bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.push(0x09); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownTag(0x09)));
}
#[test]
fn unknown_attr_tag_rejected() {
let mut bytes = vec![0x06]; bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(b'k');
bytes.push(0x09); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownTag(0x09)));
}
#[test]
fn unknown_style_field_id_rejected() {
let mut bytes = vec![0x05]; bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(0xFF); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownFieldId(0xFF)));
}
#[test]
fn unknown_dim_tag_rejected() {
let mut bytes = vec![0x05]; bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.push(28); bytes.push(0x09); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownTag(0x09)));
}
#[test]
fn unknown_border_style_tag_rejected() {
let mut bytes = vec![0x05];
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.push(36); bytes.push(0x09); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnknownTag(0x09)));
}
#[test]
fn invalid_bool_byte_rejected() {
let mut bytes = vec![0x07]; bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.push(0x02); assert_eq!(decode_ops(&bytes), Err(DecodeError::InvalidBool(0x02)));
}
#[test]
fn invalid_utf8_string_rejected() {
let mut bytes = vec![0x04]; bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&2u32.to_le_bytes()); bytes.extend_from_slice(&[0xFF, 0xFE]); assert_eq!(decode_ops(&bytes), Err(DecodeError::InvalidUtf8));
}
#[test]
fn style_field_count_overrun_is_eof_not_panic() {
let mut bytes = vec![0x05];
bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&5u32.to_le_bytes()); assert_eq!(decode_ops(&bytes), Err(DecodeError::UnexpectedEof));
}
#[test]
fn garbage_buffer_never_panics() {
for seed in 0u32..256 {
let mut x = seed.wrapping_mul(2_654_435_761);
let len = (seed as usize) % 64;
let mut bytes = Vec::with_capacity(len);
for _ in 0..len {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
bytes.push((x & 0xFF) as u8);
}
let _ = decode_ops(&bytes);
}
}
#[test]
fn decode_error_is_std_error_and_displays() {
let e: &dyn std::error::Error = &DecodeError::UnknownOpcode(0xAB);
assert!(e.to_string().contains("0xAB"));
}
const fn le(id: u32) -> [u8; 4] {
id.to_le_bytes()
}
#[test]
fn literal_bytes_pin_every_opcode() {
let cases: Vec<(Vec<u8>, Op)> = vec![
(
[&[0x00][..], &le(1), &[0x01]].concat(),
Op::Create {
id: 1,
kind: Kind::Box,
},
),
(
[&[0x01][..], &le(2), &le(3)].concat(),
Op::AppendChild {
parent: 2,
child: 3,
},
),
(
[&[0x02][..], &le(4), &le(5), &le(6)].concat(),
Op::InsertBefore {
parent: 4,
child: 5,
before: 6,
},
),
(
[&[0x03][..], &le(7), &le(8)].concat(),
Op::RemoveChild {
parent: 7,
child: 8,
},
),
(
[&[0x04][..], &le(9), &le(2), b"hi"].concat(),
Op::SetText {
id: 9,
text: "hi".into(),
},
),
(
[&[0x05][..], &le(10), &le(0)].concat(),
Op::SetStyle {
id: 10,
style: Box::new(Style::default()),
},
),
(
[
&[0x06][..],
&le(11),
&le(1),
b"k",
&[0x02][..],
&1.0f64.to_le_bytes(),
]
.concat(),
Op::SetAttribute {
id: 11,
key: "k".into(),
value: AttrValue::Number(1.0),
},
),
(
[&[0x07][..], &le(12), &[0x01]].concat(),
Op::SetTransform { id: 12, has: true },
),
(
[&[0x08][..], &le(13), &[0x00]].concat(),
Op::SetStatic {
id: 13,
value: false,
},
),
([&[0x09][..], &le(14)].concat(), Op::Hide { id: 14 }),
([&[0x0A][..], &le(15)].concat(), Op::Unhide { id: 15 }),
([&[0x0B][..], &le(16)].concat(), Op::Free { id: 16 }),
];
for (bytes, expected) in cases {
assert_eq!(
decode_ops(&bytes).unwrap(),
vec![expected.clone()],
"literal opcode fixture decoded wrong: {expected:?}"
);
}
}
#[test]
fn literal_bytes_pin_attr_value_and_border_variants() {
let bool_attr = [&[0x06][..], &le(1), &le(1), b"b", &[0x00, 0x01]].concat();
assert_eq!(
decode_ops(&bool_attr).unwrap(),
vec![Op::SetAttribute {
id: 1,
key: "b".into(),
value: AttrValue::Bool(true)
}]
);
let str_attr = [
&[0x06][..],
&le(1),
&le(1),
b"s",
&[0x01][..],
&le(2),
b"yo",
]
.concat();
assert_eq!(
decode_ops(&str_attr).unwrap(),
vec![Op::SetAttribute {
id: 1,
key: "s".into(),
value: AttrValue::Str("yo".into())
}]
);
let named = [
&[0x05][..],
&le(1),
&le(1), &[36, 0x00][..], &le(3),
"abc".as_bytes(),
]
.concat();
assert_eq!(
decode_ops(&named).unwrap(),
vec![Op::SetStyle {
id: 1,
style: Box::new(Style {
border_style: Some(BorderStyle::Named("abc".into())),
..Style::default()
})
}]
);
}
#[test]
fn literal_bytes_pin_all_style_field_ids() {
type Case = (u8, Vec<u8>, fn(&mut Style));
let dim_points = |v: f32| [&[0x00u8][..], &v.to_le_bytes()].concat(); let lp_points = |v: f32| [&[0x00u8][..], &v.to_le_bytes()].concat();
let cases: Vec<Case> = vec![
(0, vec![0x01], |s| s.position = Some(Position::Absolute)),
(1, dim_points(1.0), |s| s.top = Some(Dim::Points(1.0))),
(2, dim_points(2.0), |s| s.right = Some(Dim::Points(2.0))),
(3, dim_points(3.0), |s| s.bottom = Some(Dim::Points(3.0))),
(4, dim_points(4.0), |s| s.left = Some(Dim::Points(4.0))),
(5, lp_points(5.0), |s| s.margin = Some(Lp::Points(5.0))),
(6, lp_points(6.0), |s| s.margin_x = Some(Lp::Points(6.0))),
(7, lp_points(7.0), |s| s.margin_y = Some(Lp::Points(7.0))),
(8, lp_points(8.0), |s| s.margin_top = Some(Lp::Points(8.0))),
(9, lp_points(9.0), |s| {
s.margin_right = Some(Lp::Points(9.0))
}),
(10, lp_points(10.0), |s| {
s.margin_bottom = Some(Lp::Points(10.0))
}),
(11, lp_points(11.0), |s| {
s.margin_left = Some(Lp::Points(11.0))
}),
(12, lp_points(12.0), |s| s.padding = Some(Lp::Points(12.0))),
(13, lp_points(13.0), |s| {
s.padding_x = Some(Lp::Points(13.0))
}),
(14, lp_points(14.0), |s| {
s.padding_y = Some(Lp::Points(14.0))
}),
(15, lp_points(15.0), |s| {
s.padding_top = Some(Lp::Points(15.0))
}),
(16, lp_points(16.0), |s| {
s.padding_right = Some(Lp::Points(16.0))
}),
(17, lp_points(17.0), |s| {
s.padding_bottom = Some(Lp::Points(17.0))
}),
(18, lp_points(18.0), |s| {
s.padding_left = Some(Lp::Points(18.0))
}),
(19, vec![0x01], |s| s.flex_direction = Some(FlexDir::Column)),
(20, vec![0x01], |s| s.flex_wrap = Some(FlexWrap::Wrap)),
(21, 2.0f32.to_le_bytes().to_vec(), |s| {
s.flex_grow = Some(2.0)
}),
(22, 0.5f32.to_le_bytes().to_vec(), |s| {
s.flex_shrink = Some(0.5)
}),
(23, dim_points(23.0), |s| {
s.flex_basis = Some(Dim::Points(23.0))
}),
(24, vec![0x02], |s| s.align_items = Some(Align::Center)),
(25, vec![0x04], |s| s.align_self = Some(Align::Baseline)),
(26, vec![0x03], |s| {
s.align_content = Some(ContentAlign::SpaceBetween)
}),
(27, vec![0x01], |s| {
s.justify_content = Some(ContentAlign::Center)
}),
(28, dim_points(28.0), |s| s.width = Some(Dim::Points(28.0))),
(29, dim_points(29.0), |s| s.height = Some(Dim::Points(29.0))),
(30, dim_points(30.0), |s| {
s.min_width = Some(Dim::Points(30.0))
}),
(31, dim_points(31.0), |s| {
s.min_height = Some(Dim::Points(31.0))
}),
(32, dim_points(32.0), |s| {
s.max_width = Some(Dim::Points(32.0))
}),
(33, dim_points(33.0), |s| {
s.max_height = Some(Dim::Points(33.0))
}),
(34, 1.5f32.to_le_bytes().to_vec(), |s| {
s.aspect_ratio = Some(1.5)
}),
(35, vec![0x01], |s| s.display = Some(Display::None)),
(36, [&[0x00u8][..], &le(1), b"x"].concat(), |s| {
s.border_style = Some(BorderStyle::Named("x".into()))
}),
(37, vec![0x01], |s| s.border_top = Some(true)),
(38, vec![0x00], |s| s.border_right = Some(false)),
(39, vec![0x01], |s| s.border_bottom = Some(true)),
(40, vec![0x00], |s| s.border_left = Some(false)),
(41, 1.0f32.to_le_bytes().to_vec(), |s| s.gap = Some(1.0)),
(42, 2.0f32.to_le_bytes().to_vec(), |s| {
s.column_gap = Some(2.0)
}),
(43, 3.0f32.to_le_bytes().to_vec(), |s| s.row_gap = Some(3.0)),
(44, vec![0x01], |s| s.text_wrap = Some(TextWrap::Hard)),
(45, vec![0x01], |s| s.overflow_x = Some(Overflow::Hidden)),
(46, vec![0x00], |s| s.overflow_y = Some(Overflow::Visible)),
(47, [&le(1)[..], b"a"].concat(), |s| {
s.background_color = Some("a".into())
}),
(48, [&le(1)[..], b"b"].concat(), |s| {
s.border_color = Some("b".into())
}),
(49, [&le(1)[..], b"c"].concat(), |s| {
s.border_top_color = Some("c".into())
}),
(50, [&le(1)[..], b"d"].concat(), |s| {
s.border_right_color = Some("d".into())
}),
(51, [&le(1)[..], b"e"].concat(), |s| {
s.border_bottom_color = Some("e".into())
}),
(52, [&le(1)[..], b"f"].concat(), |s| {
s.border_left_color = Some("f".into())
}),
(53, [&le(1)[..], b"g"].concat(), |s| {
s.border_background_color = Some("g".into())
}),
(54, [&le(1)[..], b"h"].concat(), |s| {
s.border_top_background_color = Some("h".into())
}),
(55, [&le(1)[..], b"i"].concat(), |s| {
s.border_right_background_color = Some("i".into())
}),
(56, [&le(1)[..], b"j"].concat(), |s| {
s.border_bottom_background_color = Some("j".into())
}),
(57, [&le(1)[..], b"k"].concat(), |s| {
s.border_left_background_color = Some("k".into())
}),
(58, vec![0x01], |s| s.border_dim_color = Some(true)),
(59, vec![0x00], |s| s.border_top_dim_color = Some(false)),
(60, vec![0x01], |s| s.border_right_dim_color = Some(true)),
(61, vec![0x00], |s| s.border_bottom_dim_color = Some(false)),
(62, vec![0x01], |s| s.border_left_dim_color = Some(true)),
];
let mut ids: Vec<u8> = cases.iter().map(|(id, _, _)| *id).collect();
ids.sort_unstable();
assert_eq!(
ids,
(0u8..=62).collect::<Vec<_>>(),
"field-id table has a gap or dup"
);
for (field_id, value_bytes, mutate) in cases {
let mut bytes = vec![0x05u8];
bytes.extend_from_slice(&le(1));
bytes.extend_from_slice(&le(1)); bytes.push(field_id);
bytes.extend_from_slice(&value_bytes);
let mut expected = Style::default();
mutate(&mut expected);
assert_eq!(
decode_ops(&bytes).unwrap(),
vec![Op::SetStyle {
id: 1,
style: Box::new(expected)
}],
"style field id {field_id} decoded to the wrong field"
);
}
}
#[test]
fn literal_bytes_pin_set_text_style() {
let bytes = [
&[0x0C][..], &le(7), &le(2), &[0x00][..], &le(3), b"red", &[0x02, 0x01][..], ]
.concat();
let expected = Op::SetTextStyle {
id: 7,
style: TextStyle {
color: Some("red".into()),
bold: true,
..TextStyle::default()
},
};
assert_eq!(
decode_ops(&bytes).unwrap(),
vec![expected],
"SET_TEXT_STYLE literal fixture decoded wrong"
);
let bad_field = [
&[0x0C][..],
&le(7),
&le(1), &[0x09][..], &[0x01][..],
]
.concat();
assert_eq!(
decode_ops(&bad_field),
Err(DecodeError::UnknownFieldId(0x09))
);
}
#[test]
fn set_text_style_truncated_is_unexpected_eof() {
let full = [
&[0x0C][..],
&le(7),
&le(2),
&[0x00][..],
&le(3),
b"red",
&[0x02, 0x01][..],
]
.concat();
assert!(decode_ops(&full).is_ok());
assert_eq!(decode_ops(&[]), Ok(vec![]));
for cut in 1..full.len() {
assert_eq!(
decode_ops(&full[..cut]),
Err(DecodeError::UnexpectedEof),
"prefix of length {cut} should be UnexpectedEof"
);
}
}
#[test]
fn set_text_style_round_trips_via_enc() {
let ops = vec![
Op::SetTextStyle {
id: 1,
style: TextStyle {
color: Some("green".into()),
background_color: Some("#000".into()),
bold: true,
italic: false,
underline: true,
strikethrough: false,
inverse: true,
dim_color: false,
},
},
Op::SetTextStyle {
id: 2,
style: TextStyle::default(),
},
];
let bytes = enc_ops(&ops);
assert_eq!(decode_ops(&bytes).unwrap(), ops);
let mut arena = Arena::new();
apply(
&mut arena,
&[Op::Create {
id: 1,
kind: Kind::Text,
}],
);
apply(&mut arena, &decode_ops(&bytes).unwrap()[..1]);
assert_eq!(
arena.get(1).unwrap().text_styling,
Some(TextStyle {
color: Some("green".into()),
background_color: Some("#000".into()),
bold: true,
underline: true,
inverse: true,
..TextStyle::default()
})
);
}
#[test]
fn literal_bytes_pin_clear_text_style() {
let bytes = [&[0x0D][..], &le(7)].concat();
assert_eq!(
decode_ops(&bytes).unwrap(),
vec![Op::ClearTextStyle { id: 7 }],
"CLEAR_TEXT_STYLE literal fixture decoded wrong"
);
assert_eq!(decode_ops(&[0x0D]), Err(DecodeError::UnexpectedEof));
}
#[test]
fn clear_text_style_resets_node_to_none() {
let mut arena = Arena::new();
apply(
&mut arena,
&[
Op::Create {
id: 1,
kind: Kind::Text,
},
Op::SetTextStyle {
id: 1,
style: TextStyle {
color: Some("red".into()),
bold: true,
..TextStyle::default()
},
},
],
);
assert!(
arena.get(1).unwrap().text_styling.is_some(),
"precondition: node carries the native style after SetTextStyle"
);
let bytes = enc_ops(&[Op::ClearTextStyle { id: 1 }]);
apply(&mut arena, &decode_ops(&bytes).unwrap());
assert_eq!(
arena.get(1).unwrap().text_styling,
None,
"ClearTextStyle must reset text_styling to None (P6.2 styled→plain fix)"
);
assert!(
!arena.get(1).unwrap().has_transform,
"ClearTextStyle leaves has_transform untouched"
);
}