1use serde::{Deserialize, Serialize};
9
10pub const MIN_SUPPORTED_PROTOCOL_VERSION: u32 = 1;
14
15pub const PROTOCOL_VERSION: u32 = 2;
17
18pub const DEVTOOLS_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum PeerKind {
24 Target,
25 Devtools,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Hello {
33 pub protocol_version: u32,
34 pub devtools_version: String,
35 pub kind: PeerKind,
36 pub auth_token: String,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum ErrorCode {
42 ProtocolVersionMismatch,
43 Unauthorized,
44 UnknownId,
45 StaleId,
46 Unsupported,
47 InvalidRequest,
48 InternalError,
49}
50
51macro_rules! opaque_id {
58 ($name:ident) => {
59 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
60 pub struct $name {
61 index: u64,
62 generation: u64,
63 }
64 impl $name {
65 pub const fn new(index: u64, generation: u64) -> Self {
66 Self { index, generation }
67 }
68 pub const fn index(&self) -> u64 {
69 self.index
70 }
71 pub const fn generation(&self) -> u64 {
72 self.generation
73 }
74 }
75 impl std::fmt::Display for $name {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 write!(
78 f,
79 "{}:{}:{}",
80 stringify!($name),
81 self.index,
82 self.generation
83 )
84 }
85 }
86 };
87}
88
89opaque_id!(DevWindowId);
90opaque_id!(DevWidgetId);
91opaque_id!(DevRenderId);
92opaque_id!(DevLayerId);
93opaque_id!(DevSemanticsId);
94opaque_id!(DevSignalId);
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum DebugValue {
103 Bool(bool),
104 Int(i64),
105 Uint(u64),
106 Float(f64),
107 Str(String),
109 Color(u8, u8, u8, u8),
110 Enum(String),
111 Size([f32; 2]),
112 Offset([f32; 2]),
113 Rect([f32; 4]),
114 Insets([f32; 4]),
115 Constraints {
116 min_width: f32,
117 #[serde(with = "unbounded_maximum")]
120 max_width: f32,
121 min_height: f32,
122 #[serde(with = "unbounded_maximum")]
123 max_height: f32,
124 },
125 Optional(Option<Box<DebugValue>>),
126 List(Vec<DebugValue>),
127 Redacted,
128}
129
130mod unbounded_maximum {
131 use serde::{Deserialize, Deserializer, Serializer, ser::Error};
132
133 pub fn serialize<S: Serializer>(value: &f32, serializer: S) -> Result<S::Ok, S::Error> {
134 if value.is_finite() {
135 serializer.serialize_f32(*value)
136 } else if *value == f32::INFINITY {
137 serializer.serialize_none()
138 } else {
139 Err(S::Error::custom(
140 "constraint maximum must be finite or positive infinity",
141 ))
142 }
143 }
144
145 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f32, D::Error> {
146 Option::<f32>::deserialize(deserializer).map(|maximum| maximum.unwrap_or(f32::INFINITY))
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
151pub struct DebugProperty {
152 pub name: String,
153 pub value: DebugValue,
154 #[serde(default)]
156 pub overridden: bool,
157 #[serde(default)]
158 pub editable: bool,
159}
160
161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162pub struct PropertyChange {
163 pub name: String,
164 pub old: Option<DebugValue>,
165 pub new: Option<DebugValue>,
166}
167
168#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case", tag = "kind")]
170pub enum InvalidationReason {
171 SignalWrite {
172 signal: DevSignalId,
173 name: String,
174 old: Option<String>,
175 new: Option<String>,
176 },
177 ParentReconciliation {
178 changed: Vec<PropertyChange>,
179 },
180 WidgetConfigurationChanged,
181 EnvironmentChanged {
182 field: String,
183 old: Option<String>,
184 new: Option<String>,
185 },
186 LocaleChanged,
187 WindowMetricsChanged,
188 ConstraintsChanged,
189 Animation,
190 TaskCompletion,
191 Navigation,
192 Restoration,
193 ManualInvalidation,
194 Mounted,
195}
196
197#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
198pub struct SourceLocation {
199 pub file: String,
200 pub line: u32,
201 pub column: u32,
202}
203
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
209pub struct WidgetNode {
210 pub id: DevWidgetId,
211 pub parent: Option<DevWidgetId>,
212 pub type_name: String,
213 pub key: Option<String>,
214 pub label: Option<String>,
216 pub child_ids: Vec<DevWidgetId>,
217 pub revision: u64,
220}
221
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case", tag = "kind")]
224pub enum TreeDelta {
225 Snapshot {
226 window: DevWindowId,
227 root: Box<WidgetNode>,
228 nodes: Vec<WidgetNode>,
229 truncated: bool,
230 },
231 Insert {
232 node: Box<WidgetNode>,
233 },
234 Remove {
235 id: DevWidgetId,
236 },
237 Move {
238 id: DevWidgetId,
239 parent: DevWidgetId,
240 position: usize,
241 },
242 Update {
243 node: Box<WidgetNode>,
244 },
245}
246
247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
252pub struct ElementState {
253 pub mount_generation: u64,
254 pub builds: u64,
255 pub layouts: u64,
256 pub paints: u64,
257 pub composites: u64,
258 pub semantic_updates: u64,
259 pub constraints: Option<DebugValue>,
260 pub size: Option<[f32; 2]>,
261 pub offset: Option<[f32; 2]>,
262 pub world_bounds: Option<[f32; 4]>,
263 #[serde(default)]
265 pub baseline: Option<f32>,
266 #[serde(default)]
269 pub clip: Option<[f32; 4]>,
270}
271
272#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
276pub struct LayoutInspection {
277 pub node: DevWidgetId,
278 pub parent: Option<DevWidgetId>,
279 pub incoming_constraints: Option<DebugValue>,
280 pub resolved_size: [f32; 2],
281 pub local_offset: [f32; 2],
282 pub local_transform: [f32; 6],
284 pub world_transform: [f32; 6],
286 pub world_bounds: [f32; 4],
287 pub content_bounds: [f32; 4],
288 pub padding: Option<[f32; 4]>,
289 pub clip: Option<[f32; 4]>,
290 pub baseline: Option<f32>,
291 pub details: LayoutDetails,
292}
293
294#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
295pub struct LayoutHistoryEntry {
296 pub sequence: u64,
297 pub old_constraints: Option<DebugValue>,
298 pub new_constraints: DebugValue,
299 pub old_size: [f32; 2],
300 pub new_size: [f32; 2],
301 pub cause: Option<String>,
302}
303
304#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
305pub struct WorkReasons {
306 pub layout: Option<String>,
307 pub paint: Option<String>,
308 pub composite: Option<String>,
309}
310
311#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case", tag = "kind")]
316pub enum LayoutDetails {
317 Box,
318 Flex {
319 axis: String,
320 available_main: Option<f32>,
321 non_flex_extent: f32,
322 flexible_extent: f32,
323 used_extent: f32,
324 remaining_extent: f32,
325 overflow: f32,
326 children: Vec<FlexChildInspection>,
327 },
328 Stack {
329 alignment: [f32; 2],
330 indexed_active: Option<usize>,
331 children: Vec<StackChildInspection>,
332 },
333 Positioned {
334 left: Option<f32>,
335 right: Option<f32>,
336 top: Option<f32>,
337 bottom: Option<f32>,
338 width: Option<f32>,
339 height: Option<f32>,
340 },
341 Transform {
342 matrix: [f32; 6],
343 determinant: f32,
344 invertible: bool,
345 },
346 Fitted {
347 fit: String,
348 alignment: [f32; 2],
349 source_size: Option<[f32; 2]>,
350 destination_size: [f32; 2],
351 matrix: [f32; 6],
352 determinant: f32,
353 invertible: bool,
354 },
355 Scroll {
356 axis: String,
357 viewport_extent: f32,
358 content_extent: f32,
359 offset: f32,
360 min_scroll: f32,
361 max_scroll: f32,
362 },
363 LazyViewport {
364 item_count: usize,
365 materialized_start: usize,
366 materialized_end: usize,
367 materialized_items: usize,
368 viewport_extent: f32,
369 cache_extent: f32,
370 scroll_offset: f32,
371 },
372 Text {
373 text_length: usize,
374 max_lines: Option<usize>,
375 overflow: String,
376 line_count: Option<usize>,
377 },
378 Custom {
379 layout_kind: String,
380 },
381}
382
383#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
384pub struct FlexChildInspection {
385 pub node: DevWidgetId,
386 pub index: usize,
387 pub type_name: String,
388 pub size: [f32; 2],
389 pub offset: [f32; 2],
390 pub flex: Option<u32>,
391 pub fit: Option<String>,
392 pub allocated_main_extent: Option<f32>,
394 pub actual_main_extent: f32,
395 pub cross_extent: f32,
396}
397
398#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
399pub struct StackChildInspection {
400 pub node: DevWidgetId,
401 pub index: usize,
402 pub type_name: String,
403 pub bounds: [f32; 4],
404 pub painted: bool,
405 pub left: Option<f32>,
406 pub right: Option<f32>,
407 pub top: Option<f32>,
408 pub bottom: Option<f32>,
409 pub width: Option<f32>,
410 pub height: Option<f32>,
411}
412
413#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
414pub struct NodeDetails {
415 pub id: DevWidgetId,
416 pub window: DevWindowId,
417 pub type_name: String,
418 pub key: Option<String>,
419 pub state: ElementState,
420 #[serde(default)]
423 pub layout: Option<LayoutInspection>,
424 #[serde(default)]
427 pub layout_history: Vec<LayoutHistoryEntry>,
428 #[serde(default)]
429 pub work_reasons: WorkReasons,
430 pub properties: Vec<DebugProperty>,
431 #[serde(default)]
434 pub property_changes: Vec<PropertyChange>,
435 #[serde(default)]
438 pub invalidation_causes: Vec<InvalidationReason>,
439 #[serde(default)]
442 pub consumed_signals: Vec<DevSignalId>,
443 pub invalidation: Option<InvalidationReason>,
444 pub render: Option<DevRenderId>,
445 pub semantics: Option<DevSemanticsId>,
446 pub source: Option<SourceLocation>,
447}
448
449#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
450pub struct FrameTimingsWire {
451 pub event_processing: u32,
452 pub runtime_messages: u32,
453 pub build: u32,
454 pub layout: u32,
455 pub composite: u32,
456 pub semantics: u32,
457 pub paint: u32,
458 pub cpu_total: u32,
459 pub prepare: u32,
460 pub encode: u32,
461 pub submit: u32,
462 pub gpu_us: Option<f64>,
464}
465
466#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
467pub struct FrameRecordEvent {
468 pub window: DevWindowId,
469 pub frame: u64,
470 pub timings: FrameTimingsWire,
471 #[serde(default)]
474 pub budget_us: Option<u32>,
475 pub over_budget: bool,
476 pub draw_calls: u32,
477 pub instances: u32,
478 pub upload_bytes: u64,
479 pub pipelines_created: u32,
480}
481
482#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(rename_all = "snake_case")]
484pub enum DevtoolsProfilerMode {
485 #[default]
486 Basic,
487 Performance,
488 Deep,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
492#[serde(rename_all = "snake_case")]
493pub enum TracePhase {
494 Build,
495 Layout,
496 Paint,
497 Semantics,
498 Composite,
499}
500
501#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
503pub struct TraceEvent {
504 pub node: DevWidgetId,
505 pub phase: TracePhase,
506 pub parent: Option<u32>,
507 pub start_us: u32,
508 pub duration_us: u32,
509}
510
511#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
514pub struct DeepFrameTrace {
515 pub window: DevWindowId,
516 pub frame: u64,
517 pub events: Vec<TraceEvent>,
518 pub truncated: bool,
519 pub dropped_events: u32,
520}
521
522#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
523pub struct ResourceCounts {
524 pub elements: usize,
525 pub render_objects: usize,
526 pub layers: usize,
527 pub semantics_nodes: usize,
528 pub signals: usize,
529 pub tasks_active: usize,
530 pub glyph_atlas_pages: usize,
531 pub image_resources: usize,
532 pub gradient_resources: usize,
533 pub path_meshes: usize,
534 pub offscreen_bytes: usize,
535 pub effect_cached_bytes: usize,
536 pub rss_mb: usize,
537}
538
539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
540pub struct MemorySnapshot {
541 pub label: String,
542 pub counts: ResourceCounts,
543}
544
545#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
546pub struct SignalSummary {
547 pub id: DevSignalId,
548 pub name: Option<String>,
549 pub type_name: String,
550 #[serde(default)]
552 pub generation: u64,
553 pub write_count: u64,
554 pub subscriber_count: usize,
555 pub last_write_summary: Option<String>,
556 pub editable: bool,
557}
558
559#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
560pub struct SignalSubscriber {
561 pub signal: DevSignalId,
562 pub element: DevWidgetId,
563 pub path: String,
564}
565
566#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
567#[serde(rename_all = "snake_case")]
568pub enum DebugOption {
569 LayoutBounds,
570 LayoutBoundsSubtree,
571 LayoutBoundsWholeWindow,
572 PaddingContent,
573 Baselines,
574 Clips,
575 HitTestRegions,
576 SemanticsBounds,
577 ScrollViewports,
578 LayerBoundaries,
579 RepaintRainbow,
580 FlashRebuilds,
581 HighlightBuild,
582 HighlightLayout,
583 HighlightPaint,
584 HighlightSemantics,
585 HighlightComposite,
586 HighlightOversizedImages,
587}
588
589#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
590#[serde(rename_all = "snake_case")]
591pub enum CacheKind {
592 TextLayouts,
593 GlyphAtlas,
594 Images,
595 Paths,
596 Gradients,
597 Offscreen,
598}
599
600#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
601#[serde(rename_all = "snake_case")]
602pub enum EditableValue {
603 Bool(bool),
604 Int(i64),
605 Uint(u64),
606 Float(f64),
607 Str(String),
608 Color(u8, u8, u8, u8),
609}
610
611#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
612pub struct CacheReport {
613 pub which: CacheKind,
614 pub entries: usize,
615 pub hits: u64,
616 pub misses: u64,
617}
618
619#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
626#[serde(rename_all = "snake_case", tag = "method")]
627pub enum RequestMethod {
628 GetTargetInfo,
629 GetWidgetTree {
630 window: DevWindowId,
631 },
632 GetNodeDetails {
633 id: DevWidgetId,
634 },
635 EditProperty {
636 id: DevWidgetId,
637 name: String,
638 value: DebugValue,
639 },
640 StartInspectMode {
641 window: DevWindowId,
642 },
643 StopInspectMode {
644 window: DevWindowId,
645 },
646 HighlightNode {
647 window: DevWindowId,
648 id: Option<DevWidgetId>,
649 },
650 SetDebugOption {
651 name: DebugOption,
652 enabled: bool,
653 },
654 SetProfilerMode {
655 mode: DevtoolsProfilerMode,
656 },
657 StartRecording,
658 StopRecording,
659 TakeMemorySnapshot {
660 label: String,
661 },
662 ListSignals,
663 GetSignalSubscribers {
664 id: DevSignalId,
665 },
666 EditSignal {
667 id: DevSignalId,
668 value: EditableValue,
669 },
670 ResetOverrides,
671 ClearCache {
672 which: CacheKind,
673 },
674 SetAnimationSpeed {
675 scale: f32,
676 },
677}
678
679#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case", tag = "result")]
681pub enum ResponsePayload {
682 Ok,
683 TargetInfo(Box<TargetInfo>),
684 WidgetTree {
685 deltas: Vec<TreeDelta>,
686 tree_revision: u64,
687 },
688 NodeDetails(Box<NodeDetails>),
689 RecordingStarted,
690 RecordingStopped {
691 frames: u64,
692 },
693 MemorySnapshot(MemorySnapshot),
694 Signals {
695 signals: Vec<SignalSummary>,
696 },
697 SignalSubscribers {
698 subscribers: Vec<SignalSubscriber>,
699 },
700 Edited,
701 OverridesReset,
702 CacheCleared(CacheReport),
703 AnimationSpeedSet(f32),
704 ProfilerModeSet(DevtoolsProfilerMode),
705 Error {
706 code: ErrorCode,
707 message: String,
708 },
709}
710
711#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
713#[serde(rename_all = "snake_case", tag = "event")]
714pub enum TargetEvent {
715 WidgetTreeDeltas {
719 window: DevWindowId,
720 deltas: Vec<TreeDelta>,
721 tree_revision: u64,
722 },
723 FrameRecord(FrameRecordEvent),
724 DeepTrace(DeepFrameTrace),
725 WidgetSelectedByUser {
726 window: DevWindowId,
727 id: DevWidgetId,
728 },
729 Log {
730 level: String,
731 target: String,
732 message: String,
733 },
734 WindowsChanged,
735 DroppedTelemetry {
736 count: u64,
737 },
738 InspectModeEnded {
739 window: DevWindowId,
740 },
741}
742
743#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
745#[serde(rename_all = "snake_case", tag = "type")]
746pub enum Message {
747 Request {
748 request_id: u64,
749 body: RequestMethod,
750 },
751 Response {
752 request_id: u64,
753 payload: Result<ResponsePayload, ErrorCode>,
754 },
755 Event(TargetEvent),
756 Rejection {
758 code: ErrorCode,
759 message: String,
760 },
761}
762
763#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
764pub struct TargetInfo {
765 pub protocol_version: u32,
766 pub framework_version: String,
767 pub pid: u32,
768 pub executable: String,
769 pub platform: String,
770 pub windows: Vec<WindowSummary>,
771}
772
773#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
774pub struct WindowSummary {
775 pub id: DevWindowId,
776 pub title: String,
777 pub logical_size: [f32; 2],
778 pub scale_factor: f64,
779}
780
781#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
784pub struct DiscoveryRecord {
785 pub pid: u32,
786 pub app_name: String,
787 pub port: u16,
788 pub auth_token: String,
791 pub started_unix_ms: u64,
792 pub protocol_version: u32,
793}
794
795pub fn check_hello(
798 hello: &Hello,
799 expected_kind: PeerKind,
800 expected_token: Option<&str>,
801) -> Result<(), ErrorCode> {
802 if !(MIN_SUPPORTED_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&hello.protocol_version) {
803 return Err(ErrorCode::ProtocolVersionMismatch);
804 }
805 if let Some(token) = expected_token {
806 if !token_eq(&hello.auth_token, token) {
809 return Err(ErrorCode::Unauthorized);
810 }
811 }
812 if hello.kind != expected_kind {
813 return Err(ErrorCode::InvalidRequest);
814 }
815 Ok(())
816}
817
818fn token_eq(a: &str, b: &str) -> bool {
819 let (a, b) = (a.as_bytes(), b.as_bytes());
820 if a.len() != b.len() {
821 return false;
822 }
823 let mut diff = 0u8;
824 for (x, y) in a.iter().zip(b.iter()) {
825 diff |= x ^ y;
826 }
827 diff == 0
828}