1use rustc_hash::FxHashMap as HashMap;
7
8#[derive(Debug, Clone, PartialEq, Default)]
34pub enum Position {
35 Absolute(f32),
39
40 Relative(f32),
46
47 Label(String),
51
52 #[default]
56 AfterPrevious,
57
58 WithPrevious,
62
63 Start,
65
66 End,
68}
69
70impl Position {
71 pub fn resolve(
84 &self,
85 previous_end: f32,
86 previous_start: f32,
87 timeline_end: f32,
88 labels: &HashMap<String, f32>,
89 ) -> Option<f32> {
90 let time = match self {
91 Self::Absolute(t) => *t,
92 Self::Relative(offset) => previous_end + offset,
93 Self::Label(name) => *labels.get(name)?,
94 Self::AfterPrevious => previous_end,
95 Self::WithPrevious => previous_start,
96 Self::Start => 0.0,
97 Self::End => timeline_end,
98 };
99
100 Some(time.max(0.0))
101 }
102
103 pub fn is_absolute(&self) -> bool {
105 matches!(self, Self::Absolute(_) | Self::Label(_) | Self::Start)
106 }
107
108 pub fn is_relative(&self) -> bool {
110 matches!(
111 self,
112 Self::Relative(_) | Self::AfterPrevious | Self::WithPrevious
113 )
114 }
115}
116
117impl From<f32> for Position {
118 fn from(time: f32) -> Self {
119 Self::Absolute(time)
120 }
121}
122
123impl From<&str> for Position {
124 fn from(s: &str) -> Self {
125 if s == "<" {
127 return Self::WithPrevious;
128 }
129
130 if s == ">" {
131 return Self::AfterPrevious;
132 }
133
134 if let Some(offset) = s.strip_prefix("+=")
135 && let Ok(n) = offset.parse::<f32>()
136 {
137 return Self::Relative(n);
138 }
139
140 if let Some(offset) = s.strip_prefix("-=")
141 && let Ok(n) = offset.parse::<f32>()
142 {
143 return Self::Relative(-n);
144 }
145
146 if let Ok(n) = s.parse::<f32>() {
148 return Self::Absolute(n);
149 }
150
151 Self::Label(s.to_string())
153 }
154}
155
156impl From<String> for Position {
157 fn from(s: String) -> Self {
158 Self::from(s.as_str())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 use rustc_hash::FxHashMap as HashMap;
167
168 #[test]
169 fn test_absolute_position() {
170 let pos = Position::Absolute(2.0);
171 let labels = HashMap::default();
172
173 assert_eq!(pos.resolve(1.0, 0.0, 3.0, &labels), Some(2.0));
174 }
175
176 #[test]
177 fn test_relative_position() {
178 let labels = HashMap::default();
179
180 let pos = Position::Relative(0.5);
181 assert_eq!(pos.resolve(1.0, 0.0, 3.0, &labels), Some(1.5));
182
183 let pos = Position::Relative(-0.3);
184 assert_eq!(pos.resolve(1.0, 0.0, 3.0, &labels), Some(0.7));
185 }
186
187 #[test]
188 fn test_after_previous() {
189 let pos = Position::AfterPrevious;
190 let labels = HashMap::default();
191
192 assert_eq!(pos.resolve(1.5, 0.5, 3.0, &labels), Some(1.5));
193 }
194
195 #[test]
196 fn test_with_previous() {
197 let pos = Position::WithPrevious;
198 let labels = HashMap::default();
199
200 assert_eq!(pos.resolve(1.5, 0.5, 3.0, &labels), Some(0.5));
201 }
202
203 #[test]
204 fn test_label_position() {
205 let mut labels = HashMap::default();
206
207 labels.insert("intro".to_string(), 1.0);
208
209 let pos = Position::Label("intro".to_string());
210
211 assert_eq!(pos.resolve(0.0, 0.0, 3.0, &labels), Some(1.0));
212
213 let pos = Position::Label("missing".to_string());
214
215 assert_eq!(pos.resolve(0.0, 0.0, 3.0, &labels), None);
216 }
217
218 #[test]
219 fn test_from_string() {
220 assert_eq!(Position::from("<"), Position::WithPrevious);
221 assert_eq!(Position::from(">"), Position::AfterPrevious);
222 assert_eq!(Position::from("+=0.5"), Position::Relative(0.5));
223 assert_eq!(Position::from("-=0.3"), Position::Relative(-0.3));
224 assert_eq!(Position::from("2.0"), Position::Absolute(2.0));
225 assert_eq!(
226 Position::from("myLabel"),
227 Position::Label("myLabel".to_string())
228 );
229 }
230
231 #[test]
232 fn test_clamp_negative() {
233 let pos = Position::Relative(-10.0);
234 let labels = HashMap::default();
235
236 assert_eq!(pos.resolve(1.0, 0.0, 3.0, &labels), Some(0.0));
238 }
239}