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 crate::graph::{
5    CachePolicy, DrawPrimitiveNode, IsolationReasons, LayerNode, PrimitiveEntry, PrimitiveNode,
6    PrimitivePhase, ProjectiveTransform, RenderGraph, RenderNode, TextPrimitiveNode,
7};
8use crate::raster_cache::LayerRasterCacheHashes;
9use cranpose_ui_graphics::{
10    Brush, Color, CornerRadii, DrawPrimitive, GraphicsLayer, Point, Rect, Size,
11};
12
13/// Cache key for the last-built overlay; rebuilding only on change keeps the
14/// overlay free when the text is stable.
15#[derive(Clone, PartialEq, Eq)]
16pub struct DevOverlayKey {
17    pub text: String,
18    pub viewport_width_bits: u32,
19    pub viewport_height_bits: u32,
20}
21
22impl DevOverlayKey {
23    pub fn new(text: &str, viewport: Size) -> Self {
24        Self {
25            text: text.to_string(),
26            viewport_width_bits: viewport.width.to_bits(),
27            viewport_height_bits: viewport.height.to_bits(),
28        }
29    }
30}
31
32/// Build the dev-overlay graph: a translucent pill with the stats text in
33/// the top-right corner, sized with fixed monospace-ish metrics so the
34/// overlay never depends on renderer text measurement.
35pub fn build_dev_overlay_graph(
36    text: &str,
37    viewport: Size,
38    node_id: cranpose_core::NodeId,
39) -> RenderGraph {
40    let padding = 8.0;
41    let font_size = 14.0;
42    let char_width = 7.0;
43    let text_width = text.len() as f32 * char_width;
44    let text_height = font_size * 1.4;
45    let x = (viewport.width - text_width - padding * 2.0).max(padding);
46    let y = padding;
47
48    let mut overlay_layer = LayerNode {
49        node_id: Some(node_id),
50        local_bounds: Rect {
51            x: 0.0,
52            y: 0.0,
53            width: text_width + padding,
54            height: text_height + padding / 2.0,
55        },
56        transform_to_parent: ProjectiveTransform::translation(x, y),
57        content_offset: Point::default(),
58        motion_context_animated: false,
59        translated_content_context: false,
60        translated_content_offset: Point::default(),
61        scene_children_origin: Point::default(),
62        scene_children_layer_translation: Point::default(),
63        graphics_layer: GraphicsLayer::default(),
64        clip_to_bounds: false,
65        shadow_clip: None,
66        hit_test: None,
67        has_hit_targets: false,
68        isolation: IsolationReasons::default(),
69        cache_policy: CachePolicy::None,
70        cache_hashes: LayerRasterCacheHashes::default(),
71        cache_hashes_valid: false,
72        children: vec![
73            RenderNode::Primitive(PrimitiveEntry {
74                phase: PrimitivePhase::BeforeChildren,
75                node: PrimitiveNode::Draw(DrawPrimitiveNode {
76                    primitive: DrawPrimitive::RoundRect {
77                        rect: Rect {
78                            x: 0.0,
79                            y: 0.0,
80                            width: text_width + padding,
81                            height: text_height + padding / 2.0,
82                        },
83                        brush: Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
84                        radii: CornerRadii::uniform(4.0),
85                    },
86                    clip: None,
87                }),
88            }),
89            RenderNode::Primitive(PrimitiveEntry {
90                phase: PrimitivePhase::AfterChildren,
91                node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
92                    node_id,
93                    rect: Rect {
94                        x: padding / 2.0,
95                        y: padding / 4.0,
96                        width: text_width,
97                        height: text_height,
98                    },
99                    text: cranpose_ui::text::AnnotatedString::from(text),
100                    text_style: cranpose_ui::TextStyle::default(),
101                    font_size,
102                    layout_options: cranpose_ui::TextLayoutOptions::default(),
103                    clip: None,
104                })),
105            }),
106        ],
107    };
108    overlay_layer.recompute_raster_cache_hashes();
109
110    let mut graph = RenderGraph::new(LayerNode {
111        node_id: None,
112        local_bounds: Rect::from_size(viewport),
113        transform_to_parent: ProjectiveTransform::identity(),
114        content_offset: Point::default(),
115        motion_context_animated: false,
116        translated_content_context: false,
117        translated_content_offset: Point::default(),
118        scene_children_origin: Point::default(),
119        scene_children_layer_translation: Point::default(),
120        graphics_layer: GraphicsLayer::default(),
121        clip_to_bounds: false,
122        shadow_clip: None,
123        hit_test: None,
124        has_hit_targets: false,
125        isolation: IsolationReasons::default(),
126        cache_policy: CachePolicy::None,
127        cache_hashes: LayerRasterCacheHashes::default(),
128        cache_hashes_valid: false,
129        children: vec![RenderNode::Layer(Box::new(overlay_layer))],
130    });
131    graph.root.recompute_raster_cache_hashes();
132    graph
133}