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}