fret_ui_headless/
hover_intent.rs1#[derive(Debug, Clone, Copy)]
7pub struct HoverIntentConfig {
8 pub open_delay_ticks: u64,
9 pub close_delay_ticks: u64,
10}
11
12impl HoverIntentConfig {
13 pub fn new(open_delay_ticks: u64, close_delay_ticks: u64) -> Self {
14 Self {
15 open_delay_ticks,
16 close_delay_ticks,
17 }
18 }
19}
20
21#[derive(Debug, Default, Clone, Copy)]
22pub struct HoverIntentState {
23 open: bool,
24 hover_start: Option<u64>,
25 leave_start: Option<u64>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct HoverIntentUpdate {
30 pub open: bool,
31 pub wants_continuous_ticks: bool,
32}
33
34impl HoverIntentState {
35 pub fn is_open(&self) -> bool {
36 self.open
37 }
38
39 pub fn set_open(&mut self, open: bool) {
40 if self.open == open {
41 return;
42 }
43 self.open = open;
44 self.hover_start = None;
45 self.leave_start = None;
46 }
47
48 pub fn update(&mut self, hovered: bool, now: u64, cfg: HoverIntentConfig) -> HoverIntentUpdate {
49 if hovered {
50 self.leave_start = None;
51 if !self.open {
52 if cfg.open_delay_ticks == 0 {
53 self.open = true;
54 self.hover_start = None;
55 } else {
56 let start = self.hover_start.get_or_insert(now);
57 let elapsed = now.saturating_sub(*start);
58 if elapsed >= cfg.open_delay_ticks {
59 self.open = true;
60 self.hover_start = None;
61 }
62 }
63 }
64 } else {
65 self.hover_start = None;
66 if self.open {
67 if cfg.close_delay_ticks == 0 {
68 self.open = false;
69 self.leave_start = None;
70 } else {
71 let start = self.leave_start.get_or_insert(now);
72 let elapsed = now.saturating_sub(*start);
73 if elapsed >= cfg.close_delay_ticks {
74 self.open = false;
75 self.leave_start = None;
76 }
77 }
78 } else {
79 self.leave_start = None;
80 }
81 }
82
83 let wants_continuous_ticks = (hovered && !self.open && cfg.open_delay_ticks > 0)
84 || (!hovered && self.open && cfg.close_delay_ticks > 0);
85
86 HoverIntentUpdate {
87 open: self.open,
88 wants_continuous_ticks,
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn opens_after_delay_and_closes_after_delay() {
99 let cfg = HoverIntentConfig::new(2, 3);
100 let mut st = HoverIntentState::default();
101
102 assert_eq!(
104 st.update(true, 0, cfg),
105 HoverIntentUpdate {
106 open: false,
107 wants_continuous_ticks: true
108 }
109 );
110 assert_eq!(
111 st.update(true, 1, cfg),
112 HoverIntentUpdate {
113 open: false,
114 wants_continuous_ticks: true
115 }
116 );
117
118 assert_eq!(
120 st.update(true, 2, cfg),
121 HoverIntentUpdate {
122 open: true,
123 wants_continuous_ticks: false
124 }
125 );
126
127 assert_eq!(
129 st.update(false, 3, cfg),
130 HoverIntentUpdate {
131 open: true,
132 wants_continuous_ticks: true
133 }
134 );
135 assert_eq!(
136 st.update(false, 5, cfg),
137 HoverIntentUpdate {
138 open: true,
139 wants_continuous_ticks: true
140 }
141 );
142 assert_eq!(
143 st.update(false, 6, cfg),
144 HoverIntentUpdate {
145 open: false,
146 wants_continuous_ticks: false
147 }
148 );
149 }
150
151 #[test]
152 fn zero_delays_toggle_immediately() {
153 let cfg = HoverIntentConfig::new(0, 0);
154 let mut st = HoverIntentState::default();
155
156 assert_eq!(
157 st.update(true, 10, cfg),
158 HoverIntentUpdate {
159 open: true,
160 wants_continuous_ticks: false
161 }
162 );
163 assert_eq!(
164 st.update(false, 11, cfg),
165 HoverIntentUpdate {
166 open: false,
167 wants_continuous_ticks: false
168 }
169 );
170 }
171
172 #[test]
173 fn set_open_resets_pending_delays() {
174 let cfg = HoverIntentConfig::new(5, 5);
175 let mut st = HoverIntentState::default();
176
177 let out0 = st.update(true, 0, cfg);
179 assert!(!out0.open);
180 assert!(out0.wants_continuous_ticks);
181
182 st.set_open(true);
184 let out1 = st.update(true, 1, cfg);
185 assert!(out1.open);
186 assert!(!out1.wants_continuous_ticks);
187
188 let out2 = st.update(false, 2, cfg);
190 assert!(out2.open);
191 assert!(out2.wants_continuous_ticks);
192
193 st.set_open(false);
194 let out3 = st.update(false, 3, cfg);
195 assert!(!out3.open);
196 assert!(!out3.wants_continuous_ticks);
197 }
198}