Skip to main content

azul_layout/managers/
iframe.rs

1//! IFrame lifecycle management for layout
2//!
3//! This module provides:
4//! - IFrame re-invocation logic for lazy loading
5//! - WebRender PipelineId tracking
6//! - Nested DOM ID management
7
8use alloc::collections::BTreeMap;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11use azul_core::{
12    callbacks::{EdgeType, IFrameCallbackReason},
13    dom::{DomId, NodeId},
14    geom::{LogicalPosition, LogicalRect, LogicalSize},
15    hit_test::PipelineId,
16};
17
18use crate::managers::scroll_state::ScrollManager;
19
20static NEXT_PIPELINE_ID: AtomicUsize = AtomicUsize::new(1);
21
22/// Distance in pixels from edge that triggers edge-scrolled callback
23const EDGE_THRESHOLD: f32 = 200.0;
24
25/// Manages IFrame lifecycle, including re-invocation and PipelineId generation
26///
27/// Tracks which IFrames have been invoked, assigns unique DOM IDs to nested
28/// IFrames, and determines when IFrames need to be re-invoked (e.g., when
29/// the container bounds expand or the user scrolls near an edge).
30#[derive(Debug, Clone, Default)]
31pub struct IFrameManager {
32    /// Per-IFrame state keyed by (parent DomId, NodeId of iframe element)
33    states: BTreeMap<(DomId, NodeId), IFrameState>,
34    /// WebRender PipelineId for each IFrame
35    pipeline_ids: BTreeMap<(DomId, NodeId), PipelineId>,
36    /// Counter for generating unique nested DOM IDs
37    next_dom_id: usize,
38}
39
40/// Internal state for a single IFrame instance
41///
42/// Tracks invocation status, content dimensions, and edge triggers
43/// to determine when the IFrame callback needs to be re-invoked.
44#[derive(Debug, Clone)]
45struct IFrameState {
46    /// Content size reported by IFrame callback (actual rendered size)
47    iframe_scroll_size: Option<LogicalSize>,
48    /// Virtual scroll size for infinite scroll scenarios
49    iframe_virtual_scroll_size: Option<LogicalSize>,
50    /// Whether the IFrame has ever been invoked
51    iframe_was_invoked: bool,
52    /// Whether invoked for current container expansion
53    invoked_for_current_expansion: bool,
54    /// Whether invoked for current edge scroll event
55    invoked_for_current_edge: bool,
56    /// Which edges have already triggered callbacks
57    last_edge_triggered: EdgeFlags,
58    /// Unique DOM ID assigned to this IFrame's content
59    nested_dom_id: DomId,
60    /// Last known layout bounds of the IFrame container
61    last_bounds: LogicalRect,
62}
63
64/// Flags indicating which scroll edges have been triggered
65///
66/// Used to prevent repeated edge-scroll callbacks for the same edge
67/// until the user scrolls away and back.
68#[derive(Debug, Clone, Copy, PartialEq, Default)]
69pub struct EdgeFlags {
70    /// Near top edge
71    pub top: bool,
72    /// Near bottom edge
73    pub bottom: bool,
74    /// Near left edge
75    pub left: bool,
76    /// Near right edge
77    pub right: bool,
78}
79
80impl IFrameManager {
81    /// Creates a new IFrameManager with no tracked IFrames
82    pub fn new() -> Self {
83        Self {
84            next_dom_id: 1, // 0 is root
85            ..Default::default()
86        }
87    }
88
89    /// Called at the start of each frame (currently a no-op)
90    pub fn begin_frame(&mut self) {
91        // Nothing to do here for now, but good practice for stateful managers
92    }
93
94    /// Gets or creates a unique nested DOM ID for an IFrame
95    ///
96    /// Returns the existing DOM ID if the IFrame was previously registered,
97    /// otherwise allocates a new unique ID and initializes the IFrame state.
98    pub fn get_or_create_nested_dom_id(&mut self, dom_id: DomId, node_id: NodeId) -> DomId {
99        let key = (dom_id, node_id);
100
101        // Check if already exists
102        if let Some(state) = self.states.get(&key) {
103            return state.nested_dom_id;
104        }
105
106        // Create new nested DOM ID
107        let nested_dom_id = DomId {
108            inner: self.next_dom_id,
109        };
110        self.next_dom_id += 1;
111
112        self.states.insert(key, IFrameState::new(nested_dom_id));
113        nested_dom_id
114    }
115
116    /// Gets the nested DOM ID for an IFrame if it exists
117    pub fn get_nested_dom_id(&self, dom_id: DomId, node_id: NodeId) -> Option<DomId> {
118        self.states.get(&(dom_id, node_id)).map(|s| s.nested_dom_id)
119    }
120
121    /// Gets or creates a WebRender PipelineId for an IFrame
122    ///
123    /// PipelineIds are used by WebRender to identify distinct rendering contexts.
124    pub fn get_or_create_pipeline_id(&mut self, dom_id: DomId, node_id: NodeId) -> PipelineId {
125        *self
126            .pipeline_ids
127            .entry((dom_id, node_id))
128            .or_insert_with(|| PipelineId(dom_id.inner as u32, node_id.index() as u32))
129    }
130
131    /// Returns whether the IFrame has ever been invoked
132    pub fn was_iframe_invoked(&self, dom_id: DomId, node_id: NodeId) -> bool {
133        self.states
134            .get(&(dom_id, node_id))
135            .map(|s| s.iframe_was_invoked)
136            .unwrap_or(false)
137    }
138
139    /// Updates the IFrame's content size information
140    ///
141    /// Called after the IFrame callback returns to record the actual content
142    /// dimensions. If the new size is larger than previously recorded, clears
143    /// the expansion flag to allow BoundsExpanded re-invocation.
144    pub fn update_iframe_info(
145        &mut self,
146        dom_id: DomId,
147        node_id: NodeId,
148        scroll_size: LogicalSize,
149        virtual_scroll_size: LogicalSize,
150    ) -> Option<()> {
151        let state = self.states.get_mut(&(dom_id, node_id))?;
152
153        // Reset expansion flag if content grew
154        if let Some(old_size) = state.iframe_scroll_size {
155            if scroll_size.width > old_size.width || scroll_size.height > old_size.height {
156                state.invoked_for_current_expansion = false;
157            }
158        }
159        state.iframe_scroll_size = Some(scroll_size);
160        state.iframe_virtual_scroll_size = Some(virtual_scroll_size);
161
162        Some(())
163    }
164
165    /// Marks an IFrame as invoked for a specific reason
166    ///
167    /// Updates internal state flags based on the callback reason to prevent
168    /// duplicate callbacks for the same trigger condition.
169    pub fn mark_invoked(
170        &mut self,
171        dom_id: DomId,
172        node_id: NodeId,
173        reason: IFrameCallbackReason,
174    ) -> Option<()> {
175        let state = self.states.get_mut(&(dom_id, node_id))?;
176
177        state.iframe_was_invoked = true;
178        match reason {
179            IFrameCallbackReason::BoundsExpanded => state.invoked_for_current_expansion = true,
180            IFrameCallbackReason::EdgeScrolled(edge) => {
181                state.invoked_for_current_edge = true;
182                state.last_edge_triggered = edge.into();
183            }
184            _ => {}
185        }
186
187        Some(())
188    }
189
190    /// Force an IFrame to be re-invoked on the next layout pass
191    ///
192    /// Clears all invocation flags, causing check_reinvoke() to return InitialRender.
193    /// Used by trigger_iframe_rerender() to manually refresh IFrame content.
194    pub fn force_reinvoke(&mut self, dom_id: DomId, node_id: NodeId) -> Option<()> {
195        let state = self.states.get_mut(&(dom_id, node_id))?;
196
197        state.iframe_was_invoked = false;
198        state.invoked_for_current_expansion = false;
199        state.invoked_for_current_edge = false;
200
201        Some(())
202    }
203
204    /// Checks whether an IFrame needs to be re-invoked and returns the reason
205    ///
206    /// Returns `Some(reason)` if the IFrame callback should be invoked:
207    /// - `InitialRender`: IFrame has never been invoked
208    /// - `BoundsExpanded`: Container grew larger than content
209    /// - `EdgeScrolled`: User scrolled near an edge (for lazy loading)
210    ///
211    /// Returns `None` if no re-invocation is needed.
212    pub fn check_reinvoke(
213        &mut self,
214        dom_id: DomId,
215        node_id: NodeId,
216        scroll_manager: &ScrollManager,
217        layout_bounds: LogicalRect,
218    ) -> Option<IFrameCallbackReason> {
219        let state = self.states.entry((dom_id, node_id)).or_insert_with(|| {
220            let nested_dom_id = DomId {
221                inner: self.next_dom_id,
222            };
223            self.next_dom_id += 1;
224            IFrameState::new(nested_dom_id)
225        });
226
227        if !state.iframe_was_invoked {
228            return Some(IFrameCallbackReason::InitialRender);
229        }
230
231        // Check for bounds expansion
232        if layout_bounds.size.width > state.last_bounds.size.width
233            || layout_bounds.size.height > state.last_bounds.size.height
234        {
235            state.invoked_for_current_expansion = false;
236        }
237        state.last_bounds = layout_bounds;
238
239        let scroll_offset = scroll_manager
240            .get_current_offset(dom_id, node_id)
241            .unwrap_or_default();
242
243        state.check_reinvoke_condition(scroll_offset, layout_bounds.size)
244    }
245}
246
247impl IFrameState {
248    /// Creates a new IFrameState with the given nested DOM ID
249    fn new(nested_dom_id: DomId) -> Self {
250        Self {
251            iframe_scroll_size: None,
252            iframe_virtual_scroll_size: None,
253            iframe_was_invoked: false,
254            invoked_for_current_expansion: false,
255            invoked_for_current_edge: false,
256            last_edge_triggered: EdgeFlags::default(),
257            nested_dom_id,
258            last_bounds: LogicalRect::zero(),
259        }
260    }
261
262    /// Determines if the IFrame callback should be re-invoked based on
263    // scroll position
264    ///
265    /// Checks two conditions:
266    /// 1. Container bounds expanded beyond content size
267    /// 2. User scrolled within EDGE_THRESHOLD pixels of an edge (for lazy loading)
268    fn check_reinvoke_condition(
269        &mut self,
270        current_offset: LogicalPosition,
271        container_size: LogicalSize,
272    ) -> Option<IFrameCallbackReason> {
273        // Need scroll_size to determine if we can scroll at all
274        let Some(scroll_size) = self.iframe_scroll_size else {
275            return None;
276        };
277
278        // Check 1: Container grew larger than content - need more content
279        if !self.invoked_for_current_expansion
280            && (container_size.width > scroll_size.width
281                || container_size.height > scroll_size.height)
282        {
283            return Some(IFrameCallbackReason::BoundsExpanded);
284        }
285
286        // Check 2: Edge-based lazy loading
287        // Determine if scrolling is possible in each direction
288        let scrollable_width = scroll_size.width > container_size.width;
289        let scrollable_height = scroll_size.height > container_size.height;
290
291        // Calculate which edges the user is currently near
292        let current_edges = EdgeFlags {
293            top: scrollable_height && current_offset.y <= EDGE_THRESHOLD,
294            bottom: scrollable_height
295                && (scroll_size.height - container_size.height - current_offset.y)
296                    <= EDGE_THRESHOLD,
297            left: scrollable_width && current_offset.x <= EDGE_THRESHOLD,
298            right: scrollable_width
299                && (scroll_size.width - container_size.width - current_offset.x) <= EDGE_THRESHOLD,
300        };
301
302        // Trigger edge callback if near an edge that hasn't been triggered yet
303        // Prioritize bottom/right edges (common infinite scroll directions)
304        if !self.invoked_for_current_edge && current_edges.any() {
305            if current_edges.bottom && !self.last_edge_triggered.bottom {
306                return Some(IFrameCallbackReason::EdgeScrolled(EdgeType::Bottom));
307            }
308            if current_edges.right && !self.last_edge_triggered.right {
309                return Some(IFrameCallbackReason::EdgeScrolled(EdgeType::Right));
310            }
311        }
312
313        None
314    }
315}
316
317impl EdgeFlags {
318    /// Returns true if any edge flag is set
319    fn any(&self) -> bool {
320        self.top || self.bottom || self.left || self.right
321    }
322}
323
324impl From<EdgeType> for EdgeFlags {
325    fn from(edge: EdgeType) -> Self {
326        match edge {
327            EdgeType::Top => Self {
328                top: true,
329                ..Default::default()
330            },
331            EdgeType::Bottom => Self {
332                bottom: true,
333                ..Default::default()
334            },
335            EdgeType::Left => Self {
336                left: true,
337                ..Default::default()
338            },
339            EdgeType::Right => Self {
340                right: true,
341                ..Default::default()
342            },
343        }
344    }
345}