1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct HandshakeParams {
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub token: Option<String>,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct HandshakeInfo {
33 pub app_name: String,
34 pub frust_version: String,
35 pub protocol_version: u32,
36 pub capabilities: Vec<Capability>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum Capability {
45 WidgetTree,
46 FrameStats,
47 Input,
48 Metrics,
49 Screenshot,
50 #[serde(other)]
55 Unknown,
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct WidgetTreeDump {
64 pub roots: Vec<WidgetNode>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct WidgetNode {
69 pub id: u64,
70 pub type_name: String,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub debug_label: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub bounds: Option<RectPx>,
75 #[serde(default)]
76 pub children: Vec<WidgetNode>,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
83pub struct RectPx {
84 pub x: f64,
85 pub y: f64,
86 pub width: f64,
87 pub height: f64,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub struct WidgetPropsParams {
93 pub id: u64,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct WidgetProps {
99 pub id: u64,
100 pub entries: Vec<(String, String)>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113pub struct FrameStats {
114 pub n: u64,
115 pub total_us: u64,
116 pub rebuild_us: u64,
117 pub layout_us: u64,
118 pub paint_us: u64,
119 pub encode_us: u64,
120 pub acquire_us: u64,
121 pub submit_us: u64,
122 pub skipped: bool,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128pub struct MetricsSnapshot {
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub rss_bytes: Option<u64>,
131 pub uptime_ms: u64,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
136pub struct InputTapParams {
137 pub x: f64,
138 pub y: f64,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
145pub struct InputScrollParams {
146 pub x: f64,
147 pub y: f64,
148 pub dx: f64,
149 pub dy: f64,
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154pub struct InputTextParams {
155 pub text: String,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub struct AckResult {
162 #[serde(default = "default_true")]
163 pub ok: bool,
164}
165
166fn default_true() -> bool {
167 true
168}
169
170impl Default for AckResult {
171 fn default() -> Self {
172 Self { ok: true }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct ScreenshotResult {
182 pub png_base64: String,
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn handshake_params_round_trip_with_and_without_a_token() {
191 let with = HandshakeParams {
192 token: Some("0123456789abcdef".to_string()),
193 };
194 let json = serde_json::to_string(&with).unwrap();
195 assert_eq!(
196 serde_json::from_str::<HandshakeParams>(&json).unwrap(),
197 with
198 );
199
200 let without: HandshakeParams = serde_json::from_str("{}").unwrap();
203 assert_eq!(without, HandshakeParams::default());
204 assert_eq!(without.token, None);
205 assert!(!serde_json::to_string(&without).unwrap().contains("token"));
206 }
207
208 #[test]
209 fn no_server_written_payload_carries_a_token_field() {
210 let info = HandshakeInfo {
213 app_name: "app".to_string(),
214 frust_version: "0.1.0".to_string(),
215 protocol_version: 1,
216 capabilities: vec![Capability::WidgetTree],
217 };
218 let json = serde_json::to_string(&info).unwrap();
219 assert!(!json.contains("token"));
220 }
221
222 #[test]
223 fn capability_round_trips() {
224 for cap in [
225 Capability::WidgetTree,
226 Capability::FrameStats,
227 Capability::Input,
228 Capability::Metrics,
229 Capability::Screenshot,
230 ] {
231 let json = serde_json::to_string(&cap).unwrap();
232 let back: Capability = serde_json::from_str(&json).unwrap();
233 assert_eq!(cap, back);
234 }
235 }
236
237 #[test]
238 fn unknown_capability_deserializes_to_unknown_variant() {
239 let cap: Capability = serde_json::from_str("\"some_future_capability\"").unwrap();
240 assert_eq!(cap, Capability::Unknown);
241 }
242
243 #[test]
244 fn ack_result_defaults_ok_true_when_field_omitted() {
245 let ack: AckResult = serde_json::from_str("{}").unwrap();
246 assert_eq!(ack, AckResult { ok: true });
247 assert_eq!(AckResult::default(), AckResult { ok: true });
248 }
249
250 #[test]
251 fn frame_stats_field_names_match_perf_raw_line() {
252 let stats = FrameStats {
253 n: 42,
254 total_us: 1_000,
255 rebuild_us: 100,
256 layout_us: 200,
257 paint_us: 300,
258 encode_us: 150,
259 acquire_us: 50,
260 submit_us: 200,
261 skipped: false,
262 };
263 let json = serde_json::to_value(stats).unwrap();
264 for key in [
265 "n",
266 "total_us",
267 "rebuild_us",
268 "layout_us",
269 "paint_us",
270 "encode_us",
271 "acquire_us",
272 "submit_us",
273 "skipped",
274 ] {
275 assert!(json.get(key).is_some(), "missing field: {key}");
276 }
277 }
278}