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