⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
tabs.rs

1//! Tabbed Browsing
2//!
3//! Manages multiple browser tabs with independent browsing contexts.
4//! Each tab maintains its own URL, title, navigation history, and
5//! rendering state. The tab bar provides visual tab switching with
6//! keyboard shortcuts (Ctrl+T, Ctrl+W, Ctrl+Tab, Ctrl+Shift+Tab).
7
8#![allow(dead_code)]
9
10use alloc::{
11    collections::BTreeMap,
12    string::{String, ToString},
13    vec::Vec,
14};
15
16// ---------------------------------------------------------------------------
17// Tab identity
18// ---------------------------------------------------------------------------
19
20/// Unique identifier for a tab
21pub type TabId = u64;
22
23// ---------------------------------------------------------------------------
24// Navigation history
25// ---------------------------------------------------------------------------
26
27/// Navigation history for a single tab
28#[derive(Debug, Clone)]
29pub struct NavigationHistory {
30    /// Back stack (most recent at end)
31    back: Vec<String>,
32    /// Current URL
33    current: String,
34    /// Forward stack (most recent at end)
35    forward: Vec<String>,
36    /// Maximum history entries per direction
37    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    /// Navigate to a new URL, pushing current to back stack
57    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    /// Go back one page, returns the URL to navigate to
69    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    /// Go forward one page, returns the URL to navigate to
77    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    /// Whether back navigation is available
85    pub(crate) fn can_go_back(&self) -> bool {
86        !self.back.is_empty()
87    }
88
89    /// Whether forward navigation is available
90    pub(crate) fn can_go_forward(&self) -> bool {
91        !self.forward.is_empty()
92    }
93
94    /// Current URL
95    pub(crate) fn current_url(&self) -> &str {
96        &self.current
97    }
98
99    /// Number of entries in back stack
100    pub(crate) fn back_count(&self) -> usize {
101        self.back.len()
102    }
103
104    /// Number of entries in forward stack
105    pub(crate) fn forward_count(&self) -> usize {
106        self.forward.len()
107    }
108}
109
110// ---------------------------------------------------------------------------
111// Tab loading state
112// ---------------------------------------------------------------------------
113
114/// Loading state of a tab
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub enum TabLoadState {
117    /// Tab is idle / fully loaded
118    #[default]
119    Idle,
120    /// DNS resolution in progress
121    Resolving,
122    /// Connecting to server
123    Connecting,
124    /// Downloading page content
125    Loading,
126    /// Parsing and rendering
127    Rendering,
128    /// Load failed
129    Error,
130}
131
132// ---------------------------------------------------------------------------
133// Tab
134// ---------------------------------------------------------------------------
135
136/// A single browser tab
137#[derive(Clone)]
138pub struct Tab {
139    /// Unique tab identifier
140    pub id: TabId,
141    /// Tab title (from <title> tag or URL)
142    pub title: String,
143    /// Current URL
144    pub url: String,
145    /// Whether this tab is the active (visible) tab
146    pub active: bool,
147    /// Loading state
148    pub load_state: TabLoadState,
149    /// Favicon data (raw pixel bytes, 16x16 BGRA)
150    pub favicon: Option<Vec<u8>>,
151    /// Navigation history
152    pub history: NavigationHistory,
153    /// Whether the tab has been modified (e.g., form data)
154    pub dirty: bool,
155    /// Whether the tab is pinned
156    pub pinned: bool,
157    /// Tab creation order (for sorting)
158    pub creation_order: u64,
159    /// Last active timestamp (tick count)
160    pub last_active_tick: u64,
161    /// Optional error message
162    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    /// Navigate this tab to a new URL
188    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    /// Mark loading complete
197    pub(crate) fn finish_loading(&mut self) {
198        self.load_state = TabLoadState::Idle;
199    }
200
201    /// Mark loading failed
202    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    /// Set the tab title (from <title> tag)
208    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    /// Get display title (truncated for tab bar)
217    pub(crate) fn display_title(&self, max_len: usize) -> String {
218        truncate_title(&self.title, max_len)
219    }
220
221    /// Whether the tab can go back
222    pub(crate) fn can_go_back(&self) -> bool {
223        self.history.can_go_back()
224    }
225
226    /// Whether the tab can go forward
227    pub(crate) fn can_go_forward(&self) -> bool {
228        self.history.can_go_forward()
229    }
230
231    /// Go back, returns URL if successful
232    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    /// Go forward, returns URL if successful
241    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    /// Reload the current page
250    pub(crate) fn reload(&mut self) {
251        self.load_state = TabLoadState::Loading;
252        self.error_message = None;
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Tab bar (visual representation)
258// ---------------------------------------------------------------------------
259
260/// Visual tab bar state for rendering
261pub struct TabBar {
262    /// Width of each tab in pixels
263    pub tab_width: i32,
264    /// Minimum tab width
265    pub min_tab_width: i32,
266    /// Maximum tab width
267    pub max_tab_width: i32,
268    /// Height of the tab bar
269    pub height: i32,
270    /// Scroll offset for many tabs
271    pub scroll_offset: i32,
272    /// Total visible width
273    pub visible_width: i32,
274    /// Close button size
275    pub close_button_size: i32,
276    /// Hovered tab (for visual feedback)
277    pub hovered_tab: Option<TabId>,
278    /// Hovered close button
279    pub hovered_close: Option<TabId>,
280    /// Tab being dragged
281    pub dragging_tab: Option<TabId>,
282    /// Drag x offset
283    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    /// Calculate the width for each tab given the number of tabs
310    pub(crate) fn compute_tab_width(&self, num_tabs: usize) -> i32 {
311        if num_tabs == 0 {
312            return self.max_tab_width;
313        }
314        // Leave space for new-tab button (32px)
315        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    /// Get the tab at a given x position, given an ordered list of tab IDs
321    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    /// Check if the click is on the close button of a tab
336    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        // Close button is in the right portion of the tab
352        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    /// Check if click is on the new-tab button
362    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    /// Render the tab bar into a pixel buffer (BGRA format)
369    /// Returns the rendered line data for the tab bar area
370    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        // Background
375        let bg_color: u32 = 0xFF2D2D30; // dark gray
376        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        // Draw each tab
385        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 // active tab: lighter
393            } else if self.hovered_tab == Some(tab.id) {
394                0xFF353535_u32 // hovered: slightly lighter
395            } else {
396                0xFF2D2D30_u32 // inactive: same as bar
397            };
398
399            // Tab body
400            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            // Active tab indicator (blue line at top)
410            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            // Tab title (simplified: 1 char = 8px)
423            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            // Close button (X)
430            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            // Loading indicator
449            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        // New tab button (+)
463        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    /// Scroll to ensure a tab is visible
470    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// ---------------------------------------------------------------------------
484// Tab manager
485// ---------------------------------------------------------------------------
486
487/// Keyboard shortcut actions for tabs
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum TabAction {
490    /// Ctrl+T: new tab
491    NewTab,
492    /// Ctrl+W: close current tab
493    CloseTab,
494    /// Ctrl+Tab: next tab
495    NextTab,
496    /// Ctrl+Shift+Tab: previous tab
497    PrevTab,
498    /// Ctrl+1..9: switch to tab N
499    SwitchToIndex(usize),
500    /// Ctrl+Shift+T: reopen last closed tab
501    ReopenClosed,
502    /// None
503    None,
504}
505
506/// Manages all browser tabs
507pub struct TabManager {
508    /// All tabs, keyed by TabId
509    tabs: BTreeMap<TabId, Tab>,
510    /// Ordered list of tab IDs (display order)
511    tab_order: Vec<TabId>,
512    /// Currently active tab ID
513    active_tab: Option<TabId>,
514    /// Next tab ID to assign
515    next_id: TabId,
516    /// Maximum number of tabs allowed
517    max_tabs: usize,
518    /// Recently closed tab URLs (for reopen)
519    recently_closed: Vec<String>,
520    /// Maximum recently closed entries
521    max_recently_closed: usize,
522    /// Tab bar visual state
523    pub tab_bar: TabBar,
524    /// Current tick counter
525    current_tick: u64,
526}
527
528impl Default for TabManager {
529    fn default() -> Self {
530        Self::new()
531    }
532}
533
534impl TabManager {
535    /// Maximum tabs default
536    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    /// Create a new tab with the given URL.
553    /// Returns the TabId, or None if max tabs reached.
554    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 no active tab, make this one active
567        if self.active_tab.is_none() {
568            self.switch_tab(id);
569        }
570
571        Some(id)
572    }
573
574    /// Close a tab by ID. Returns true if closed.
575    /// If closing the active tab, switches to an adjacent tab.
576    pub(crate) fn close_tab(&mut self, id: TabId) -> bool {
577        // Don't close the last tab - open a blank tab instead
578        if self.tabs.len() <= 1 {
579            if let Some(tab) = self.tabs.get_mut(&id) {
580                // Save URL for reopen
581                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        // Find index of tab being closed
597        let order_idx = match self.tab_order.iter().position(|&t| t == id) {
598            Some(idx) => idx,
599            None => return false,
600        };
601
602        // Save URL for reopen
603        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        // Remove from data structures
613        self.tabs.remove(&id);
614        self.tab_order.remove(order_idx);
615
616        // If we closed the active tab, switch to adjacent
617        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    /// Switch to a tab by ID
634    pub(crate) fn switch_tab(&mut self, id: TabId) -> bool {
635        if !self.tabs.contains_key(&id) {
636            return false;
637        }
638
639        // Deactivate current
640        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        // Activate new
647        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        // Ensure visible in tab bar
654        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    /// Switch to the next tab (wraps around)
662    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    /// Switch to the previous tab (wraps around)
676    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    /// Switch to tab at a specific index (0-based)
694    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    /// Move a tab to a new position in the tab order
703    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    /// Reopen the most recently closed tab
715    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    /// Get the active tab
721    pub(crate) fn active_tab(&self) -> Option<&Tab> {
722        self.active_tab.and_then(|id| self.tabs.get(&id))
723    }
724
725    /// Get the active tab mutably
726    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    /// Get a tab by ID
732    pub(crate) fn get_tab(&self, id: TabId) -> Option<&Tab> {
733        self.tabs.get(&id)
734    }
735
736    /// Get a tab mutably by ID
737    pub(crate) fn get_tab_mut(&mut self, id: TabId) -> Option<&mut Tab> {
738        self.tabs.get_mut(&id)
739    }
740
741    /// Get all tabs in display order
742    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    /// Number of open tabs
750    pub(crate) fn tab_count(&self) -> usize {
751        self.tabs.len()
752    }
753
754    /// Active tab ID
755    pub(crate) fn active_tab_id(&self) -> Option<TabId> {
756        self.active_tab
757    }
758
759    /// Ordered tab IDs
760    pub(crate) fn tab_order(&self) -> &[TabId] {
761        &self.tab_order
762    }
763
764    /// Process a keyboard shortcut, returning the action to take
765    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            // Tab key (scancode approximation)
774            0x09 if !shift => TabAction::NextTab,
775            0x09 if shift => TabAction::PrevTab,
776            // Ctrl+1 through Ctrl+9
777            b'1'..=b'9' => TabAction::SwitchToIndex((key - b'1') as usize),
778            _ => TabAction::None,
779        }
780    }
781
782    /// Execute a tab action
783    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    /// Update tick counter
802    pub(crate) fn tick(&mut self) {
803        self.current_tick += 1;
804    }
805
806    /// Set the tab bar width
807    pub(crate) fn set_viewport_width(&mut self, width: i32) {
808        self.tab_bar.visible_width = width;
809    }
810
811    /// Handle tab bar click at position (x, y)
812    /// Returns an action to take
813    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        // Check close button first
818        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        // Check new tab button
824        if self.tab_bar.is_new_tab_button_hit(x, num_tabs) {
825            return TabAction::NewTab;
826        }
827
828        // Check tab click
829        if let Some(id) = self.tab_bar.tab_at_x(x, &ids, num_tabs) {
830            self.switch_tab(id);
831            return TabAction::SwitchToIndex(0); // signal that we switched
832        }
833
834        TabAction::None
835    }
836}
837
838// ---------------------------------------------------------------------------
839// Helpers
840// ---------------------------------------------------------------------------
841
842/// Extract a display title from a URL
843fn title_from_url(url: &str) -> String {
844    if url.is_empty() {
845        return "New Tab".to_string();
846    }
847    // Strip protocol
848    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    // Take up to first /
854    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
862/// Truncate a title to max_len characters, adding ellipsis if needed
863fn 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
875/// Simple text rendering: draw text as colored pixels (1 char = 8px wide)
876/// This is a placeholder; real rendering uses the font system.
877fn 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        // Draw a small dot pattern for each character
892        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
904/// Render a small X close button
905fn 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        // Diagonal \
915        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        // Diagonal /
921        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// ---------------------------------------------------------------------------
930// Tests
931// ---------------------------------------------------------------------------
932
933#[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        // Should still have one tab (blank)
1043        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(); // wraps
1061        assert_eq!(tm.active_tab_id(), Some(id1));
1062
1063        tm.prev_tab(); // wraps back
1064        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        // Should have moved to next tab
1148    }
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}