1pub(crate) mod normalizer;
6
7pub use normalizer::add_observer;
11
12use crate::traits::LoadError;
13use std::fmt;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct NavigationId(u64);
23
24static NAVIGATION_ID_SEQUENCE: AtomicU64 = AtomicU64::new(1);
25
26impl NavigationId {
27 pub(crate) fn next() -> Self {
29 Self(NAVIGATION_ID_SEQUENCE.fetch_add(1, Ordering::Relaxed))
30 }
31
32 pub fn get(self) -> u64 {
33 self.0
34 }
35
36 #[cfg(feature = "test-support")]
38 pub fn from_raw(raw: u64) -> Self {
39 Self(raw)
40 }
41}
42
43impl fmt::Display for NavigationId {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "nav#{}", self.0)
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum NavigationCancellationReason {
55 Superseded,
57 Stopped,
59 WebViewDestroyed,
61 Other,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum NavigationEvent {
76 Started {
77 id: NavigationId,
78 requested_url: String,
79 },
80 Succeeded {
81 id: NavigationId,
82 final_url: String,
83 },
84 Failed {
85 id: NavigationId,
86 error: LoadError,
87 },
88 Cancelled {
89 id: NavigationId,
90 reason: NavigationCancellationReason,
91 },
92}
93
94impl NavigationEvent {
95 pub fn id(&self) -> NavigationId {
96 match self {
97 NavigationEvent::Started { id, .. }
98 | NavigationEvent::Succeeded { id, .. }
99 | NavigationEvent::Failed { id, .. }
100 | NavigationEvent::Cancelled { id, .. } => *id,
101 }
102 }
103
104 pub fn is_terminal(&self) -> bool {
105 !matches!(self, NavigationEvent::Started { .. })
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum WebViewStateChange {
115 Location {
116 url: String,
117 },
118 Title {
119 title: Option<String>,
121 },
122 Favicon {
123 png_bytes: Option<Vec<u8>>,
125 },
126 BackForwardAvailability {
127 can_go_back: bool,
128 can_go_forward: bool,
129 },
130}
131
132pub enum WebViewObservedEvent<'a> {
136 Navigation(&'a NavigationEvent),
137 State(&'a WebViewStateChange),
138}
139
140pub type WebViewEventObserver = Arc<dyn Fn(WebViewObservedEvent<'_>) + Send + Sync>;
144
145#[derive(Debug, Default)]
149pub struct NavigationProgress {
150 newest: Option<NavigationId>,
151 newest_terminal: bool,
152}
153
154impl NavigationProgress {
155 pub fn apply(&mut self, event: &NavigationEvent) {
157 match event {
158 NavigationEvent::Started { id, .. } => {
159 self.newest = Some(*id);
160 self.newest_terminal = false;
161 }
162 terminal => {
163 if self.newest == Some(terminal.id()) {
164 self.newest_terminal = true;
165 }
166 }
167 }
168 }
169
170 pub fn is_loading(&self) -> bool {
172 self.newest.is_some() && !self.newest_terminal
173 }
174
175 pub fn current(&self) -> Option<NavigationId> {
177 if self.newest_terminal {
178 None
179 } else {
180 self.newest
181 }
182 }
183
184 pub fn is_current(&self, id: NavigationId) -> bool {
186 self.newest == Some(id)
187 }
188}
189
190#[derive(Debug, Clone, Default, PartialEq, Eq)]
194pub struct ObservedWebViewState {
195 pub url: Option<String>,
196 pub title: Option<String>,
197 pub favicon_png: Option<Vec<u8>>,
198 pub can_go_back: bool,
199 pub can_go_forward: bool,
200}
201
202impl ObservedWebViewState {
203 pub fn apply(&mut self, change: WebViewStateChange) {
206 match change {
207 WebViewStateChange::Location { url } => self.url = Some(url),
208 WebViewStateChange::Title { title } => self.title = title,
209 WebViewStateChange::Favicon { png_bytes } => self.favicon_png = png_bytes,
210 WebViewStateChange::BackForwardAvailability {
211 can_go_back,
212 can_go_forward,
213 } => {
214 self.can_go_back = can_go_back;
215 self.can_go_forward = can_go_forward;
216 }
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::traits::{LoadError, LoadErrorKind};
225
226 fn id(raw: u64) -> NavigationId {
227 NavigationId(raw)
228 }
229
230 fn started(raw: u64) -> NavigationEvent {
231 NavigationEvent::Started {
232 id: id(raw),
233 requested_url: format!("https://example.com/{raw}"),
234 }
235 }
236
237 fn succeeded(raw: u64) -> NavigationEvent {
238 NavigationEvent::Succeeded {
239 id: id(raw),
240 final_url: format!("https://example.com/{raw}"),
241 }
242 }
243
244 #[test]
245 fn navigation_id_displays_for_diagnostics() {
246 assert_eq!(id(42).to_string(), "nav#42");
247 }
248
249 #[test]
250 fn progress_tracks_single_attempt() {
251 let mut progress = NavigationProgress::default();
252 assert!(!progress.is_loading());
253 progress.apply(&started(1));
254 assert!(progress.is_loading());
255 assert_eq!(progress.current(), Some(id(1)));
256 progress.apply(&succeeded(1));
257 assert!(!progress.is_loading());
258 assert_eq!(progress.current(), None);
259 assert!(progress.is_current(id(1)));
260 }
261
262 #[test]
263 fn terminal_for_older_attempt_keeps_newest_loading() {
264 let mut progress = NavigationProgress::default();
265 progress.apply(&started(1));
266 progress.apply(&started(2));
267 progress.apply(&NavigationEvent::Cancelled {
268 id: id(1),
269 reason: NavigationCancellationReason::Superseded,
270 });
271 assert!(progress.is_loading());
272 assert_eq!(progress.current(), Some(id(2)));
273 assert!(!progress.is_current(id(1)));
274 }
275
276 #[test]
277 fn failed_terminal_ends_loading_for_current_attempt() {
278 let mut progress = NavigationProgress::default();
279 progress.apply(&started(1));
280 progress.apply(&NavigationEvent::Failed {
281 id: id(1),
282 error: LoadError {
283 failing_url: Some("https://example.com/1".into()),
284 kind: LoadErrorKind::Network,
285 description: "boom".into(),
286 },
287 });
288 assert!(!progress.is_loading());
289 }
290
291 #[test]
292 fn observed_state_applies_none_clears() {
293 let mut state = ObservedWebViewState::default();
294 state.apply(WebViewStateChange::Title {
295 title: Some("Example".into()),
296 });
297 state.apply(WebViewStateChange::Favicon {
298 png_bytes: Some(vec![1, 2, 3]),
299 });
300 state.apply(WebViewStateChange::Location {
301 url: "https://example.com/".into(),
302 });
303 state.apply(WebViewStateChange::BackForwardAvailability {
304 can_go_back: true,
305 can_go_forward: false,
306 });
307 assert_eq!(state.title.as_deref(), Some("Example"));
308 assert_eq!(state.favicon_png.as_deref(), Some(&[1u8, 2, 3][..]));
309 assert!(state.can_go_back);
310
311 state.apply(WebViewStateChange::Title { title: None });
312 state.apply(WebViewStateChange::Favicon { png_bytes: None });
313 assert_eq!(state.title, None);
314 assert_eq!(state.favicon_png, None);
315 assert_eq!(state.url.as_deref(), Some("https://example.com/"));
316 }
317}