oxide_browser/
navigation.rs1use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Clone, Debug)]
12pub struct HistoryEntry {
13 pub url: String,
15 pub title: String,
17 pub state: Vec<u8>,
20 #[allow(dead_code)]
22 pub timestamp_ms: u64,
23}
24
25impl HistoryEntry {
26 pub fn new(url: impl Into<String>) -> Self {
27 Self {
28 url: url.into(),
29 title: String::new(),
30 state: Vec::new(),
31 timestamp_ms: SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .unwrap_or_default()
34 .as_millis() as u64,
35 }
36 }
37
38 pub fn with_title(mut self, title: impl Into<String>) -> Self {
39 self.title = title.into();
40 self
41 }
42
43 pub fn with_state(mut self, state: Vec<u8>) -> Self {
44 self.state = state;
45 self
46 }
47}
48
49#[derive(Clone, Debug)]
51pub struct NavigationStack {
52 entries: Vec<HistoryEntry>,
53 index: isize,
55}
56
57impl NavigationStack {
58 pub fn new() -> Self {
59 Self {
60 entries: Vec::new(),
61 index: -1,
62 }
63 }
64
65 pub fn push(&mut self, entry: HistoryEntry) {
67 let new_index = self.index + 1;
68 self.entries.truncate(new_index as usize);
69 self.entries.push(entry);
70 self.index = new_index;
71 }
72
73 pub fn replace_current(&mut self, entry: HistoryEntry) {
75 if self.index >= 0 && (self.index as usize) < self.entries.len() {
76 self.entries[self.index as usize] = entry;
77 } else {
78 self.push(entry);
79 }
80 }
81
82 #[allow(dead_code)]
85 pub fn update_current(
86 &mut self,
87 title: Option<&str>,
88 state: Option<Vec<u8>>,
89 url: Option<&str>,
90 ) {
91 if let Some(entry) = self.current_mut() {
92 if let Some(t) = title {
93 entry.title = t.to_string();
94 }
95 if let Some(s) = state {
96 entry.state = s;
97 }
98 if let Some(u) = url {
99 entry.url = u.to_string();
100 }
101 }
102 }
103
104 pub fn go_back(&mut self) -> Option<&HistoryEntry> {
106 if self.index > 0 {
107 self.index -= 1;
108 Some(&self.entries[self.index as usize])
109 } else {
110 None
111 }
112 }
113
114 pub fn go_forward(&mut self) -> Option<&HistoryEntry> {
116 if self.index + 1 < self.entries.len() as isize {
117 self.index += 1;
118 Some(&self.entries[self.index as usize])
119 } else {
120 None
121 }
122 }
123
124 pub fn can_go_back(&self) -> bool {
125 self.index > 0
126 }
127
128 pub fn can_go_forward(&self) -> bool {
129 self.index + 1 < self.entries.len() as isize
130 }
131
132 pub fn current(&self) -> Option<&HistoryEntry> {
133 if self.index >= 0 && (self.index as usize) < self.entries.len() {
134 Some(&self.entries[self.index as usize])
135 } else {
136 None
137 }
138 }
139
140 #[allow(dead_code)]
141 pub fn current_mut(&mut self) -> Option<&mut HistoryEntry> {
142 if self.index >= 0 && (self.index as usize) < self.entries.len() {
143 Some(&mut self.entries[self.index as usize])
144 } else {
145 None
146 }
147 }
148
149 pub fn len(&self) -> usize {
150 self.entries.len()
151 }
152
153 #[allow(dead_code)]
154 pub fn is_empty(&self) -> bool {
155 self.entries.is_empty()
156 }
157
158 #[allow(dead_code)]
159 pub fn current_index(&self) -> isize {
160 self.index
161 }
162
163 #[allow(dead_code)]
164 pub fn entries(&self) -> &[HistoryEntry] {
165 &self.entries
166 }
167}
168
169impl Default for NavigationStack {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn push_and_navigate() {
181 let mut stack = NavigationStack::new();
182 assert!(!stack.can_go_back());
183 assert!(!stack.can_go_forward());
184
185 stack.push(HistoryEntry::new("https://a.com"));
186 stack.push(HistoryEntry::new("https://b.com"));
187 stack.push(HistoryEntry::new("https://c.com"));
188
189 assert_eq!(stack.current().unwrap().url, "https://c.com");
190 assert!(stack.can_go_back());
191 assert!(!stack.can_go_forward());
192
193 let entry = stack.go_back().unwrap();
194 assert_eq!(entry.url, "https://b.com");
195 let entry = stack.go_back().unwrap();
196 assert_eq!(entry.url, "https://a.com");
197 assert!(!stack.can_go_back());
198 assert!(stack.can_go_forward());
199
200 let entry = stack.go_forward().unwrap();
201 assert_eq!(entry.url, "https://b.com");
202
203 stack.push(HistoryEntry::new("https://d.com"));
204 assert!(!stack.can_go_forward());
205 assert_eq!(stack.len(), 3); }
207
208 #[test]
209 fn replace_current() {
210 let mut stack = NavigationStack::new();
211 stack.push(HistoryEntry::new("https://a.com"));
212 stack.replace_current(HistoryEntry::new("https://b.com"));
213 assert_eq!(stack.current().unwrap().url, "https://b.com");
214 assert_eq!(stack.len(), 1);
215 }
216
217 #[test]
218 fn update_current_fields() {
219 let mut stack = NavigationStack::new();
220 stack.push(HistoryEntry::new("https://a.com"));
221 stack.update_current(Some("Page A"), Some(vec![1, 2, 3]), None);
222 let cur = stack.current().unwrap();
223 assert_eq!(cur.title, "Page A");
224 assert_eq!(cur.state, vec![1, 2, 3]);
225 assert_eq!(cur.url, "https://a.com");
226 }
227
228 #[test]
229 fn state_preserved_through_back_forward() {
230 let mut stack = NavigationStack::new();
231 stack.push(HistoryEntry::new("https://a.com").with_state(vec![10, 20]));
232 stack.push(HistoryEntry::new("https://b.com"));
233
234 let entry = stack.go_back().unwrap();
235 assert_eq!(entry.state, vec![10, 20]);
236 }
237}