concinnity_world/schema/panel.rs
1//! UI panel container schema.
2
3/// A titled background container for grouping UI overlay elements.
4///
5/// `Panel` is a build-time shorthand: it expands into a filled, optionally
6/// rounded background [Sprite](#sprite) and, when `title` is set, a
7/// [TextLabel](#textlabel) heading inset from the top-left corner. Place other
8/// overlay elements over it to frame a group (a settings card, a dialog body).
9///
10/// Like the other build-time UI shorthands, generated names are prefixed with
11/// this asset's `name` (`<name>_bg`, `<name>_title`), so a panel named with a
12/// screen prefix (`pause_card`) puts its children in that [Screen](#screen)
13/// (`pause`) via the `<screen>_*` rule and they never clash with hand-authored
14/// assets.
15///
16/// ```rust
17/// # use concinnity_world::registry::build_only::Panel;
18/// Panel {
19/// title: "Paused".into(),
20/// x: 440.0,
21/// y: 220.0,
22/// width: 400.0,
23/// height: 280.0,
24/// ..Default::default()
25/// };
26/// ```
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28#[serde(default)]
29pub struct Panel {
30 /// Left edge of the panel in window pixels.
31 pub x: f32,
32 /// Top edge of the panel in window pixels.
33 pub y: f32,
34 /// Panel width in window pixels.
35 pub width: f32,
36 /// Panel height in window pixels.
37 pub height: f32,
38 /// RGBA fill of the background box, each channel in [0, 1].
39 pub color: [f32; 4],
40 /// Corner rounding radius of the background box, in panel pixels. 0 keeps
41 /// sharp corners.
42 pub corner_radius: f32,
43 /// Heading text drawn at the top-left. Empty draws no heading.
44 pub title: String,
45 /// [Font](#font) for the title. Empty uses the built-in font.
46 pub title_font: String,
47 /// Linear-space RGB colour of the title text.
48 pub title_color: [f32; 3],
49 /// Scale applied to the title text.
50 pub title_scale: f32,
51 /// Inset of the title from the panel's top-left corner, in pixels.
52 pub padding: f32,
53}
54
55impl Default for Panel {
56 fn default() -> Self {
57 Self {
58 x: 0.0,
59 y: 0.0,
60 width: 400.0,
61 height: 300.0,
62 color: [0.08, 0.09, 0.12, 0.96],
63 corner_radius: 8.0,
64 title: String::new(),
65 title_font: String::new(),
66 title_color: [0.95, 0.95, 0.97],
67 title_scale: 1.0,
68 padding: 16.0,
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn a_blank_panel_is_a_rounded_near_opaque_dark_container() {
79 let p = Panel::default();
80 assert_eq!((p.width, p.height), (400.0, 300.0));
81 assert_eq!(p.corner_radius, 8.0);
82 assert_eq!(p.padding, 16.0);
83 assert_eq!(p.title_scale, 1.0);
84 assert!(p.title.is_empty());
85 assert!(p.title_font.is_empty());
86 // Near-opaque rather than fully so, so the world reads faintly behind it.
87 assert_eq!(p.color[3], 0.96);
88 }
89
90 #[test]
91 fn an_authored_panel_parses_and_round_trips_through_postcard() {
92 let p: Panel = serde_json::from_str(
93 r#"{"x":20,"y":30,"width":520,"height":360,"color":[0,0,0,1],
94 "corner_radius":0,"title":"Outliner","title_font":"body",
95 "title_color":[1,1,1],"title_scale":1.2,"padding":8}"#,
96 )
97 .unwrap();
98 assert_eq!(p.title, "Outliner");
99 assert_eq!(p.corner_radius, 0.0);
100
101 let bytes = postcard::to_allocvec(&p).unwrap();
102 let back: Panel = postcard::from_bytes(&bytes).unwrap();
103 assert_eq!((back.x, back.y), (20.0, 30.0));
104 assert_eq!((back.width, back.height), (520.0, 360.0));
105 assert_eq!(back.color, [0.0, 0.0, 0.0, 1.0]);
106 assert_eq!(back.title_font, "body");
107 assert_eq!(back.title_color, [1.0, 1.0, 1.0]);
108 assert_eq!(back.title_scale, 1.2);
109 assert_eq!(back.padding, 8.0);
110 }
111}