1#![allow(dead_code)]
9
10use alloc::{
11 collections::BTreeMap,
12 string::{String, ToString},
13 vec::Vec,
14};
15
16pub type TabId = u64;
22
23#[derive(Debug, Clone)]
29pub struct NavigationHistory {
30 back: Vec<String>,
32 current: String,
34 forward: Vec<String>,
36 max_entries: usize,
38}
39
40impl Default for NavigationHistory {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl NavigationHistory {
47 pub fn new() -> Self {
48 Self {
49 back: Vec::new(),
50 current: String::new(),
51 forward: Vec::new(),
52 max_entries: 50,
53 }
54 }
55
56 pub(crate) fn navigate(&mut self, url: &str) {
58 if !self.current.is_empty() {
59 self.back.push(self.current.clone());
60 if self.back.len() > self.max_entries {
61 self.back.remove(0);
62 }
63 }
64 self.current = url.to_string();
65 self.forward.clear();
66 }
67
68 pub(crate) fn go_back(&mut self) -> Option<String> {
70 let prev = self.back.pop()?;
71 self.forward.push(self.current.clone());
72 self.current = prev.clone();
73 Some(prev)
74 }
75
76 pub(crate) fn go_forward(&mut self) -> Option<String> {
78 let next = self.forward.pop()?;
79 self.back.push(self.current.clone());
80 self.current = next.clone();
81 Some(next)
82 }
83
84 pub(crate) fn can_go_back(&self) -> bool {
86 !self.back.is_empty()
87 }
88
89 pub(crate) fn can_go_forward(&self) -> bool {
91 !self.forward.is_empty()
92 }
93
94 pub(crate) fn current_url(&self) -> &str {
96 &self.current
97 }
98
99 pub(crate) fn back_count(&self) -> usize {
101 self.back.len()
102 }
103
104 pub(crate) fn forward_count(&self) -> usize {
106 self.forward.len()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub enum TabLoadState {
117 #[default]
119 Idle,
120 Resolving,
122 Connecting,
124 Loading,
126 Rendering,
128 Error,
130}
131
132#[derive(Clone)]
138pub struct Tab {
139 pub id: TabId,
141 pub title: String,
143 pub url: String,
145 pub active: bool,
147 pub load_state: TabLoadState,
149 pub favicon: Option<Vec<u8>>,
151 pub history: NavigationHistory,
153 pub dirty: bool,
155 pub pinned: bool,
157 pub creation_order: u64,
159 pub last_active_tick: u64,
161 pub error_message: Option<String>,
163}
164
165impl Tab {
166 pub fn new(id: TabId, url: &str, creation_order: u64) -> Self {
167 let mut history = NavigationHistory::new();
168 if !url.is_empty() {
169 history.navigate(url);
170 }
171 Self {
172 id,
173 title: title_from_url(url),
174 url: url.to_string(),
175 active: false,
176 load_state: TabLoadState::Idle,
177 favicon: None,
178 history,
179 dirty: false,
180 pinned: false,
181 creation_order,
182 last_active_tick: 0,
183 error_message: None,
184 }
185 }
186
187 pub(crate) fn navigate(&mut self, url: &str) {
189 self.history.navigate(url);
190 self.url = url.to_string();
191 self.title = title_from_url(url);
192 self.load_state = TabLoadState::Loading;
193 self.error_message = None;
194 }
195
196 pub(crate) fn finish_loading(&mut self) {
198 self.load_state = TabLoadState::Idle;
199 }
200
201 pub(crate) fn fail_loading(&mut self, error: &str) {
203 self.load_state = TabLoadState::Error;
204 self.error_message = Some(error.to_string());
205 }
206
207 pub(crate) fn set_title(&mut self, title: &str) {
209 self.title = if title.is_empty() {
210 title_from_url(&self.url)
211 } else {
212 truncate_title(title, 64)
213 };
214 }
215
216 pub(crate) fn display_title(&self, max_len: usize) -> String {
218 truncate_title(&self.title, max_len)
219 }
220
221 pub(crate) fn can_go_back(&self) -> bool {
223 self.history.can_go_back()
224 }
225
226 pub(crate) fn can_go_forward(&self) -> bool {
228 self.history.can_go_forward()
229 }
230
231 pub(crate) fn go_back(&mut self) -> Option<String> {
233 let url = self.history.go_back()?;
234 self.url = url.clone();
235 self.title = title_from_url(&url);
236 self.load_state = TabLoadState::Loading;
237 Some(url)
238 }
239
240 pub(crate) fn go_forward(&mut self) -> Option<String> {
242 let url = self.history.go_forward()?;
243 self.url = url.clone();
244 self.title = title_from_url(&url);
245 self.load_state = TabLoadState::Loading;
246 Some(url)
247 }
248
249 pub(crate) fn reload(&mut self) {
251 self.load_state = TabLoadState::Loading;
252 self.error_message = None;
253 }
254}
255
256pub struct TabBar {
262 pub tab_width: i32,
264 pub min_tab_width: i32,
266 pub max_tab_width: i32,
268 pub height: i32,
270 pub scroll_offset: i32,
272 pub visible_width: i32,
274 pub close_button_size: i32,
276 pub hovered_tab: Option<TabId>,
278 pub hovered_close: Option<TabId>,
280 pub dragging_tab: Option<TabId>,
282 pub drag_offset_x: i32,
284}
285
286impl Default for TabBar {
287 fn default() -> Self {
288 Self::new(800)
289 }
290}
291
292impl TabBar {
293 pub fn new(visible_width: i32) -> Self {
294 Self {
295 tab_width: 200,
296 min_tab_width: 80,
297 max_tab_width: 250,
298 height: 32,
299 scroll_offset: 0,
300 visible_width,
301 close_button_size: 16,
302 hovered_tab: None,
303 hovered_close: None,
304 dragging_tab: None,
305 drag_offset_x: 0,
306 }
307 }
308
309 pub(crate) fn compute_tab_width(&self, num_tabs: usize) -> i32 {
311 if num_tabs == 0 {
312 return self.max_tab_width;
313 }
314 let available = self.visible_width - 32;
316 let per_tab = available / (num_tabs as i32);
317 per_tab.clamp(self.min_tab_width, self.max_tab_width)
318 }
319
320 pub(crate) fn tab_at_x(&self, x: i32, tab_ids: &[TabId], num_tabs: usize) -> Option<TabId> {
322 let tw = self.compute_tab_width(num_tabs);
323 let local_x = x + self.scroll_offset;
324 if local_x < 0 {
325 return None;
326 }
327 let idx = local_x / tw;
328 if (idx as usize) < tab_ids.len() {
329 Some(tab_ids[idx as usize])
330 } else {
331 None
332 }
333 }
334
335 pub(crate) fn is_close_button_hit(
337 &self,
338 x: i32,
339 tab_ids: &[TabId],
340 num_tabs: usize,
341 ) -> Option<TabId> {
342 let tw = self.compute_tab_width(num_tabs);
343 let local_x = x + self.scroll_offset;
344 if local_x < 0 {
345 return None;
346 }
347 let idx = local_x / tw;
348 if (idx as usize) >= tab_ids.len() {
349 return None;
350 }
351 let tab_start = idx * tw;
353 let close_start = tab_start + tw - self.close_button_size - 4;
354 if local_x >= close_start && local_x <= close_start + self.close_button_size {
355 Some(tab_ids[idx as usize])
356 } else {
357 None
358 }
359 }
360
361 pub(crate) fn is_new_tab_button_hit(&self, x: i32, num_tabs: usize) -> bool {
363 let tw = self.compute_tab_width(num_tabs);
364 let tabs_end = (num_tabs as i32) * tw - self.scroll_offset;
365 x >= tabs_end && x <= tabs_end + 32
366 }
367
368 pub(crate) fn render_tab_bar(&self, tabs: &[&Tab], buf: &mut [u32], buf_width: usize) {
371 let height = self.height as usize;
372 let tw = self.compute_tab_width(tabs.len()) as usize;
373
374 let bg_color: u32 = 0xFF2D2D30; for y in 0..height {
377 for x in 0..buf_width {
378 if y * buf_width + x < buf.len() {
379 buf[y * buf_width + x] = bg_color;
380 }
381 }
382 }
383
384 for (i, tab) in tabs.iter().enumerate() {
386 let tab_x = (i * tw).saturating_sub(self.scroll_offset as usize);
387 if tab_x >= buf_width {
388 break;
389 }
390
391 let tab_bg = if tab.active {
392 0xFF3C3C3C_u32 } else if self.hovered_tab == Some(tab.id) {
394 0xFF353535_u32 } else {
396 0xFF2D2D30_u32 };
398
399 for y in 2..height {
401 let end_x = (tab_x + tw).min(buf_width);
402 for x in (tab_x + 1)..end_x.saturating_sub(1) {
403 if y * buf_width + x < buf.len() {
404 buf[y * buf_width + x] = tab_bg;
405 }
406 }
407 }
408
409 if tab.active {
411 let indicator_color: u32 = 0xFF007ACC;
412 for x in (tab_x + 1)..(tab_x + tw).min(buf_width).saturating_sub(1) {
413 if x < buf_width {
414 buf[x] = indicator_color;
415 if buf_width + x < buf.len() {
416 buf[buf_width + x] = indicator_color;
417 }
418 }
419 }
420 }
421
422 let title = tab.display_title((tw.saturating_sub(30)) / 8);
424 let text_y = height / 2;
425 let text_x = tab_x + 8;
426 let text_color: u32 = if tab.active { 0xFFFFFFFF } else { 0xFFA0A0A0 };
427 render_text_simple(buf, buf_width, text_x, text_y, &title, text_color);
428
429 if !tab.pinned {
431 let cx = tab_x + tw - (self.close_button_size as usize) - 4;
432 let cy = (height - self.close_button_size as usize) / 2;
433 let close_color: u32 = if self.hovered_close == Some(tab.id) {
434 0xFFFF0000
435 } else {
436 0xFF808080
437 };
438 render_close_button(
439 buf,
440 buf_width,
441 cx,
442 cy,
443 self.close_button_size as usize,
444 close_color,
445 );
446 }
447
448 if tab.load_state == TabLoadState::Loading {
450 let lx = tab_x + tw - 24;
451 let ly = height / 2 - 2;
452 for dx in 0..4 {
453 let px = lx + dx;
454 let py = ly;
455 if py * buf_width + px < buf.len() {
456 buf[py * buf_width + px] = 0xFF00AAFF_u32;
457 }
458 }
459 }
460 }
461
462 let plus_x = tabs.len() * tw;
464 if plus_x < buf_width.saturating_sub(32) {
465 render_text_simple(buf, buf_width, plus_x + 10, height / 2, "+", 0xFFA0A0A0);
466 }
467 }
468
469 pub(crate) fn ensure_visible(&mut self, tab_index: usize, num_tabs: usize) {
471 let tw = self.compute_tab_width(num_tabs);
472 let tab_start = (tab_index as i32) * tw;
473 let tab_end = tab_start + tw;
474
475 if tab_start < self.scroll_offset {
476 self.scroll_offset = tab_start;
477 } else if tab_end > self.scroll_offset + self.visible_width - 32 {
478 self.scroll_offset = tab_end - self.visible_width + 32;
479 }
480 }
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum TabAction {
490 NewTab,
492 CloseTab,
494 NextTab,
496 PrevTab,
498 SwitchToIndex(usize),
500 ReopenClosed,
502 None,
504}
505
506pub struct TabManager {
508 tabs: BTreeMap<TabId, Tab>,
510 tab_order: Vec<TabId>,
512 active_tab: Option<TabId>,
514 next_id: TabId,
516 max_tabs: usize,
518 recently_closed: Vec<String>,
520 max_recently_closed: usize,
522 pub tab_bar: TabBar,
524 current_tick: u64,
526}
527
528impl Default for TabManager {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534impl TabManager {
535 const DEFAULT_MAX_TABS: usize = 32;
537
538 pub fn new() -> Self {
539 Self {
540 tabs: BTreeMap::new(),
541 tab_order: Vec::new(),
542 active_tab: None,
543 next_id: 1,
544 max_tabs: Self::DEFAULT_MAX_TABS,
545 recently_closed: Vec::new(),
546 max_recently_closed: 10,
547 tab_bar: TabBar::new(800),
548 current_tick: 0,
549 }
550 }
551
552 pub(crate) fn new_tab(&mut self, url: &str) -> Option<TabId> {
555 if self.tabs.len() >= self.max_tabs {
556 return None;
557 }
558
559 let id = self.next_id;
560 self.next_id += 1;
561
562 let tab = Tab::new(id, url, id);
563 self.tabs.insert(id, tab);
564 self.tab_order.push(id);
565
566 if self.active_tab.is_none() {
568 self.switch_tab(id);
569 }
570
571 Some(id)
572 }
573
574 pub(crate) fn close_tab(&mut self, id: TabId) -> bool {
577 if self.tabs.len() <= 1 {
579 if let Some(tab) = self.tabs.get_mut(&id) {
580 if !tab.url.is_empty() {
582 self.recently_closed.push(tab.url.clone());
583 if self.recently_closed.len() > self.max_recently_closed {
584 self.recently_closed.remove(0);
585 }
586 }
587 tab.url.clear();
588 tab.title = "New Tab".to_string();
589 tab.history = NavigationHistory::new();
590 tab.load_state = TabLoadState::Idle;
591 return true;
592 }
593 return false;
594 }
595
596 let order_idx = match self.tab_order.iter().position(|&t| t == id) {
598 Some(idx) => idx,
599 None => return false,
600 };
601
602 if let Some(tab) = self.tabs.get(&id) {
604 if !tab.url.is_empty() {
605 self.recently_closed.push(tab.url.clone());
606 if self.recently_closed.len() > self.max_recently_closed {
607 self.recently_closed.remove(0);
608 }
609 }
610 }
611
612 self.tabs.remove(&id);
614 self.tab_order.remove(order_idx);
615
616 if self.active_tab == Some(id) {
618 let new_idx = if order_idx >= self.tab_order.len() {
619 self.tab_order.len().saturating_sub(1)
620 } else {
621 order_idx
622 };
623 if let Some(&new_id) = self.tab_order.get(new_idx) {
624 self.switch_tab(new_id);
625 } else {
626 self.active_tab = None;
627 }
628 }
629
630 true
631 }
632
633 pub(crate) fn switch_tab(&mut self, id: TabId) -> bool {
635 if !self.tabs.contains_key(&id) {
636 return false;
637 }
638
639 if let Some(old_id) = self.active_tab {
641 if let Some(old_tab) = self.tabs.get_mut(&old_id) {
642 old_tab.active = false;
643 }
644 }
645
646 if let Some(tab) = self.tabs.get_mut(&id) {
648 tab.active = true;
649 tab.last_active_tick = self.current_tick;
650 }
651 self.active_tab = Some(id);
652
653 if let Some(idx) = self.tab_order.iter().position(|&t| t == id) {
655 self.tab_bar.ensure_visible(idx, self.tab_order.len());
656 }
657
658 true
659 }
660
661 pub(crate) fn next_tab(&mut self) -> bool {
663 if self.tab_order.len() <= 1 {
664 return false;
665 }
666 let current_idx = self
667 .active_tab
668 .and_then(|id| self.tab_order.iter().position(|&t| t == id))
669 .unwrap_or(0);
670 let next_idx = (current_idx + 1) % self.tab_order.len();
671 let next_id = self.tab_order[next_idx];
672 self.switch_tab(next_id)
673 }
674
675 pub(crate) fn prev_tab(&mut self) -> bool {
677 if self.tab_order.len() <= 1 {
678 return false;
679 }
680 let current_idx = self
681 .active_tab
682 .and_then(|id| self.tab_order.iter().position(|&t| t == id))
683 .unwrap_or(0);
684 let prev_idx = if current_idx == 0 {
685 self.tab_order.len() - 1
686 } else {
687 current_idx - 1
688 };
689 let prev_id = self.tab_order[prev_idx];
690 self.switch_tab(prev_id)
691 }
692
693 pub(crate) fn switch_to_index(&mut self, index: usize) -> bool {
695 if let Some(&id) = self.tab_order.get(index) {
696 self.switch_tab(id)
697 } else {
698 false
699 }
700 }
701
702 pub(crate) fn move_tab(&mut self, id: TabId, new_index: usize) -> bool {
704 let current_idx = match self.tab_order.iter().position(|&t| t == id) {
705 Some(idx) => idx,
706 None => return false,
707 };
708 let clamped = new_index.min(self.tab_order.len().saturating_sub(1));
709 self.tab_order.remove(current_idx);
710 self.tab_order.insert(clamped, id);
711 true
712 }
713
714 pub(crate) fn reopen_closed_tab(&mut self) -> Option<TabId> {
716 let url = self.recently_closed.pop()?;
717 self.new_tab(&url)
718 }
719
720 pub(crate) fn active_tab(&self) -> Option<&Tab> {
722 self.active_tab.and_then(|id| self.tabs.get(&id))
723 }
724
725 pub(crate) fn active_tab_mut(&mut self) -> Option<&mut Tab> {
727 let id = self.active_tab?;
728 self.tabs.get_mut(&id)
729 }
730
731 pub(crate) fn get_tab(&self, id: TabId) -> Option<&Tab> {
733 self.tabs.get(&id)
734 }
735
736 pub(crate) fn get_tab_mut(&mut self, id: TabId) -> Option<&mut Tab> {
738 self.tabs.get_mut(&id)
739 }
740
741 pub(crate) fn tabs_in_order(&self) -> Vec<&Tab> {
743 self.tab_order
744 .iter()
745 .filter_map(|id| self.tabs.get(id))
746 .collect()
747 }
748
749 pub(crate) fn tab_count(&self) -> usize {
751 self.tabs.len()
752 }
753
754 pub(crate) fn active_tab_id(&self) -> Option<TabId> {
756 self.active_tab
757 }
758
759 pub(crate) fn tab_order(&self) -> &[TabId] {
761 &self.tab_order
762 }
763
764 pub(crate) fn decode_shortcut(ctrl: bool, shift: bool, key: u8) -> TabAction {
766 if !ctrl {
767 return TabAction::None;
768 }
769 match key {
770 b't' | b'T' if !shift => TabAction::NewTab,
771 b'w' | b'W' if !shift => TabAction::CloseTab,
772 b'T' if shift => TabAction::ReopenClosed,
773 0x09 if !shift => TabAction::NextTab,
775 0x09 if shift => TabAction::PrevTab,
776 b'1'..=b'9' => TabAction::SwitchToIndex((key - b'1') as usize),
778 _ => TabAction::None,
779 }
780 }
781
782 pub(crate) fn execute_action(&mut self, action: TabAction) -> bool {
784 match action {
785 TabAction::NewTab => self.new_tab("").is_some(),
786 TabAction::CloseTab => {
787 if let Some(id) = self.active_tab {
788 self.close_tab(id)
789 } else {
790 false
791 }
792 }
793 TabAction::NextTab => self.next_tab(),
794 TabAction::PrevTab => self.prev_tab(),
795 TabAction::SwitchToIndex(idx) => self.switch_to_index(idx),
796 TabAction::ReopenClosed => self.reopen_closed_tab().is_some(),
797 TabAction::None => false,
798 }
799 }
800
801 pub(crate) fn tick(&mut self) {
803 self.current_tick += 1;
804 }
805
806 pub(crate) fn set_viewport_width(&mut self, width: i32) {
808 self.tab_bar.visible_width = width;
809 }
810
811 pub(crate) fn handle_tab_bar_click(&mut self, x: i32, _y: i32) -> TabAction {
814 let num_tabs = self.tab_order.len();
815 let ids: Vec<TabId> = self.tab_order.clone();
816
817 if let Some(id) = self.tab_bar.is_close_button_hit(x, &ids, num_tabs) {
819 self.close_tab(id);
820 return TabAction::CloseTab;
821 }
822
823 if self.tab_bar.is_new_tab_button_hit(x, num_tabs) {
825 return TabAction::NewTab;
826 }
827
828 if let Some(id) = self.tab_bar.tab_at_x(x, &ids, num_tabs) {
830 self.switch_tab(id);
831 return TabAction::SwitchToIndex(0); }
833
834 TabAction::None
835 }
836}
837
838fn title_from_url(url: &str) -> String {
844 if url.is_empty() {
845 return "New Tab".to_string();
846 }
847 let stripped = url
849 .strip_prefix("https://")
850 .or_else(|| url.strip_prefix("http://"))
851 .or_else(|| url.strip_prefix("veridian://"))
852 .unwrap_or(url);
853 let host = stripped.split('/').next().unwrap_or(stripped);
855 if host.is_empty() {
856 "New Tab".to_string()
857 } else {
858 host.to_string()
859 }
860}
861
862fn truncate_title(title: &str, max_len: usize) -> String {
864 if title.len() <= max_len {
865 title.to_string()
866 } else if max_len <= 3 {
867 title[..max_len].to_string()
868 } else {
869 let mut s = title[..max_len - 3].to_string();
870 s.push_str("...");
871 s
872 }
873}
874
875fn render_text_simple(
878 buf: &mut [u32],
879 buf_width: usize,
880 x: usize,
881 y: usize,
882 text: &str,
883 color: u32,
884) {
885 for (i, _ch) in text.chars().enumerate() {
886 let px = x + i * 8 + 2;
887 let py = y;
888 if py * buf_width + px < buf.len() {
889 buf[py * buf_width + px] = color;
890 }
891 for dy in 0..5_usize {
893 for dx in 0..5_usize {
894 let ppx = x + i * 8 + dx + 1;
895 let ppy = y.saturating_sub(2) + dy;
896 if ppy * buf_width + ppx < buf.len() && (dy + dx) % 2 == 0 {
897 buf[ppy * buf_width + ppx] = color;
898 }
899 }
900 }
901 }
902}
903
904fn render_close_button(
906 buf: &mut [u32],
907 buf_width: usize,
908 x: usize,
909 y: usize,
910 size: usize,
911 color: u32,
912) {
913 for i in 0..size {
914 let px1 = x + i;
916 let py1 = y + i;
917 if py1 * buf_width + px1 < buf.len() {
918 buf[py1 * buf_width + px1] = color;
919 }
920 let px2 = x + size - 1 - i;
922 let py2 = y + i;
923 if py2 * buf_width + px2 < buf.len() {
924 buf[py2 * buf_width + px2] = color;
925 }
926 }
927}
928
929#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[test]
938 fn test_navigation_history_new() {
939 let h = NavigationHistory::new();
940 assert_eq!(h.current_url(), "");
941 assert!(!h.can_go_back());
942 assert!(!h.can_go_forward());
943 }
944
945 #[test]
946 fn test_navigation_navigate() {
947 let mut h = NavigationHistory::new();
948 h.navigate("https://example.com");
949 assert_eq!(h.current_url(), "https://example.com");
950 h.navigate("https://test.com");
951 assert_eq!(h.current_url(), "https://test.com");
952 assert!(h.can_go_back());
953 assert!(!h.can_go_forward());
954 }
955
956 #[test]
957 fn test_navigation_back_forward() {
958 let mut h = NavigationHistory::new();
959 h.navigate("https://a.com");
960 h.navigate("https://b.com");
961 h.navigate("https://c.com");
962
963 let back = h.go_back().unwrap();
964 assert_eq!(back, "https://b.com");
965 assert!(h.can_go_forward());
966
967 let fwd = h.go_forward().unwrap();
968 assert_eq!(fwd, "https://c.com");
969 }
970
971 #[test]
972 fn test_navigation_clears_forward_on_navigate() {
973 let mut h = NavigationHistory::new();
974 h.navigate("https://a.com");
975 h.navigate("https://b.com");
976 h.go_back();
977 h.navigate("https://c.com");
978 assert!(!h.can_go_forward());
979 }
980
981 #[test]
982 fn test_tab_new() {
983 let tab = Tab::new(1, "https://example.com", 1);
984 assert_eq!(tab.id, 1);
985 assert_eq!(tab.url, "https://example.com");
986 assert_eq!(tab.title, "example.com");
987 assert!(!tab.active);
988 }
989
990 #[test]
991 fn test_tab_navigate() {
992 let mut tab = Tab::new(1, "", 1);
993 tab.navigate("https://test.org/page");
994 assert_eq!(tab.url, "https://test.org/page");
995 assert_eq!(tab.load_state, TabLoadState::Loading);
996 }
997
998 #[test]
999 fn test_tab_back_forward() {
1000 let mut tab = Tab::new(1, "https://a.com", 1);
1001 tab.navigate("https://b.com");
1002 assert!(tab.can_go_back());
1003 let back = tab.go_back().unwrap();
1004 assert_eq!(back, "https://a.com");
1005 let fwd = tab.go_forward().unwrap();
1006 assert_eq!(fwd, "https://b.com");
1007 }
1008
1009 #[test]
1010 fn test_tab_manager_new() {
1011 let tm = TabManager::new();
1012 assert_eq!(tm.tab_count(), 0);
1013 assert!(tm.active_tab_id().is_none());
1014 }
1015
1016 #[test]
1017 fn test_new_tab() {
1018 let mut tm = TabManager::new();
1019 let id = tm.new_tab("https://example.com").unwrap();
1020 assert_eq!(tm.tab_count(), 1);
1021 assert_eq!(tm.active_tab_id(), Some(id));
1022 let tab = tm.get_tab(id).unwrap();
1023 assert!(tab.active);
1024 }
1025
1026 #[test]
1027 fn test_close_tab() {
1028 let mut tm = TabManager::new();
1029 let id1 = tm.new_tab("https://a.com").unwrap();
1030 let id2 = tm.new_tab("https://b.com").unwrap();
1031 tm.switch_tab(id2);
1032 tm.close_tab(id2);
1033 assert_eq!(tm.tab_count(), 1);
1034 assert_eq!(tm.active_tab_id(), Some(id1));
1035 }
1036
1037 #[test]
1038 fn test_close_last_tab() {
1039 let mut tm = TabManager::new();
1040 let id = tm.new_tab("https://a.com").unwrap();
1041 tm.close_tab(id);
1042 assert_eq!(tm.tab_count(), 1);
1044 let tab = tm.get_tab(id).unwrap();
1045 assert_eq!(tab.url, "");
1046 }
1047
1048 #[test]
1049 fn test_next_prev_tab() {
1050 let mut tm = TabManager::new();
1051 let id1 = tm.new_tab("https://a.com").unwrap();
1052 let id2 = tm.new_tab("https://b.com").unwrap();
1053 let id3 = tm.new_tab("https://c.com").unwrap();
1054 tm.switch_tab(id1);
1055
1056 tm.next_tab();
1057 assert_eq!(tm.active_tab_id(), Some(id2));
1058 tm.next_tab();
1059 assert_eq!(tm.active_tab_id(), Some(id3));
1060 tm.next_tab(); assert_eq!(tm.active_tab_id(), Some(id1));
1062
1063 tm.prev_tab(); assert_eq!(tm.active_tab_id(), Some(id3));
1065 }
1066
1067 #[test]
1068 fn test_move_tab() {
1069 let mut tm = TabManager::new();
1070 let id1 = tm.new_tab("a").unwrap();
1071 let id2 = tm.new_tab("b").unwrap();
1072 let id3 = tm.new_tab("c").unwrap();
1073 tm.move_tab(id3, 0);
1074 assert_eq!(tm.tab_order()[0], id3);
1075 assert_eq!(tm.tab_order()[1], id1);
1076 assert_eq!(tm.tab_order()[2], id2);
1077 }
1078
1079 #[test]
1080 fn test_switch_to_index() {
1081 let mut tm = TabManager::new();
1082 let _id1 = tm.new_tab("a").unwrap();
1083 let id2 = tm.new_tab("b").unwrap();
1084 tm.switch_to_index(1);
1085 assert_eq!(tm.active_tab_id(), Some(id2));
1086 assert!(!tm.switch_to_index(99));
1087 }
1088
1089 #[test]
1090 fn test_max_tabs() {
1091 let mut tm = TabManager::new();
1092 tm.max_tabs = 3;
1093 tm.new_tab("1").unwrap();
1094 tm.new_tab("2").unwrap();
1095 tm.new_tab("3").unwrap();
1096 assert!(tm.new_tab("4").is_none());
1097 }
1098
1099 #[test]
1100 fn test_reopen_closed() {
1101 let mut tm = TabManager::new();
1102 let id1 = tm.new_tab("https://saved.com").unwrap();
1103 let _id2 = tm.new_tab("https://keep.com").unwrap();
1104 tm.switch_tab(id1);
1105 tm.close_tab(id1);
1106 let reopened = tm.reopen_closed_tab().unwrap();
1107 let tab = tm.get_tab(reopened).unwrap();
1108 assert_eq!(tab.url, "https://saved.com");
1109 }
1110
1111 #[test]
1112 fn test_decode_shortcut() {
1113 assert_eq!(
1114 TabManager::decode_shortcut(true, false, b't'),
1115 TabAction::NewTab
1116 );
1117 assert_eq!(
1118 TabManager::decode_shortcut(true, false, b'w'),
1119 TabAction::CloseTab
1120 );
1121 assert_eq!(
1122 TabManager::decode_shortcut(true, true, b'T'),
1123 TabAction::ReopenClosed
1124 );
1125 assert_eq!(
1126 TabManager::decode_shortcut(true, false, b'1'),
1127 TabAction::SwitchToIndex(0)
1128 );
1129 assert_eq!(
1130 TabManager::decode_shortcut(true, false, b'5'),
1131 TabAction::SwitchToIndex(4)
1132 );
1133 assert_eq!(
1134 TabManager::decode_shortcut(false, false, b't'),
1135 TabAction::None
1136 );
1137 }
1138
1139 #[test]
1140 fn test_execute_action() {
1141 let mut tm = TabManager::new();
1142 tm.execute_action(TabAction::NewTab);
1143 assert_eq!(tm.tab_count(), 1);
1144 tm.execute_action(TabAction::NewTab);
1145 assert_eq!(tm.tab_count(), 2);
1146 tm.execute_action(TabAction::NextTab);
1147 }
1149
1150 #[test]
1151 fn test_title_from_url() {
1152 assert_eq!(title_from_url(""), "New Tab");
1153 assert_eq!(title_from_url("https://example.com"), "example.com");
1154 assert_eq!(title_from_url("https://example.com/path"), "example.com");
1155 assert_eq!(title_from_url("veridian://settings"), "settings");
1156 }
1157
1158 #[test]
1159 fn test_truncate_title() {
1160 assert_eq!(truncate_title("hello", 10), "hello");
1161 assert_eq!(truncate_title("hello world test", 10), "hello w...");
1162 assert_eq!(truncate_title("ab", 2), "ab");
1163 }
1164
1165 #[test]
1166 fn test_tab_bar_compute_width() {
1167 let bar = TabBar::new(800);
1168 let w = bar.compute_tab_width(5);
1169 assert!(w >= bar.min_tab_width);
1170 assert!(w <= bar.max_tab_width);
1171 }
1172
1173 #[test]
1174 fn test_tab_bar_tab_at_x() {
1175 let bar = TabBar::new(800);
1176 let ids = [1u64, 2, 3];
1177 let tw = bar.compute_tab_width(3);
1178 assert_eq!(bar.tab_at_x(tw / 2, &ids, 3), Some(1));
1179 assert_eq!(bar.tab_at_x(tw + tw / 2, &ids, 3), Some(2));
1180 }
1181
1182 #[test]
1183 fn test_tabs_in_order() {
1184 let mut tm = TabManager::new();
1185 let id1 = tm.new_tab("a").unwrap();
1186 let id2 = tm.new_tab("b").unwrap();
1187 let tabs = tm.tabs_in_order();
1188 assert_eq!(tabs.len(), 2);
1189 assert_eq!(tabs[0].id, id1);
1190 assert_eq!(tabs[1].id, id2);
1191 }
1192
1193 #[test]
1194 fn test_tab_set_title() {
1195 let mut tab = Tab::new(1, "https://example.com", 1);
1196 tab.set_title("My Page");
1197 assert_eq!(tab.title, "My Page");
1198 tab.set_title("");
1199 assert_eq!(tab.title, "example.com");
1200 }
1201
1202 #[test]
1203 fn test_tab_reload() {
1204 let mut tab = Tab::new(1, "https://example.com", 1);
1205 tab.finish_loading();
1206 assert_eq!(tab.load_state, TabLoadState::Idle);
1207 tab.reload();
1208 assert_eq!(tab.load_state, TabLoadState::Loading);
1209 }
1210
1211 #[test]
1212 fn test_tab_fail_loading() {
1213 let mut tab = Tab::new(1, "https://example.com", 1);
1214 tab.fail_loading("Connection refused");
1215 assert_eq!(tab.load_state, TabLoadState::Error);
1216 assert_eq!(tab.error_message.as_deref(), Some("Connection refused"));
1217 }
1218
1219 #[test]
1220 fn test_tab_bar_default() {
1221 let bar = TabBar::default();
1222 assert_eq!(bar.visible_width, 800);
1223 assert_eq!(bar.height, 32);
1224 }
1225
1226 #[test]
1227 fn test_tab_manager_tick() {
1228 let mut tm = TabManager::new();
1229 tm.tick();
1230 tm.tick();
1231 assert_eq!(tm.current_tick, 2);
1232 }
1233
1234 #[test]
1235 fn test_navigation_history_counts() {
1236 let mut h = NavigationHistory::new();
1237 h.navigate("a");
1238 h.navigate("b");
1239 h.navigate("c");
1240 assert_eq!(h.back_count(), 2);
1241 assert_eq!(h.forward_count(), 0);
1242 h.go_back();
1243 assert_eq!(h.back_count(), 1);
1244 assert_eq!(h.forward_count(), 1);
1245 }
1246}