fission_core/ui/custom_render.rs
1//! Custom render object trait for CustomNode.
2//!
3//! Allows third-party or application-specific nodes to participate in
4//! hit-testing, event handling, and painting without requiring changes to the
5//! core IR enum variants.
6
7use crate::action::ActionEnvelope;
8use fission_ir::op::PaintOp;
9use fission_ir::{AnyRenderObject, NodeId};
10use fission_layout::{LayoutPoint, LayoutRect};
11use std::fmt::Debug;
12use std::sync::Arc;
13
14// ---------------------------------------------------------------------------
15// Result types
16// ---------------------------------------------------------------------------
17
18/// Result of a custom hit-test.
19///
20/// `byte_offset` is intentionally generic -- for a text-like custom node it is
21/// the byte offset into the content at the hit point; for other widgets it can
22/// be any application-defined index (or `None` when the point is simply
23/// "inside the widget").
24#[derive(Debug, Clone)]
25pub struct CustomHitResult {
26 /// Whether the point is inside the custom render object at all.
27 pub hit: bool,
28 /// Optional byte/content offset at the hit point.
29 pub byte_offset: Option<usize>,
30}
31
32impl CustomHitResult {
33 /// Convenience: the point was inside the node.
34 pub fn inside(byte_offset: Option<usize>) -> Self {
35 Self {
36 hit: true,
37 byte_offset,
38 }
39 }
40
41 /// Convenience: the point was outside the node.
42 pub fn miss() -> Self {
43 Self {
44 hit: false,
45 byte_offset: None,
46 }
47 }
48}
49
50/// Result of custom event handling.
51#[derive(Debug, Clone)]
52pub struct CustomEventResult {
53 /// If `true` the event was consumed and should not propagate further.
54 pub handled: bool,
55 /// Zero or more actions to dispatch as a consequence of the event.
56 pub actions: Vec<(NodeId, ActionEnvelope)>,
57}
58
59impl CustomEventResult {
60 /// The event was not consumed.
61 pub fn ignored() -> Self {
62 Self {
63 handled: false,
64 actions: Vec::new(),
65 }
66 }
67
68 /// The event was consumed with no resulting actions.
69 pub fn consumed() -> Self {
70 Self {
71 handled: true,
72 actions: Vec::new(),
73 }
74 }
75
76 /// The event was consumed and produced actions.
77 pub fn consumed_with(actions: Vec<(NodeId, ActionEnvelope)>) -> Self {
78 Self {
79 handled: true,
80 actions,
81 }
82 }
83}
84
85// ---------------------------------------------------------------------------
86// Trait
87// ---------------------------------------------------------------------------
88
89/// Extension point for custom nodes that need to participate in rendering,
90/// hit-testing, and event handling.
91///
92/// Implementors are stored behind `Arc<dyn CustomRenderObject>` so they must
93/// be `Send + Sync`. The trait is object-safe.
94pub trait CustomRenderObject: Send + Sync + Debug {
95 /// Whether this render object should be treated as runtime-dynamic by the
96 /// retained pipeline even when the surrounding widget tree is otherwise
97 /// static.
98 fn is_runtime_dynamic(&self) -> bool {
99 false
100 }
101
102 /// Whether this custom render object participates in text input / IME.
103 fn accepts_text_input(&self) -> bool {
104 false
105 }
106
107 /// Hit-test the custom content.
108 ///
109 /// `local_point` is relative to the top-left corner of the node's layout
110 /// rect. `node_rect` is the absolute layout rect for reference.
111 ///
112 /// The default implementation returns a hit whenever the point is inside
113 /// `node_rect`.
114 fn hit_test(&self, local_point: LayoutPoint, node_rect: LayoutRect) -> CustomHitResult {
115 let _ = local_point;
116 let _ = node_rect;
117 // By default any point that reached us (caller already checked bounds)
118 // is a hit with no offset information.
119 CustomHitResult::inside(None)
120 }
121
122 /// Handle an input event targeted at (or bubbling through) this node.
123 ///
124 /// `node_id` is the IR node that owns this render object.
125 /// `event` is the original input event.
126 ///
127 /// Returning `CustomEventResult { handled: true, .. }` prevents further
128 /// propagation through the standard controller chain.
129 fn handle_event(
130 &self,
131 node_id: NodeId,
132 event: &crate::event::InputEvent,
133 node_rect: LayoutRect,
134 ) -> CustomEventResult {
135 let _ = (node_id, event, node_rect);
136 CustomEventResult::ignored()
137 }
138
139 /// Platform IME cursor area for this render object, in absolute layout coordinates.
140 fn ime_cursor_area(&self, _node_rect: LayoutRect) -> Option<LayoutRect> {
141 None
142 }
143
144 /// Actions to dispatch if this render object loses focus.
145 fn blur_actions(&self, _node_id: NodeId) -> Vec<(NodeId, ActionEnvelope)> {
146 Vec::new()
147 }
148
149 /// Produce paint operations for this custom content.
150 ///
151 /// The returned `PaintOp`s are appended to the display list at the
152 /// position corresponding to this node. An empty vec means the node
153 /// paints nothing extra (it might still have children that paint).
154 fn paint(&self, node_rect: LayoutRect) -> Vec<PaintOp> {
155 let _ = node_rect;
156 Vec::new()
157 }
158}
159
160// ---------------------------------------------------------------------------
161// Type-erasure helpers for storing in CoreIR
162// ---------------------------------------------------------------------------
163
164/// Wrapper that allows `Arc<dyn CustomRenderObject>` to be stored as
165/// `Arc<dyn Any + Send + Sync>` inside the dependency-free `fission-ir` crate.
166#[derive(Debug, Clone)]
167pub struct RenderObjectHolder(pub Arc<dyn CustomRenderObject>);
168
169/// Try to recover an `Arc<dyn CustomRenderObject>` from an
170/// `AnyRenderObject` stored in `CoreIR::custom_render_objects`.
171///
172/// Returns `None` when the erased value is not a `RenderObjectHolder`.
173pub fn downcast_render_object(any: &AnyRenderObject) -> Option<&Arc<dyn CustomRenderObject>> {
174 any.downcast_ref::<RenderObjectHolder>()
175 .map(|holder| &holder.0)
176}