Skip to main content

cranpose_render_wgpu/
scene.rs

1//! Scene structures for GPU rendering
2
3use cranpose_core::{run_in_mutable_snapshot, NodeId};
4use cranpose_foundation::{PointerEvent, PointerEventKind};
5use cranpose_render_common::{HitTestTarget, RenderScene};
6use cranpose_ui_graphics::{Brush, Color, Point, Rect, RoundedCornerShape};
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11#[derive(Clone)]
12pub struct DrawShape {
13    pub rect: Rect,
14    pub brush: Brush,
15    pub shape: Option<RoundedCornerShape>,
16    pub z_index: usize,
17    pub clip: Option<Rect>,
18}
19
20#[derive(Clone)]
21pub struct TextDraw {
22    pub node_id: NodeId,
23    pub rect: Rect,
24    pub text: Rc<str>,
25    pub color: Color,
26    pub scale: f32,
27    pub z_index: usize,
28    pub clip: Option<Rect>,
29}
30
31#[derive(Clone)]
32pub enum ClickAction {
33    Simple(Rc<RefCell<dyn FnMut()>>),
34    WithPoint(Rc<dyn Fn(Point)>),
35}
36
37impl ClickAction {
38    pub(crate) fn invoke(&self, rect: Rect, x: f32, y: f32) {
39        match self {
40            ClickAction::Simple(handler) => (handler.borrow_mut())(),
41            ClickAction::WithPoint(handler) => handler(Point {
42                x: x - rect.x,
43                y: y - rect.y,
44            }),
45        }
46    }
47}
48
49#[derive(Clone)]
50pub struct HitRegion {
51    pub node_id: NodeId,
52    pub rect: Rect,
53    pub shape: Option<RoundedCornerShape>,
54    pub click_actions: Vec<ClickAction>,
55    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
56    pub z_index: usize,
57    pub hit_clip: Option<Rect>,
58}
59
60impl HitTestTarget for HitRegion {
61    fn dispatch(&self, event: PointerEvent) {
62        let x = event.global_position.x;
63        let y = event.global_position.y;
64        let kind = event.kind;
65
66        let local = Point {
67            x: x - self.rect.x,
68            y: y - self.rect.y,
69        };
70
71        let local_event = event.copy_with_local_position(local);
72
73        let has_pointer_inputs = !self.pointer_inputs.is_empty();
74        let has_click_actions = kind == PointerEventKind::Down && !self.click_actions.is_empty();
75
76        if !has_pointer_inputs && !has_click_actions {
77            return;
78        }
79
80        if let Err(err) = run_in_mutable_snapshot(|| {
81            for handler in self.pointer_inputs.iter() {
82                // If consumed by a previous handler in this loop (or outer loop), stop.
83                if local_event.is_consumed() {
84                    break;
85                }
86                handler(local_event.clone());
87            }
88
89            // Only perform click actions if NOT consumed
90            if kind == PointerEventKind::Down && !local_event.is_consumed() {
91                for action in &self.click_actions {
92                    action.invoke(self.rect, x, y);
93                }
94            }
95        }) {
96            log::error!(
97                "failed to apply mutable snapshot for pointer event {:?} at ({}, {}): {}",
98                kind,
99                x,
100                y,
101                err
102            );
103        }
104    }
105
106    fn node_id(&self) -> NodeId {
107        self.node_id
108    }
109}
110
111impl HitRegion {
112    pub fn contains(&self, x: f32, y: f32) -> bool {
113        if let Some(clip) = self.hit_clip {
114            if !clip.contains(x, y) {
115                return false;
116            }
117        }
118        if let Some(shape) = self.shape {
119            point_in_rounded_rect(x, y, self.rect, shape)
120        } else {
121            self.rect.contains(x, y)
122        }
123    }
124}
125
126pub struct Scene {
127    pub shapes: Vec<DrawShape>,
128    pub texts: Vec<TextDraw>,
129    pub hits: Vec<HitRegion>,
130    /// Index for O(1) node lookup by NodeId
131    node_index: HashMap<NodeId, HitRegion>,
132    next_z: usize,
133}
134
135impl Scene {
136    pub fn new() -> Self {
137        Self {
138            shapes: Vec::new(),
139            texts: Vec::new(),
140            hits: Vec::new(),
141            node_index: HashMap::new(),
142            next_z: 0,
143        }
144    }
145
146    pub fn push_shape(
147        &mut self,
148        rect: Rect,
149        brush: Brush,
150        shape: Option<RoundedCornerShape>,
151        clip: Option<Rect>,
152    ) {
153        let z_index = self.next_z;
154        self.next_z += 1;
155        self.shapes.push(DrawShape {
156            rect,
157            brush,
158            shape,
159            z_index,
160            clip,
161        });
162    }
163
164    pub fn push_text(
165        &mut self,
166        node_id: NodeId,
167        rect: Rect,
168        text: Rc<str>,
169        color: Color,
170        scale: f32,
171        clip: Option<Rect>,
172    ) {
173        let z_index = self.next_z;
174        self.next_z += 1;
175        self.texts.push(TextDraw {
176            node_id,
177            rect,
178            text,
179            color,
180            scale,
181            z_index,
182            clip,
183        });
184    }
185
186    pub fn push_hit(
187        &mut self,
188        node_id: NodeId,
189        rect: Rect,
190        shape: Option<RoundedCornerShape>,
191        click_actions: Vec<ClickAction>,
192        pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
193        hit_clip: Option<Rect>,
194    ) {
195        if click_actions.is_empty() && pointer_inputs.is_empty() {
196            return;
197        }
198        let z_index = self.next_z;
199        self.next_z += 1;
200        let hit_region = HitRegion {
201            node_id,
202            rect,
203            shape,
204            click_actions,
205            pointer_inputs,
206            z_index,
207            hit_clip,
208        };
209        // Populate both the list and the index for O(1) lookup
210        self.node_index.insert(node_id, hit_region.clone());
211        self.hits.push(hit_region);
212    }
213}
214
215impl Default for Scene {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl RenderScene for Scene {
222    type HitTarget = HitRegion;
223
224    fn clear(&mut self) {
225        self.shapes.clear();
226        self.texts.clear();
227        self.hits.clear();
228        self.node_index.clear();
229        self.next_z = 0;
230    }
231
232    fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
233        let mut hits: Vec<_> = self
234            .hits
235            .iter()
236            .filter(|hit| hit.contains(x, y))
237            .cloned()
238            .collect();
239
240        // Sort by z-index descending (top to bottom)
241        hits.sort_by(|a, b| b.z_index.cmp(&a.z_index));
242        hits
243    }
244
245    fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
246        // O(1) lookup using the node index
247        self.node_index.get(&node_id).cloned()
248    }
249}
250
251// Helper function for rounded rectangle hit testing
252fn point_in_rounded_rect(x: f32, y: f32, rect: Rect, shape: RoundedCornerShape) -> bool {
253    if !rect.contains(x, y) {
254        return false;
255    }
256
257    let local_x = x - rect.x;
258    let local_y = y - rect.y;
259
260    // Check corners
261    let radii = shape.resolve(rect.width, rect.height);
262    let tl = radii.top_left;
263    let tr = radii.top_right;
264    let bl = radii.bottom_left;
265    let br = radii.bottom_right;
266
267    // Top-left corner
268    if local_x < tl && local_y < tl {
269        let dx = tl - local_x;
270        let dy = tl - local_y;
271        return dx * dx + dy * dy <= tl * tl;
272    }
273
274    // Top-right corner
275    if local_x > rect.width - tr && local_y < tr {
276        let dx = local_x - (rect.width - tr);
277        let dy = tr - local_y;
278        return dx * dx + dy * dy <= tr * tr;
279    }
280
281    // Bottom-left corner
282    if local_x < bl && local_y > rect.height - bl {
283        let dx = bl - local_x;
284        let dy = local_y - (rect.height - bl);
285        return dx * dx + dy * dy <= bl * bl;
286    }
287
288    // Bottom-right corner
289    if local_x > rect.width - br && local_y > rect.height - br {
290        let dx = local_x - (rect.width - br);
291        let dy = local_y - (rect.height - br);
292        return dx * dx + dy * dy <= br * br;
293    }
294
295    true
296}