Skip to main content

gitkraft_gui/widgets/
tab_bar.rs

1//! Tab bar widget — renders a horizontal row of repository tabs at the top of
2//! the window, similar to GitKraken's tab bar.
3
4use iced::widget::{button, container, row, scrollable, text, Space};
5use iced::{Alignment, Element, Length};
6
7use crate::icons;
8use crate::message::Message;
9use crate::state::GitKraft;
10use crate::theme;
11use crate::theme::ThemeColors;
12
13/// Render the tab bar above the header toolbar.
14pub fn view(state: &GitKraft) -> Element<'_, Message> {
15    let c = state.colors();
16
17    let mut tabs_row = row![].spacing(0).align_y(Alignment::Center);
18
19    for (idx, tab) in state.tabs.iter().enumerate() {
20        let is_active = idx == state.active_tab;
21        let name = tab.display_name().to_string();
22
23        // Repo icon
24        let icon = if tab.has_repo() {
25            icon!(
26                icons::FOLDER_OPEN,
27                12,
28                if is_active { c.accent } else { c.muted }
29            )
30        } else {
31            icon!(
32                icons::PERSON_FILL,
33                12,
34                if is_active { c.accent } else { c.muted }
35            )
36        };
37
38        // Tab label
39        let label = text(name).size(12).color(if is_active {
40            c.text_primary
41        } else {
42            c.text_secondary
43        });
44
45        // Close button (only show if there's more than 1 tab)
46        let close_btn: Element<'_, Message> = if state.tabs.len() > 1 {
47            button(icon!(icons::X_CIRCLE, 10, c.muted))
48                .padding([0, 4])
49                .style(theme::ghost_button)
50                .on_press(Message::CloseTab(idx))
51                .into()
52        } else {
53            Space::with_width(0).into()
54        };
55
56        let tab_content = row![
57            icon,
58            Space::with_width(6),
59            label,
60            Space::with_width(4),
61            close_btn
62        ]
63        .align_y(Alignment::Center);
64
65        let tab_btn = button(tab_content)
66            .padding([6, 12])
67            .style(if is_active {
68                theme::active_tab_button
69            } else {
70                theme::ghost_button
71            })
72            .on_press(Message::SwitchTab(idx));
73
74        tabs_row = tabs_row.push(tab_btn);
75    }
76
77    // "+" button to add a new tab
78    let new_tab_btn = button(icon!(icons::PLUS_CIRCLE, 14, c.text_secondary))
79        .padding([6, 10])
80        .style(theme::ghost_button)
81        .on_press(Message::NewTab);
82
83    tabs_row = tabs_row.push(new_tab_btn);
84
85    let scrollable_tabs = scrollable(tabs_row)
86        .direction(scrollable::Direction::Horizontal(
87            scrollable::Scrollbar::new(),
88        ))
89        .style(crate::theme::overlay_scrollbar)
90        .width(Length::Fill);
91
92    container(scrollable_tabs)
93        .width(Length::Fill)
94        .style(tab_bar_style)
95        .into()
96}
97
98/// Dark background style for the tab bar — slightly darker than the header.
99fn tab_bar_style(theme: &iced::Theme) -> container::Style {
100    let c = ThemeColors::from_theme(theme);
101    container::Style {
102        background: Some(iced::Background::Color(iced::Color {
103            r: (c.bg.r - 0.02).max(0.0),
104            g: (c.bg.g - 0.02).max(0.0),
105            b: (c.bg.b - 0.02).max(0.0),
106            a: 1.0,
107        })),
108        border: iced::Border {
109            color: c.border,
110            width: 0.0,
111            radius: 0.0.into(),
112        },
113        ..Default::default()
114    }
115}