Skip to main content

teksilo_scene/
flags.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Per-item behavior flags.
5//!
6//! [`ItemFlags`] is a bitset packed into a `u32`. Each flag opts an
7//! item into a behavior — drag-to-move participation, hit-test
8//! response, rendering visibility, transform inheritance — that
9//! the Scene and SceneView consult at the relevant pipeline stage.
10//!
11//! Defaults: `IS_VISIBLE | IS_ENABLED | IS_SELECTABLE`. An item
12//! constructed via the standard built-in builders gets these
13//! defaults; setters layer additional flags on top.
14
15/// A bitset of per-item behavior flags.
16///
17/// Use [`ItemFlags::default`] for the standard "interactive,
18/// visible, selectable" baseline. Compose flags with `|` and toggle
19/// them with [`ItemFlags::set`] / [`ItemFlags::contains`].
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ItemFlags(u32);
22
23impl ItemFlags {
24    /// Empty bitset — no flags set.
25    pub const NONE: Self = Self(0);
26
27    /// Item paints and is hit-tested. Default on. Clearing this is
28    /// the equivalent of Qt's `setVisible(false)` — the item is
29    /// neither painted nor hit-tested. Children of an invisible
30    /// item are also effectively invisible.
31    pub const IS_VISIBLE: Self = Self(1 << 0);
32
33    /// Item dispatches pointer events. Default on. Disabled items
34    /// are still painted but pass clicks through to items beneath.
35    pub const IS_ENABLED: Self = Self(1 << 1);
36
37    /// Item participates in drag-to-move. Default off.
38    pub const IS_DRAGGABLE: Self = Self(1 << 2);
39
40    /// Item is included in marquee box-select results. Default on.
41    pub const IS_SELECTABLE: Self = Self(1 << 3);
42
43    /// Item can take keyboard focus. Default off. Declared only: the
44    /// built-in traversal (`SceneView::focus_in_direction`) walks every
45    /// item in insertion order, and a `focus_order` callback is handed
46    /// the whole `Scene` and filters for itself — nothing reads this bit.
47    pub const IS_FOCUSABLE: Self = Self(1 << 4);
48
49    /// Item dispatches hover events (Qt `setAcceptHoverEvents`).
50    /// Default off. Declared only: the view dispatches hover from the
51    /// presence of a `SceneItemHandlerSet::on_hover` callback, and
52    /// nothing sets or reads this bit.
53    pub const ACCEPTS_HOVER: Self = Self(1 << 5);
54
55    /// Item's paint output is clipped to its `local_bounds`.
56    /// Default off.
57    pub const CLIPS_TO_SHAPE: Self = Self(1 << 6);
58
59    /// Children are clipped to this item's `local_bounds`. Default
60    /// off; mirrors Qt's `ItemClipsChildrenToShape`.
61    pub const CLIPS_CHILDREN_TO_SHAPE: Self = Self(1 << 7);
62
63    /// Item paints and hit-tests at a fixed pixel size, independent
64    /// of the view's zoom and rotation. Its anchor (the item's
65    /// parent-relative scene point) is projected through the view
66    /// transform like any other point, so the visible position
67    /// follows pan/zoom and tracks the underlying scene data —
68    /// but the item itself does not grow with zoom or rotate with
69    /// the view. Mirrors Qt's `ItemIgnoresTransformations`.
70    /// Annotation pins for graph editors, fixed-pixel-size badges
71    /// over moving content, chart axis labels. Default off.
72    pub const IGNORES_TRANSFORMATIONS: Self = Self(1 << 8);
73
74    /// Item has nothing to paint — the paint walk skips it
75    /// entirely. Pure logical-only containers (used for AT
76    /// grouping or hit-test routing) set this. Default off.
77    pub const HAS_NO_CONTENTS: Self = Self(1 << 9);
78
79    // Bit 10 was `NEGATIVE_Z_BEHIND_PARENT` (Qt's
80    // `ItemNegativeZStacksBehindParent`): "children with `z < 0` paint behind
81    // this item rather than in front". It was declared, documented and read by
82    // nothing, in either tier, for the flag's whole life.
83    //
84    // It is **deleted** rather than implemented, and the reason is this file's
85    // neighbour, `pick.rs`. The scene's paint order is now one flat, total
86    // `PaintKey`, and that is what both paint and every hit test compare —
87    // which is the whole point of it. Parent-relative z cannot be expressed in
88    // a flat key: honouring this flag would mean a hierarchical order (sort
89    // within each parent, then recurse, Qt's actual algorithm), and that is a
90    // different ordering model for every parented scene, not a bit to switch
91    // on. Leaving a declared-but-dead flag standing in the one place whose job
92    // is "one order, no silent disagreements" would be exactly the kind of lie
93    // this module exists to remove.
94    //
95    // Nothing else moved: the remaining bits keep their values, so a
96    // `from_bits` round-trip of any previously-valid bitset is unchanged.
97
98    /// Item offers **resize** handles to a selection transform controller.
99    /// Default off, like [`IS_DRAGGABLE`](Self::IS_DRAGGABLE).
100    ///
101    /// A resize writes the item's `local_bounds`, so the item reflows into the
102    /// new box — a heavyweight card relayouts, a `RectItem` redraws at the new
103    /// size, a `PathItem` fits its geometry to it. Nothing is scaled visually
104    /// and then baked.
105    pub const IS_RESIZABLE: Self = Self(1 << 11);
106
107    /// Item offers a **rotate** handle to a selection transform controller.
108    /// Default off.
109    ///
110    /// Honoured for the lightweight tier only. A heavyweight entry carrying it
111    /// is refused, because `place_children` sizes a card from the AABB of its
112    /// transformed bounds: a rotation there inflates the layout box and rotates
113    /// nothing. See `SceneView::transform_controller`.
114    pub const IS_ROTATABLE: Self = Self(1 << 12);
115
116    /// A resize of this item keeps its aspect ratio whatever the controller's
117    /// `keep_ratio` setting says. Default off.
118    pub const ASPECT_LOCKED: Self = Self(1 << 13);
119
120    /// Whether the bitset contains every flag in `other`.
121    pub const fn contains(&self, other: Self) -> bool {
122        (self.0 & other.0) == other.0
123    }
124
125    /// Whether the bitset shares any flags with `other`.
126    pub const fn intersects(&self, other: Self) -> bool {
127        (self.0 & other.0) != 0
128    }
129
130    /// Set (when `on`) or clear (when `!on`) the bits in `flag`.
131    pub fn set(&mut self, flag: Self, on: bool) {
132        if on {
133            self.0 |= flag.0;
134        } else {
135            self.0 &= !flag.0;
136        }
137    }
138
139    /// Set the bits in `flag`, returning the new bitset.
140    pub const fn with(self, flag: Self) -> Self {
141        Self(self.0 | flag.0)
142    }
143
144    /// Clear the bits in `flag`, returning the new bitset.
145    pub const fn without(self, flag: Self) -> Self {
146        Self(self.0 & !flag.0)
147    }
148
149    /// Raw `u32` bits (debug / serialization).
150    pub const fn bits(self) -> u32 {
151        self.0
152    }
153
154    /// Construct from raw bits.
155    pub const fn from_bits(bits: u32) -> Self {
156        Self(bits)
157    }
158}
159
160impl Default for ItemFlags {
161    /// `IS_VISIBLE | IS_ENABLED | IS_SELECTABLE`.
162    fn default() -> Self {
163        Self::IS_VISIBLE
164            .with(Self::IS_ENABLED)
165            .with(Self::IS_SELECTABLE)
166    }
167}
168
169impl std::ops::BitOr for ItemFlags {
170    type Output = Self;
171    fn bitor(self, rhs: Self) -> Self {
172        Self(self.0 | rhs.0)
173    }
174}
175
176impl std::ops::BitOrAssign for ItemFlags {
177    fn bitor_assign(&mut self, rhs: Self) {
178        self.0 |= rhs.0;
179    }
180}
181
182impl std::ops::BitAnd for ItemFlags {
183    type Output = Self;
184    fn bitand(self, rhs: Self) -> Self {
185        Self(self.0 & rhs.0)
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn default_carries_visible_enabled_selectable() {
195        let f = ItemFlags::default();
196        assert!(f.contains(ItemFlags::IS_VISIBLE));
197        assert!(f.contains(ItemFlags::IS_ENABLED));
198        assert!(f.contains(ItemFlags::IS_SELECTABLE));
199        assert!(!f.contains(ItemFlags::IS_DRAGGABLE));
200        assert!(!f.contains(ItemFlags::IS_FOCUSABLE));
201        assert!(!f.contains(ItemFlags::IS_RESIZABLE));
202        assert!(!f.contains(ItemFlags::IS_ROTATABLE));
203        assert!(!f.contains(ItemFlags::ASPECT_LOCKED));
204    }
205
206    #[test]
207    fn set_toggles_individual_bits() {
208        let mut f = ItemFlags::default();
209        f.set(ItemFlags::IS_DRAGGABLE, true);
210        assert!(f.contains(ItemFlags::IS_DRAGGABLE));
211        f.set(ItemFlags::IS_VISIBLE, false);
212        assert!(!f.contains(ItemFlags::IS_VISIBLE));
213        assert!(f.contains(ItemFlags::IS_ENABLED));
214    }
215
216    #[test]
217    fn with_without_round_trip() {
218        let f = ItemFlags::default()
219            .with(ItemFlags::IS_DRAGGABLE)
220            .with(ItemFlags::IGNORES_TRANSFORMATIONS);
221        assert!(f.contains(ItemFlags::IS_DRAGGABLE));
222        assert!(f.contains(ItemFlags::IGNORES_TRANSFORMATIONS));
223        let f = f.without(ItemFlags::IS_DRAGGABLE);
224        assert!(!f.contains(ItemFlags::IS_DRAGGABLE));
225        assert!(f.contains(ItemFlags::IGNORES_TRANSFORMATIONS));
226    }
227
228    #[test]
229    fn intersects_detects_any_overlap() {
230        let f = ItemFlags::IS_VISIBLE | ItemFlags::IS_DRAGGABLE;
231        assert!(f.intersects(ItemFlags::IS_DRAGGABLE));
232        assert!(f.intersects(ItemFlags::IS_VISIBLE | ItemFlags::IS_ENABLED));
233        assert!(!f.intersects(ItemFlags::IS_FOCUSABLE));
234    }
235}