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; the focus_order
44    /// callback considers only items with this flag set.
45    pub const IS_FOCUSABLE: Self = Self(1 << 4);
46
47    /// Item dispatches hover events (Qt `setAcceptHoverEvents`).
48    /// Default off; hover handlers wired via `ItemBuilder::on_hover`
49    /// flip this on automatically.
50    pub const ACCEPTS_HOVER: Self = Self(1 << 5);
51
52    /// Item's paint output is clipped to its `local_bounds`.
53    /// Default off.
54    pub const CLIPS_TO_SHAPE: Self = Self(1 << 6);
55
56    /// Children are clipped to this item's `local_bounds`. Default
57    /// off; mirrors Qt's `ItemClipsChildrenToShape`.
58    pub const CLIPS_CHILDREN_TO_SHAPE: Self = Self(1 << 7);
59
60    /// Item paints and hit-tests at a fixed pixel size, independent
61    /// of the view's zoom and rotation. Its anchor (the item's
62    /// parent-relative scene point) is projected through the view
63    /// transform like any other point, so the visible position
64    /// follows pan/zoom and tracks the underlying scene data —
65    /// but the item itself does not grow with zoom or rotate with
66    /// the view. Mirrors Qt's `ItemIgnoresTransformations`.
67    /// Annotation pins for graph editors, fixed-pixel-size badges
68    /// over moving content, chart axis labels. Default off.
69    pub const IGNORES_TRANSFORMATIONS: Self = Self(1 << 8);
70
71    /// Item has nothing to paint — the paint walk skips it
72    /// entirely. Pure logical-only containers (used for AT
73    /// grouping or hit-test routing) set this. Default off.
74    pub const HAS_NO_CONTENTS: Self = Self(1 << 9);
75
76    /// Children with `z < 0` paint **behind** this item rather
77    /// than in front. Mirrors Qt's `ItemNegativeZStacksBehindParent`.
78    /// Default off.
79    pub const NEGATIVE_Z_BEHIND_PARENT: Self = Self(1 << 10);
80
81    /// Whether the bitset contains every flag in `other`.
82    pub const fn contains(&self, other: Self) -> bool {
83        (self.0 & other.0) == other.0
84    }
85
86    /// Whether the bitset shares any flags with `other`.
87    pub const fn intersects(&self, other: Self) -> bool {
88        (self.0 & other.0) != 0
89    }
90
91    /// Set (when `on`) or clear (when `!on`) the bits in `flag`.
92    pub fn set(&mut self, flag: Self, on: bool) {
93        if on {
94            self.0 |= flag.0;
95        } else {
96            self.0 &= !flag.0;
97        }
98    }
99
100    /// Set the bits in `flag`, returning the new bitset.
101    pub const fn with(self, flag: Self) -> Self {
102        Self(self.0 | flag.0)
103    }
104
105    /// Clear the bits in `flag`, returning the new bitset.
106    pub const fn without(self, flag: Self) -> Self {
107        Self(self.0 & !flag.0)
108    }
109
110    /// Raw `u32` bits (debug / serialization).
111    pub const fn bits(self) -> u32 {
112        self.0
113    }
114
115    /// Construct from raw bits.
116    pub const fn from_bits(bits: u32) -> Self {
117        Self(bits)
118    }
119}
120
121impl Default for ItemFlags {
122    /// `IS_VISIBLE | IS_ENABLED | IS_SELECTABLE`.
123    fn default() -> Self {
124        Self::IS_VISIBLE
125            .with(Self::IS_ENABLED)
126            .with(Self::IS_SELECTABLE)
127    }
128}
129
130impl std::ops::BitOr for ItemFlags {
131    type Output = Self;
132    fn bitor(self, rhs: Self) -> Self {
133        Self(self.0 | rhs.0)
134    }
135}
136
137impl std::ops::BitOrAssign for ItemFlags {
138    fn bitor_assign(&mut self, rhs: Self) {
139        self.0 |= rhs.0;
140    }
141}
142
143impl std::ops::BitAnd for ItemFlags {
144    type Output = Self;
145    fn bitand(self, rhs: Self) -> Self {
146        Self(self.0 & rhs.0)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn default_carries_visible_enabled_selectable() {
156        let f = ItemFlags::default();
157        assert!(f.contains(ItemFlags::IS_VISIBLE));
158        assert!(f.contains(ItemFlags::IS_ENABLED));
159        assert!(f.contains(ItemFlags::IS_SELECTABLE));
160        assert!(!f.contains(ItemFlags::IS_DRAGGABLE));
161        assert!(!f.contains(ItemFlags::IS_FOCUSABLE));
162    }
163
164    #[test]
165    fn set_toggles_individual_bits() {
166        let mut f = ItemFlags::default();
167        f.set(ItemFlags::IS_DRAGGABLE, true);
168        assert!(f.contains(ItemFlags::IS_DRAGGABLE));
169        f.set(ItemFlags::IS_VISIBLE, false);
170        assert!(!f.contains(ItemFlags::IS_VISIBLE));
171        assert!(f.contains(ItemFlags::IS_ENABLED));
172    }
173
174    #[test]
175    fn with_without_round_trip() {
176        let f = ItemFlags::default()
177            .with(ItemFlags::IS_DRAGGABLE)
178            .with(ItemFlags::IGNORES_TRANSFORMATIONS);
179        assert!(f.contains(ItemFlags::IS_DRAGGABLE));
180        assert!(f.contains(ItemFlags::IGNORES_TRANSFORMATIONS));
181        let f = f.without(ItemFlags::IS_DRAGGABLE);
182        assert!(!f.contains(ItemFlags::IS_DRAGGABLE));
183        assert!(f.contains(ItemFlags::IGNORES_TRANSFORMATIONS));
184    }
185
186    #[test]
187    fn intersects_detects_any_overlap() {
188        let f = ItemFlags::IS_VISIBLE | ItemFlags::IS_DRAGGABLE;
189        assert!(f.intersects(ItemFlags::IS_DRAGGABLE));
190        assert!(f.intersects(ItemFlags::IS_VISIBLE | ItemFlags::IS_ENABLED));
191        assert!(!f.intersects(ItemFlags::IS_FOCUSABLE));
192    }
193}