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}