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