use std::time::Duration;
use waterui::Environment;
use waterui::inspector::FrameRecorder;
use waterui_inspector_protocol::{FrameKind, FrameSample};
#[cfg(feature = "accessibility")]
use waterui::inspector::TreeRecorder;
#[cfg(feature = "accessibility")]
use waterui_inspector_protocol::{Bounds, NodeId, NodeState, TreeNode};
use super::window::{FrameMode, FrameProfile};
pub(super) fn publish_frame(
environment: &Environment,
mode: FrameMode,
profile: &FrameProfile,
total: Duration,
) {
let Some(recorder) = environment.get::<FrameRecorder>() else {
return;
};
if !recorder.is_active() {
return;
}
let phases = &profile.phases;
let counters = &profile.counters;
let refresh_hz = waterui::task::max_refresh_rate_hz();
recorder.record(FrameSample {
kind: frame_kind(mode),
layout_us: micros(phases.build_content + phases.scene_dispatch + phases.scene_finish),
encode_us: micros(phases.acquire + phases.render),
present_us: micros(phases.present),
total_us: micros(total),
budget_us: budget_us(refresh_hz),
#[expect(
clippy::cast_possible_truncation,
reason = "a display refresh rate is far inside f32 range"
)]
refresh_hz: refresh_hz as f32,
layers: counters.scene_layers,
clip_depth: counters.max_clip_depth,
});
}
const fn frame_kind(mode: FrameMode) -> FrameKind {
match mode {
FrameMode::Idle => FrameKind::Idle,
FrameMode::Refresh => FrameKind::Refresh,
}
}
fn budget_us(refresh_hz: f64) -> u32 {
if refresh_hz <= 0.0 {
return 0;
}
micros(Duration::from_secs_f64(1.0 / refresh_hz))
}
fn micros(duration: Duration) -> u32 {
u32::try_from(duration.as_micros()).unwrap_or(u32::MAX)
}
#[cfg(feature = "accessibility")]
pub(super) fn publish_tree(environment: &Environment, update: &accesskit::TreeUpdate) {
let Some(recorder) = environment.get::<TreeRecorder>() else {
return;
};
if !recorder.is_active() {
return;
}
let root = update
.tree
.as_ref()
.map_or(NodeId(0), |tree| NodeId(tree.root.0));
let nodes = update
.nodes
.iter()
.map(|(id, node)| project_node(*id, node))
.collect();
recorder.record_snapshot(root, Some(NodeId(update.focus.0)), nodes);
}
#[cfg(feature = "accessibility")]
fn project_node(id: accesskit::NodeId, node: &accesskit::Node) -> TreeNode {
let checked = match node.toggled() {
Some(accesskit::Toggled::True) => Some(true),
Some(accesskit::Toggled::False) => Some(false),
Some(accesskit::Toggled::Mixed) | None => None,
};
TreeNode {
id: NodeId(id.0),
role: format!("{:?}", node.role()).to_lowercase(),
label: node.label().map(ToOwned::to_owned),
value: node
.value()
.map(ToOwned::to_owned)
.or_else(|| node.numeric_value().map(|value| value.to_string())),
bounds: node.bounds().map(|rect| {
#[expect(
clippy::cast_possible_truncation,
reason = "window-space point coordinates are far inside f32 range"
)]
Bounds {
x: rect.x0 as f32,
y: rect.y0 as f32,
width: (rect.x1 - rect.x0) as f32,
height: (rect.y1 - rect.y0) as f32,
}
}),
state: NodeState {
enabled: !node.is_disabled(),
selected: node.is_selected().unwrap_or(false),
checked,
expanded: node.is_expanded(),
hidden: node.is_hidden(),
},
children: node.children().iter().map(|id| NodeId(id.0)).collect(),
}
}