Skip to main content

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}