Skip to main content

apace_dom/
top_layer.rs

1//! Document-owned top layers retain DOM/style ancestry but establish independent
2//! viewport layout roots. Hosts own opening, placement, focus and dismissal policy.
3use crate::layout::damage::ALL_DAMAGE;
4use crate::{BaseDocument, NodeId, node::NodeFlags, util::Color};
5use style::invalidation::element::restyle_hints::RestyleHint;
6use style_dom::ElementState;
7
8pub const MAX_TOP_LAYERS: usize = 1024;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
11pub struct TopLayerConfig {
12    /// A modal layer catches hits on its backdrop instead of falling through.
13    pub modal: bool,
14    /// Stop unconsumed user scroll at this root, including editor/touch/fling
15    /// chains, while retaining logical DOM ancestry for style and events.
16    pub contain_scroll: bool,
17    /// Optional viewport backdrop, painted immediately before this layer.
18    pub backdrop: Option<Color>,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct TopLayer {
23    pub node_id: NodeId,
24    pub config: TopLayerConfig,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum TopLayerError {
29    MissingNode,
30    InvalidTarget,
31    InvalidBackdrop,
32    Capacity,
33}
34
35impl BaseDocument {
36    pub fn top_layers(&self) -> &[TopLayer] {
37        &self.top_layers
38    }
39
40    /// A top layer does not escape a display:none ancestor. Its lease remains
41    /// current while hidden, but neither its box nor its backdrop participates.
42    pub fn top_layer_is_rendered(&self, node_id: NodeId) -> bool {
43        let mut cursor = Some(node_id);
44        while let Some(id) = cursor {
45            let Some(node) = self.nodes.get(id) else {
46                return false;
47            };
48            if !node.flags.is_in_document() {
49                return false;
50            }
51            if node.is_element() && node.primary_styles().is_none() {
52                return false;
53            }
54            if node.display_style().is_some_and(|display| {
55                display.inside() == style::values::specified::box_::DisplayInside::None
56            }) {
57                return false;
58            }
59            cursor = node.parent;
60        }
61        true
62    }
63
64    /// Opening appends once; updating an open entry preserves its order. Closing
65    /// and reopening moves it to the front of the visual stack. Validation does
66    /// not mutate the tree, including when admission exceeds the live budget.
67    pub fn set_top_layer(
68        &mut self,
69        node_id: NodeId,
70        config: Option<TopLayerConfig>,
71    ) -> Result<bool, TopLayerError> {
72        let node = self.nodes.get(node_id).ok_or(TopLayerError::MissingNode)?;
73        if config
74            .and_then(|c| c.backdrop)
75            .is_some_and(|color| color.components.iter().any(|v| !v.is_finite()))
76        {
77            return Err(TopLayerError::InvalidBackdrop);
78        }
79        let index = self
80            .top_layers
81            .iter()
82            .position(|entry| entry.node_id == node_id);
83        if config.is_some()
84            && (!node.is_element()
85                || !node.flags.is_in_document()
86                || self
87                    .try_root_element()
88                    .is_some_and(|root| root.id == node_id))
89        {
90            return Err(TopLayerError::InvalidTarget);
91        }
92        if config.is_some() && index.is_none() && self.top_layers.len() >= MAX_TOP_LAYERS {
93            return Err(TopLayerError::Capacity);
94        }
95        if index.map(|i| self.top_layers[i].config) == config {
96            return Ok(false);
97        }
98        match (index, config) {
99            (Some(i), Some(config)) => self.top_layers[i].config = config,
100            (Some(i), None) => {
101                self.top_layers.remove(i);
102            }
103            (None, Some(config)) => self.top_layers.push(TopLayer { node_id, config }),
104            (None, None) => return Ok(false),
105        }
106        self.update_top_layer_state(node_id, config);
107        self.changed_nodes.insert(node_id);
108        self.shell_provider.request_redraw();
109        Ok(true)
110    }
111
112    pub(crate) fn retire_top_layer(&mut self, node_id: NodeId) {
113        if let Some(index) = self
114            .top_layers
115            .iter()
116            .position(|entry| entry.node_id == node_id)
117        {
118            self.top_layers.remove(index);
119            self.update_top_layer_state(node_id, None);
120        }
121    }
122
123    fn update_top_layer_state(&mut self, node_id: NodeId, config: Option<TopLayerConfig>) {
124        let state = ElementState::POPOVER_OPEN | ElementState::MODAL;
125        self.snapshot_node_and(node_id, state, |node| {
126            node.flags.set(NodeFlags::IS_TOP_LAYER, config.is_some());
127            node.flags.set(
128                NodeFlags::CONTAIN_TOP_LAYER_SCROLL,
129                config.is_some_and(|c| c.contain_scroll),
130            );
131            let element_state = node.element_state_mut();
132            element_state.set(ElementState::POPOVER_OPEN, config.is_some());
133            element_state.set(ElementState::MODAL, config.is_some_and(|c| c.modal));
134            node.insert_damage(ALL_DAMAGE);
135            node.set_restyle_hint(RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS);
136            node.mark_ancestors_dirty();
137        });
138        // Parent box construction must remove/reinsert the root without ever
139        // changing its DOM parent or inherited style.
140        if let Some(parent) = self.nodes[node_id].parent {
141            self.nodes[parent].insert_damage(ALL_DAMAGE);
142        }
143        self.nodes[node_id].oof_containing_block.set(None);
144        self.nodes[node_id].layout_parent.set(None);
145    }
146}