Skip to main content

cranpose_render_common/
dev_overlay.rs

1//! Renderer-drawn dev overlay (fps counter): one shared graph builder so
2//! every backend draws the identical overlay without composition impact.
3
4use cranpose_ui_graphics::{Brush, Color, CornerRadii, DrawPrimitive, Rect, Size};
5
6use crate::graph::{
7    DrawPrimitiveNode, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase,
8    ProjectiveTransform, RenderGraph, RenderNode, TextPrimitiveNode,
9};
10
11/// Cache key for the last-built overlay; rebuilding only on change keeps the
12/// overlay free when the text is stable.
13#[derive(Clone, PartialEq, Eq)]
14pub struct DevOverlayKey {
15    pub text: String,
16    pub viewport_width_bits: u32,
17    pub viewport_height_bits: u32,
18}
19
20impl DevOverlayKey {
21    pub fn new(text: &str, viewport: Size) -> Self {
22        Self {
23            text: text.to_string(),
24            viewport_width_bits: viewport.width.to_bits(),
25            viewport_height_bits: viewport.height.to_bits(),
26        }
27    }
28}
29
30/// Build the dev-overlay graph: a translucent pill with the stats text in
31/// the top-right corner, sized with fixed monospace-ish metrics so the
32/// overlay never depends on renderer text measurement.
33pub fn build_dev_overlay_graph(
34    text: &str,
35    viewport: Size,
36    node_id: cranpose_core::NodeId,
37) -> RenderGraph {
38    let padding = 8.0;
39    let font_size = 14.0;
40    let char_width = 7.0;
41    let text_width = text.len() as f32 * char_width;
42    let text_height = font_size * 1.4;
43    let x = (viewport.width - text_width - padding * 2.0).max(padding);
44    let y = padding;
45
46    let mut overlay_layer = LayerNode {
47        node_id: Some(node_id),
48        local_bounds: Rect {
49            x: 0.0,
50            y: 0.0,
51            width: text_width + padding,
52            height: text_height + padding / 2.0,
53        },
54        transform_to_parent: ProjectiveTransform::translation(x, y),
55        children: vec![
56            RenderNode::Primitive(PrimitiveEntry {
57                phase: PrimitivePhase::BeforeChildren,
58                node: PrimitiveNode::Draw(DrawPrimitiveNode {
59                    primitive: DrawPrimitive::RoundRect {
60                        rect: Rect {
61                            x: 0.0,
62                            y: 0.0,
63                            width: text_width + padding,
64                            height: text_height + padding / 2.0,
65                        },
66                        brush: Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
67                        radii: CornerRadii::uniform(4.0),
68                        stroke: None,
69                    },
70                    clip: None,
71                }),
72            }),
73            RenderNode::Primitive(PrimitiveEntry {
74                phase: PrimitivePhase::AfterChildren,
75                node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
76                    node_id,
77                    rect: Rect {
78                        x: padding / 2.0,
79                        y: padding / 4.0,
80                        width: text_width,
81                        height: text_height,
82                    },
83                    text: std::rc::Rc::new(cranpose_ui::text::AnnotatedString::from(text)),
84                    text_style: cranpose_ui::TextStyle::default(),
85                    font_size,
86                    layout_options: cranpose_ui::TextLayoutOptions::default(),
87                    clip: None,
88                })),
89            }),
90        ],
91        ..Default::default()
92    };
93    overlay_layer.recompute_raster_cache_hashes();
94
95    let mut graph = RenderGraph::new(LayerNode {
96        local_bounds: Rect::from_size(viewport),
97        children: vec![RenderNode::Layer(Box::new(overlay_layer))],
98        ..Default::default()
99    });
100    graph.root.recompute_raster_cache_hashes();
101    graph
102}