1use crate::field::{Field, NodeId, NodeKind, Vec2};
2use crate::viewport::{FocusRing, FocusZone, Viewport};
3
4#[cfg(test)]
5use crate::field::NodeState;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum DecayLevel {
9 Hot, Cold, }
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct DecayPolicy {
15 pub node_after_ms: u64,
17}
18
19impl DecayPolicy {
20 pub fn new(node_after_ms: u64) -> Self {
21 Self { node_after_ms }
22 }
23}
24
25pub fn tick_decay(field: &mut Field, now_ms: u64, policy: DecayPolicy, focused: Option<NodeId>) {
30 let ids: Vec<NodeId> = field.nodes().keys().copied().collect();
31
32 for id in ids {
33 let Some(n) = field.node(id) else { continue };
34
35 if n.kind == NodeKind::Core {
36 continue;
37 }
38
39 if field.cluster_id_for_member_public(id).is_some() {
40 continue;
41 }
42
43 if Some(id) == focused {
44 let _ = field.set_decay_level(id, DecayLevel::Hot);
45 continue;
46 }
47
48 let age = now_ms.saturating_sub(n.last_touch_ms);
49
50 if age >= policy.node_after_ms {
51 let _ = field.set_decay_level(id, DecayLevel::Cold);
52 } else {
53 let _ = field.set_decay_level(id, DecayLevel::Hot);
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct FocusRingDecayPolicy {
60 pub inside_to_node_ms: u64,
64
65 pub outside_immediate_cold: bool,
68}
69
70impl FocusRingDecayPolicy {
71 pub fn new() -> Self {
72 Self {
73 inside_to_node_ms: 1_200_000,
74 outside_immediate_cold: true,
75 }
76 }
77}
78
79impl Default for FocusRingDecayPolicy {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85pub fn tick_decay_focus_ring(
91 field: &mut Field,
92 vp: &Viewport,
93 now_ms: u64,
94 focus_ring: FocusRing,
95 policy: FocusRingDecayPolicy,
96 focused: Option<NodeId>,
97) {
98 let ids: Vec<NodeId> = field.nodes().keys().copied().collect();
99
100 for id in ids {
101 let (kind, pos, active_extent, last_touch_ms) = {
102 let Some(n) = field.node(id) else { continue };
103 (n.kind.clone(), n.pos, n.footprint, n.last_touch_ms)
104 };
105
106 if kind == NodeKind::Core {
107 continue;
108 }
109
110 if field.cluster_id_for_member_public(id).is_some() {
111 continue;
112 }
113
114 if Some(id) == focused {
115 let _ = field.set_decay_level(id, DecayLevel::Hot);
116 continue;
117 }
118
119 let zone = dominant_focus_zone(focus_ring, vp.center, pos, active_extent);
120
121 match zone {
122 FocusZone::Inside => {
123 let age = now_ms.saturating_sub(last_touch_ms);
124 if age >= policy.inside_to_node_ms {
125 let _ = field.set_decay_level(id, DecayLevel::Cold);
126 } else {
127 let _ = field.set_decay_level(id, DecayLevel::Hot);
128 }
129 }
130 FocusZone::Outside => {
131 if policy.outside_immediate_cold {
132 let _ = field.set_decay_level(id, DecayLevel::Cold);
133 } else {
134 let age = now_ms.saturating_sub(last_touch_ms);
135 if age >= policy.inside_to_node_ms {
136 let _ = field.set_decay_level(id, DecayLevel::Cold);
137 } else {
138 let _ = field.set_decay_level(id, DecayLevel::Hot);
139 }
140 }
141 }
142 }
143 }
144}
145
146fn dominant_focus_zone(
147 focus_ring: FocusRing,
148 vp_center: Vec2,
149 pos: Vec2,
150 footprint: Vec2,
151) -> FocusZone {
152 let w = footprint.x.abs();
153 let h = footprint.y.abs();
154
155 if w < 1.0 || h < 1.0 {
156 return focus_ring.zone(vp_center, pos);
157 }
158
159 let sx = 5usize;
160 let sy = 5usize;
161 let mut inside = 0usize;
162
163 let min_x = pos.x - w * 0.5;
164 let min_y = pos.y - h * 0.5;
165
166 for iy in 0..sy {
167 for ix in 0..sx {
168 let tx = (ix as f32 + 0.5) / sx as f32;
169 let ty = (iy as f32 + 0.5) / sy as f32;
170 let p = Vec2 {
171 x: min_x + tx * w,
172 y: min_y + ty * h,
173 };
174
175 if focus_ring.zone(vp_center, p) == FocusZone::Inside {
176 inside += 1;
177 }
178 }
179 }
180
181 let total = (sx * sy) as f32;
182 let frac_inside = inside as f32 / total;
183
184 if frac_inside > 0.5 {
185 FocusZone::Inside
186 } else {
187 FocusZone::Outside
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::field::Vec2;
195
196 fn default_focus_ring() -> FocusRing {
197 FocusRing::new(50.0, 30.0, 0.0, 0.0)
198 }
199
200 #[test]
201 fn decays_hot_to_cold() {
202 let mut f = Field::new();
203 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
204
205 assert!(f.touch(a, 0));
206 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
207 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
208
209 let policy = DecayPolicy::new(5000);
210
211 tick_decay(&mut f, 1500, policy, None);
212 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
213 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
214
215 tick_decay(&mut f, 6000, policy, None);
216 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
217 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
218 }
219
220 #[test]
221 fn focused_node_stays_hot() {
222 let mut f = Field::new();
223 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
224 assert!(f.touch(a, 0));
225
226 let policy = DecayPolicy::new(5000);
227
228 tick_decay(&mut f, 6000, policy, Some(a));
229 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
230 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
231 }
232
233 #[test]
234 fn core_does_not_decay() {
235 let mut f = Field::new();
236 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
237 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
238
239 let cid = f.create_cluster(vec![a, b]).unwrap();
240 let core = f.collapse_cluster(cid).unwrap();
241
242 let policy = DecayPolicy::new(5000);
243 tick_decay(&mut f, 999_999, policy, None);
244
245 let n = f.node(core).unwrap();
246 assert_eq!(n.kind, NodeKind::Core);
247 assert_eq!(n.state, NodeState::Core);
248 }
249
250 #[test]
251 fn clustered_members_do_not_decay() {
252 let mut f = Field::new();
253 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
254 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
255
256 let cid = f.create_cluster(vec![a, b]).unwrap();
257 let _ = f.collapse_cluster(cid);
258
259 let policy = DecayPolicy::new(1);
260 tick_decay(&mut f, 999_999, policy, None);
261
262 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
263 assert_eq!(f.node(b).unwrap().state, NodeState::Node);
264 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
265 assert_eq!(f.node(b).unwrap().decay, DecayLevel::Hot);
266 }
267
268 #[test]
269 fn inside_focus_ring_near_center_stays_hot() {
270 let mut f = Field::new();
271 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
272 assert!(f.touch(a, 0));
273
274 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
275 let ring = default_focus_ring();
276 let policy = FocusRingDecayPolicy::new();
277
278 tick_decay_focus_ring(&mut f, &vp, 999_999, ring, policy, None);
279
280 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
281 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
282 }
283
284 #[test]
285 fn inside_focus_ring_stays_hot_before_threshold() {
286 let mut f = Field::new();
287 let a = f.spawn_surface("A", Vec2 { x: 49.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
288 assert!(f.touch(a, 0));
289
290 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
291 let ring = default_focus_ring();
292 let mut policy = FocusRingDecayPolicy::new();
293 policy.inside_to_node_ms = 5000;
294
295 tick_decay_focus_ring(&mut f, &vp, 1500, ring, policy, None);
296
297 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
298 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
299 }
300
301 #[test]
302 fn inside_focus_ring_can_decay_to_cold() {
303 let mut f = Field::new();
304 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
305 assert!(f.touch(a, 0));
306
307 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
308 let ring = default_focus_ring();
309 let mut policy = FocusRingDecayPolicy::new();
310 policy.inside_to_node_ms = 5000;
311
312 tick_decay_focus_ring(&mut f, &vp, 7000, ring, policy, None);
313
314 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
315 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
316 }
317
318 #[test]
319 fn outside_focus_ring_goes_cold_immediately() {
320 let mut f = Field::new();
321 let a = f.spawn_surface("A", Vec2 { x: 500.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
322 assert!(f.touch(a, 0));
323
324 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
325 let ring = default_focus_ring();
326 let policy = FocusRingDecayPolicy::new();
327
328 tick_decay_focus_ring(&mut f, &vp, 1000, ring, policy, None);
329
330 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
331 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
332 }
333
334 #[test]
335 fn focused_node_stays_hot_with_focus_ring_policy() {
336 let mut f = Field::new();
337 let a = f.spawn_surface("A", Vec2 { x: 500.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
338 assert!(f.touch(a, 0));
339
340 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
341 let ring = default_focus_ring();
342 let policy = FocusRingDecayPolicy::new();
343
344 tick_decay_focus_ring(&mut f, &vp, 999_999, ring, policy, Some(a));
345
346 assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
347 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
348 }
349}