Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(ui): flex, grid, inset coverage

Lewis: May this revision serve well! <lu5a@proton.me>

+580
+580
crates/bone-ui/src/layout/tests.rs
··· 1 + #![allow(dead_code, unused_imports)] 2 + use core::num::{NonZeroU16, NonZeroU32, NonZeroU64}; 3 + 4 + use super::axis::{Axis, CrossAxisAlign, MainAxisJustify}; 5 + use super::dock::{ 6 + DockNode, DockState, DockStateError, FloatingSurface, PanelId, SplitFraction, TabIndex, 7 + }; 8 + use super::engine::{LayoutError, NodeKind, SolvedLayout, measure}; 9 + use super::geometry::{EdgeInsets, LayoutPos, LayoutPx, LayoutPxError, LayoutSize}; 10 + use super::paint::{PaintCommand, paint_plan}; 11 + use super::primitives::{DockPanel, GridChild, Layout}; 12 + use super::retained::{RetainedLayout, ScrollOffset}; 13 + use super::scroll::{ScrollAxes, clamp_scroll}; 14 + use super::splitter::{SplitterMove, SplitterStep, apply_keyboard_move, fraction_from_drag}; 15 + use super::track::{FlexWeight, GridLine, GridLineRef, GridSpan, GridTrack, TrackName, TrackSize}; 16 + use crate::theme::{Spacing, Theme}; 17 + use crate::widget_id::WidgetId; 18 + 19 + fn wid(n: u64) -> WidgetId { 20 + WidgetId::from_raw(NonZeroU64::new(n).unwrap_or(NonZeroU64::MIN)) 21 + } 22 + 23 + fn pid(n: u32) -> PanelId { 24 + PanelId::new(NonZeroU32::new(n).unwrap_or(NonZeroU32::MIN)) 25 + } 26 + 27 + fn gl(n: u16) -> GridLine { 28 + GridLine::new(NonZeroU16::new(n).unwrap_or(NonZeroU16::MIN)) 29 + } 30 + 31 + fn fw(n: u16) -> FlexWeight { 32 + FlexWeight::new(NonZeroU16::new(n).unwrap_or(NonZeroU16::MIN)) 33 + } 34 + 35 + fn size(w: f32, h: f32) -> LayoutSize { 36 + LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)) 37 + } 38 + 39 + fn sp(px: f32) -> Spacing { 40 + Spacing::px(px) 41 + } 42 + 43 + fn solve(layout: &Layout, available: LayoutSize, retained: &RetainedLayout) -> SolvedLayout { 44 + measure(layout, available, retained).unwrap_or_else(|e| panic!("measure failed: {e:?}")) 45 + } 46 + 47 + const PX_EPSILON: f32 = 1e-3; 48 + 49 + fn approx_eq(a: f32, b: f32, tol: f32) -> bool { 50 + (a - b).abs() <= tol 51 + } 52 + 53 + #[test] 54 + fn row_with_gap_distributes_children_along_main_axis() { 55 + let layout = Layout::Row { 56 + gap: sp(10.0), 57 + justify: MainAxisJustify::Start, 58 + cross: CrossAxisAlign::Stretch, 59 + children: vec![Layout::leaf(wid(1)), Layout::leaf(wid(2))], 60 + }; 61 + let solved = solve(&layout, size(100.0, 50.0), &RetainedLayout::default()); 62 + let root = solved.root_node(); 63 + assert!(matches!(root.kind, NodeKind::Pass)); 64 + assert!(approx_eq(root.rect.size.width.value(), 100.0, PX_EPSILON)); 65 + let first = solved.node(root.children[0]); 66 + let second = solved.node(root.children[1]); 67 + let gap = second.rect.min_x().value() - first.rect.max_x().value(); 68 + assert!((gap - 10.0).abs() < 1e-3, "gap {gap} should equal 10px"); 69 + assert!(approx_eq(first.rect.min_y().value(), 0.0, PX_EPSILON)); 70 + assert!(approx_eq(first.rect.size.height.value(), 50.0, PX_EPSILON)); 71 + } 72 + 73 + #[test] 74 + fn row_justify_center_centres_content() { 75 + let layout = Layout::Row { 76 + gap: sp(0.0), 77 + justify: MainAxisJustify::Center, 78 + cross: CrossAxisAlign::Center, 79 + children: vec![Layout::Gap { size: sp(40.0) }], 80 + }; 81 + let solved = solve(&layout, size(100.0, 50.0), &RetainedLayout::default()); 82 + let only = solved.node(solved.root_node().children[0]); 83 + let left = only.rect.min_x().value(); 84 + let right = 100.0 - only.rect.max_x().value(); 85 + assert!( 86 + (left - right).abs() < 1.0, 87 + "centred: left={left} right={right}" 88 + ); 89 + } 90 + 91 + #[test] 92 + fn column_stretches_cross_axis() { 93 + let layout = Layout::Column { 94 + gap: sp(0.0), 95 + justify: MainAxisJustify::Start, 96 + cross: CrossAxisAlign::Stretch, 97 + children: vec![Layout::leaf(wid(1)), Layout::leaf(wid(2))], 98 + }; 99 + let solved = solve(&layout, size(80.0, 40.0), &RetainedLayout::default()); 100 + let kids = &solved.root_node().children; 101 + assert_eq!(kids.len(), 2); 102 + assert!(kids.iter().all(|i| approx_eq( 103 + solved.node(*i).rect.size.width.value(), 104 + 80.0, 105 + PX_EPSILON 106 + ))); 107 + assert!(solved.node(kids[1]).rect.min_y().value() >= solved.node(kids[0]).rect.max_y().value()); 108 + } 109 + 110 + #[test] 111 + fn stack_fills_children_to_full_bounds() { 112 + let layout = Layout::Stack { 113 + children: vec![Layout::leaf(wid(1)), Layout::leaf(wid(2))], 114 + }; 115 + let solved = solve(&layout, size(60.0, 40.0), &RetainedLayout::default()); 116 + let kids = &solved.root_node().children; 117 + let a = solved.node(kids[0]); 118 + let b = solved.node(kids[1]); 119 + assert!(approx_eq(a.rect.size.width.value(), 60.0, PX_EPSILON)); 120 + assert!(approx_eq(a.rect.size.height.value(), 40.0, PX_EPSILON)); 121 + assert!(approx_eq(b.rect.size.width.value(), 60.0, PX_EPSILON)); 122 + assert!(approx_eq(b.rect.size.height.value(), 40.0, PX_EPSILON)); 123 + assert_eq!(a.rect.origin, b.rect.origin); 124 + } 125 + 126 + #[test] 127 + fn grid_spans_two_columns() { 128 + let Some(span) = GridSpan::rect( 129 + gl(1), 130 + gl(3), 131 + gl(1), 132 + gl(2), 133 + ) else { 134 + panic!("valid span"); 135 + }; 136 + let layout = Layout::Grid { 137 + columns: vec![ 138 + GridTrack::unnamed(TrackSize::FLEX_1), 139 + GridTrack::unnamed(TrackSize::FLEX_1), 140 + ], 141 + rows: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 142 + column_gap: sp(0.0), 143 + row_gap: sp(0.0), 144 + children: vec![GridChild { 145 + span, 146 + child: Layout::leaf(wid(1)), 147 + }], 148 + }; 149 + let solved = solve(&layout, size(100.0, 30.0), &RetainedLayout::default()); 150 + let cell = solved.node(solved.root_node().children[0]); 151 + assert!( 152 + (cell.rect.size.width.value() - 100.0).abs() < 1e-3, 153 + "spanning two columns must take full width: {}", 154 + cell.rect.size.width.value(), 155 + ); 156 + } 157 + 158 + #[test] 159 + fn grid_resolves_named_tracks() { 160 + const SIDE: TrackName = TrackName::new("side"); 161 + const MID: TrackName = TrackName::new("mid"); 162 + const END: TrackName = TrackName::new("end"); 163 + let span = GridSpan { 164 + column_start: GridLineRef::Name(MID), 165 + column_end: GridLineRef::Name(END), 166 + row_start: GridLineRef::Line(gl(1)), 167 + row_end: GridLineRef::Line(gl(2)), 168 + }; 169 + let layout = Layout::Grid { 170 + columns: vec![ 171 + GridTrack::named(SIDE, TrackSize::FLEX_1), 172 + GridTrack::named(MID, TrackSize::FLEX_1), 173 + GridTrack::named(END, TrackSize::FLEX_1), 174 + ], 175 + rows: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 176 + column_gap: sp(0.0), 177 + row_gap: sp(0.0), 178 + children: vec![GridChild { 179 + span, 180 + child: Layout::leaf(wid(1)), 181 + }], 182 + }; 183 + let solved = solve(&layout, size(120.0, 30.0), &RetainedLayout::default()); 184 + let cell = solved.node(solved.root_node().children[0]); 185 + assert!( 186 + approx_eq(cell.rect.min_x().value(), 40.0, 1.0), 187 + "child should start at col 'mid' (40px): {}", 188 + cell.rect.min_x().value() 189 + ); 190 + assert!( 191 + approx_eq(cell.rect.size.width.value(), 40.0, 1.0), 192 + "child width = one column (40px): {}", 193 + cell.rect.size.width.value() 194 + ); 195 + } 196 + 197 + #[test] 198 + fn grid_unknown_track_name_errors() { 199 + const KNOWN: TrackName = TrackName::new("known"); 200 + const MISSING: TrackName = TrackName::new("missing"); 201 + let span = GridSpan { 202 + column_start: GridLineRef::Name(MISSING), 203 + column_end: GridLineRef::Name(KNOWN), 204 + row_start: GridLineRef::Line(gl(1)), 205 + row_end: GridLineRef::Line(gl(2)), 206 + }; 207 + let layout = Layout::Grid { 208 + columns: vec![GridTrack::named(KNOWN, TrackSize::FLEX_1)], 209 + rows: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 210 + column_gap: sp(0.0), 211 + row_gap: sp(0.0), 212 + children: vec![GridChild { 213 + span, 214 + child: Layout::leaf(wid(1)), 215 + }], 216 + }; 217 + let result = measure(&layout, size(100.0, 30.0), &RetainedLayout::default()); 218 + let Err(LayoutError::UnknownTrackName(name)) = result else { 219 + panic!("expected UnknownTrackName, got {result:?}"); 220 + }; 221 + assert_eq!(name, "missing"); 222 + } 223 + 224 + #[test] 225 + fn inset_subtracts_padding_from_child() { 226 + let layout = Layout::inset(EdgeInsets::all(sp(8.0)), Layout::leaf(wid(1))); 227 + let solved = solve(&layout, size(100.0, 100.0), &RetainedLayout::default()); 228 + let child = solved.node(solved.root_node().children[0]); 229 + assert!(approx_eq(child.rect.min_x().value(), 8.0, PX_EPSILON)); 230 + assert!(approx_eq(child.rect.min_y().value(), 8.0, PX_EPSILON)); 231 + assert!((child.rect.size.width.value() - (100.0 - 16.0)).abs() < 1e-3); 232 + } 233 + 234 + #[test] 235 + fn spacer_consumes_remaining_main_axis_space() { 236 + let layout = Layout::Row { 237 + gap: sp(0.0), 238 + justify: MainAxisJustify::Start, 239 + cross: CrossAxisAlign::Stretch, 240 + children: vec![ 241 + Layout::Gap { size: sp(20.0) }, 242 + Layout::spacer(), 243 + Layout::Gap { size: sp(20.0) }, 244 + ], 245 + }; 246 + let solved = solve(&layout, size(100.0, 10.0), &RetainedLayout::default()); 247 + let middle = solved.node(solved.root_node().children[1]); 248 + assert!((middle.rect.size.width.value() - 60.0).abs() < 1.0); 249 + } 250 + 251 + #[test] 252 + fn grid_span_rect_rejects_non_increasing_lines() { 253 + assert!( 254 + GridSpan::rect( 255 + gl(2), 256 + gl(1), 257 + gl(1), 258 + gl(2) 259 + ) 260 + .is_none() 261 + ); 262 + assert!( 263 + GridSpan::rect( 264 + gl(1), 265 + gl(1), 266 + gl(1), 267 + gl(2) 268 + ) 269 + .is_none() 270 + ); 271 + assert!( 272 + GridSpan::rect( 273 + gl(1), 274 + gl(2), 275 + gl(2), 276 + gl(2) 277 + ) 278 + .is_none() 279 + ); 280 + assert!( 281 + GridSpan::rect( 282 + gl(1), 283 + gl(2), 284 + gl(1), 285 + gl(2) 286 + ) 287 + .is_some() 288 + ); 289 + } 290 + 291 + #[test] 292 + fn layout_px_saturating_replaces_non_finite_with_zero() { 293 + assert!(approx_eq( 294 + LayoutPx::saturating(f32::NAN).value(), 295 + 0.0, 296 + PX_EPSILON 297 + )); 298 + assert!(approx_eq( 299 + LayoutPx::saturating(f32::INFINITY).value(), 300 + 0.0, 301 + PX_EPSILON 302 + )); 303 + assert!(approx_eq( 304 + LayoutPx::saturating(-3.0).value(), 305 + -3.0, 306 + PX_EPSILON 307 + )); 308 + assert!(approx_eq( 309 + LayoutPx::saturating_nonneg(-3.0).value(), 310 + 0.0, 311 + PX_EPSILON 312 + )); 313 + assert!(approx_eq( 314 + LayoutPx::saturating_nonneg(f32::NEG_INFINITY).value(), 315 + 0.0, 316 + PX_EPSILON, 317 + )); 318 + assert!(approx_eq( 319 + LayoutPx::saturating_nonneg(7.0).value(), 320 + 7.0, 321 + PX_EPSILON 322 + )); 323 + } 324 + 325 + #[test] 326 + fn layout_rect_max_x_saturates_on_overflow() { 327 + use super::geometry::{LayoutPos, LayoutRect}; 328 + let big = LayoutPx::saturating(f32::MAX); 329 + let r = LayoutRect::new( 330 + LayoutPos::new(big, LayoutPx::ZERO), 331 + LayoutSize::new(big, LayoutPx::ZERO), 332 + ); 333 + assert_eq!(r.max_x(), LayoutPx::ZERO); 334 + } 335 + 336 + #[test] 337 + fn row_space_between_pushes_extremes_to_edges() { 338 + let layout = Layout::Row { 339 + gap: sp(0.0), 340 + justify: MainAxisJustify::SpaceBetween, 341 + cross: CrossAxisAlign::Center, 342 + children: vec![ 343 + Layout::Gap { size: sp(20.0) }, 344 + Layout::Gap { size: sp(20.0) }, 345 + Layout::Gap { size: sp(20.0) }, 346 + ], 347 + }; 348 + let solved = solve(&layout, size(120.0, 10.0), &RetainedLayout::default()); 349 + let kids = &solved.root_node().children; 350 + let first = solved.node(kids[0]); 351 + let last = solved.node(kids[2]); 352 + assert!(approx_eq(first.rect.min_x().value(), 0.0, 1.0)); 353 + assert!(approx_eq(last.rect.max_x().value(), 120.0, 1.0)); 354 + } 355 + 356 + #[test] 357 + fn row_space_around_distributes_equal_padding() { 358 + let layout = Layout::Row { 359 + gap: sp(0.0), 360 + justify: MainAxisJustify::SpaceAround, 361 + cross: CrossAxisAlign::Center, 362 + children: vec![ 363 + Layout::Gap { size: sp(20.0) }, 364 + Layout::Gap { size: sp(20.0) }, 365 + ], 366 + }; 367 + let solved = solve(&layout, size(120.0, 10.0), &RetainedLayout::default()); 368 + let kids = &solved.root_node().children; 369 + let first = solved.node(kids[0]); 370 + let second = solved.node(kids[1]); 371 + let lead = first.rect.min_x().value(); 372 + let trail = 120.0 - second.rect.max_x().value(); 373 + assert!(approx_eq(lead, trail, 1.0)); 374 + let mid_gap = second.rect.min_x().value() - first.rect.max_x().value(); 375 + assert!(approx_eq(mid_gap, 2.0 * lead, 1.0)); 376 + } 377 + 378 + #[test] 379 + fn grid_duplicate_track_name_errors() { 380 + const SHARED: TrackName = TrackName::new("dup"); 381 + let span = GridSpan { 382 + column_start: GridLineRef::Name(SHARED), 383 + column_end: GridLineRef::Line(gl(3)), 384 + row_start: GridLineRef::Line(gl(1)), 385 + row_end: GridLineRef::Line(gl(2)), 386 + }; 387 + let layout = Layout::Grid { 388 + columns: vec![ 389 + GridTrack::named(SHARED, TrackSize::FLEX_1), 390 + GridTrack::named(SHARED, TrackSize::FLEX_1), 391 + ], 392 + rows: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 393 + column_gap: sp(0.0), 394 + row_gap: sp(0.0), 395 + children: vec![GridChild { 396 + span, 397 + child: Layout::leaf(wid(1)), 398 + }], 399 + }; 400 + let result = measure(&layout, size(100.0, 30.0), &RetainedLayout::default()); 401 + let Err(LayoutError::DuplicateTrackName(name)) = result else { 402 + panic!("expected DuplicateTrackName, got {result:?}"); 403 + }; 404 + assert_eq!(name, "dup"); 405 + } 406 + 407 + #[test] 408 + fn flex_weight_non_unit_doubles_spacer_share() { 409 + let layout = Layout::Row { 410 + gap: sp(0.0), 411 + justify: MainAxisJustify::Start, 412 + cross: CrossAxisAlign::Stretch, 413 + children: vec![ 414 + Layout::weighted_spacer(fw(1)), 415 + Layout::weighted_spacer(fw(2)), 416 + ], 417 + }; 418 + let solved = solve(&layout, size(150.0, 10.0), &RetainedLayout::default()); 419 + let kids = &solved.root_node().children; 420 + let one = solved.node(kids[0]); 421 + let two = solved.node(kids[1]); 422 + assert!(approx_eq(one.rect.size.width.value(), 50.0, 1.0)); 423 + assert!(approx_eq(two.rect.size.width.value(), 100.0, 1.0)); 424 + } 425 + 426 + #[test] 427 + fn grid_named_span_inverted_resolved_order_errors() { 428 + const SIDE: TrackName = TrackName::new("side"); 429 + const END: TrackName = TrackName::new("end"); 430 + let span = GridSpan { 431 + column_start: GridLineRef::Name(END), 432 + column_end: GridLineRef::Name(SIDE), 433 + row_start: GridLineRef::Line(gl(1)), 434 + row_end: GridLineRef::Line(gl(2)), 435 + }; 436 + let layout = Layout::Grid { 437 + columns: vec![ 438 + GridTrack::named(SIDE, TrackSize::FLEX_1), 439 + GridTrack::named(END, TrackSize::FLEX_1), 440 + ], 441 + rows: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 442 + column_gap: sp(0.0), 443 + row_gap: sp(0.0), 444 + children: vec![GridChild { 445 + span, 446 + child: Layout::leaf(wid(1)), 447 + }], 448 + }; 449 + let result = measure(&layout, size(100.0, 30.0), &RetainedLayout::default()); 450 + assert!(matches!(result, Err(LayoutError::GridColumnSpanNotIncreasing))); 451 + } 452 + 453 + #[test] 454 + fn grid_named_row_span_inverted_errors() { 455 + const TOP: TrackName = TrackName::new("top"); 456 + const BOTTOM: TrackName = TrackName::new("bottom"); 457 + let span = GridSpan { 458 + column_start: GridLineRef::Line(gl(1)), 459 + column_end: GridLineRef::Line(gl(2)), 460 + row_start: GridLineRef::Name(BOTTOM), 461 + row_end: GridLineRef::Name(TOP), 462 + }; 463 + let layout = Layout::Grid { 464 + columns: vec![GridTrack::unnamed(TrackSize::FLEX_1)], 465 + rows: vec![ 466 + GridTrack::named(TOP, TrackSize::FLEX_1), 467 + GridTrack::named(BOTTOM, TrackSize::FLEX_1), 468 + ], 469 + column_gap: sp(0.0), 470 + row_gap: sp(0.0), 471 + children: vec![GridChild { 472 + span, 473 + child: Layout::leaf(wid(1)), 474 + }], 475 + }; 476 + let result = measure(&layout, size(60.0, 60.0), &RetainedLayout::default()); 477 + assert!(matches!(result, Err(LayoutError::GridRowSpanNotIncreasing))); 478 + } 479 + 480 + #[test] 481 + fn inset_inside_row_distributes_main_axis_evenly() { 482 + let layout = Layout::Row { 483 + gap: sp(0.0), 484 + justify: MainAxisJustify::Start, 485 + cross: CrossAxisAlign::Stretch, 486 + children: vec![ 487 + Layout::inset(EdgeInsets::all(sp(8.0)), Layout::leaf(wid(1))), 488 + Layout::inset(EdgeInsets::all(sp(8.0)), Layout::leaf(wid(2))), 489 + ], 490 + }; 491 + let solved = solve(&layout, size(200.0, 50.0), &RetainedLayout::default()); 492 + let kids = &solved.root_node().children; 493 + let a = solved.node(kids[0]); 494 + let b = solved.node(kids[1]); 495 + assert!( 496 + approx_eq(a.rect.size.width.value(), 100.0, 1.0), 497 + "first inset: {}", 498 + a.rect.size.width.value() 499 + ); 500 + assert!( 501 + approx_eq(b.rect.size.width.value(), 100.0, 1.0), 502 + "second inset: {}", 503 + b.rect.size.width.value() 504 + ); 505 + assert!(approx_eq(b.rect.min_x().value(), 100.0, 1.0)); 506 + } 507 + 508 + #[test] 509 + fn nested_row_inside_row_claims_proportional_main_axis() { 510 + let inner = Layout::Row { 511 + gap: sp(0.0), 512 + justify: MainAxisJustify::Start, 513 + cross: CrossAxisAlign::Stretch, 514 + children: vec![Layout::leaf(wid(1)), Layout::leaf(wid(2))], 515 + }; 516 + let layout = Layout::Row { 517 + gap: sp(0.0), 518 + justify: MainAxisJustify::Start, 519 + cross: CrossAxisAlign::Stretch, 520 + children: vec![inner, Layout::leaf(wid(3))], 521 + }; 522 + let solved = solve(&layout, size(200.0, 50.0), &RetainedLayout::default()); 523 + let kids = &solved.root_node().children; 524 + let inner_solved = solved.node(kids[0]); 525 + let outer_leaf = solved.node(kids[1]); 526 + assert!( 527 + approx_eq(inner_solved.rect.size.width.value(), 100.0, 1.0), 528 + "inner row: {}", 529 + inner_solved.rect.size.width.value() 530 + ); 531 + assert!(approx_eq(outer_leaf.rect.size.width.value(), 100.0, 1.0)); 532 + } 533 + 534 + #[test] 535 + fn row_main_axis_end_pushes_children_to_right_edge() { 536 + let layout = Layout::Row { 537 + gap: sp(0.0), 538 + justify: MainAxisJustify::End, 539 + cross: CrossAxisAlign::Center, 540 + children: vec![Layout::Gap { size: sp(20.0) }, Layout::Gap { size: sp(20.0) }], 541 + }; 542 + let solved = solve(&layout, size(120.0, 10.0), &RetainedLayout::default()); 543 + let kids = &solved.root_node().children; 544 + let last = solved.node(kids[1]); 545 + assert!( 546 + approx_eq(last.rect.max_x().value(), 120.0, 1.0), 547 + "last child should hug right edge: {}", 548 + last.rect.max_x().value() 549 + ); 550 + let first = solved.node(kids[0]); 551 + assert!(approx_eq(first.rect.min_x().value(), 80.0, 1.0)); 552 + } 553 + 554 + #[test] 555 + fn layout_px_try_from_rejects_non_finite() { 556 + assert_eq!(LayoutPx::try_from(f32::NAN), Err(LayoutPxError::NotFinite)); 557 + assert_eq!( 558 + LayoutPx::try_from(f32::INFINITY), 559 + Err(LayoutPxError::NotFinite) 560 + ); 561 + assert_eq!( 562 + LayoutPx::try_from(f32::NEG_INFINITY), 563 + Err(LayoutPxError::NotFinite) 564 + ); 565 + let Ok(px) = LayoutPx::try_from(7.5) else { 566 + panic!("finite must round-trip"); 567 + }; 568 + assert!(approx_eq(px.value(), 7.5, PX_EPSILON)); 569 + } 570 + 571 + #[test] 572 + fn layout_px_serde_rejects_non_finite() { 573 + assert!(ron::de::from_str::<LayoutPx>("NaN").is_err()); 574 + assert!(ron::de::from_str::<LayoutPx>("inf").is_err()); 575 + assert!(ron::de::from_str::<LayoutPx>("-inf").is_err()); 576 + let Ok(parsed) = ron::de::from_str::<LayoutPx>("12.5") else { 577 + panic!("finite must deserialize"); 578 + }; 579 + assert!(approx_eq(parsed.value(), 12.5, PX_EPSILON)); 580 + }