Skip to main content

gitkraft_gui/widgets/
header.rs

1//! Top toolbar / header bar for the GitKraft main layout.
2//!
3//! Shows: repo name │ branch name │ fetch button │ toggle sidebar.
4
5use iced::widget::{button, container, row, text, Space};
6use iced::{Alignment, Element, Length};
7
8use crate::features::editor::editor_selector;
9use crate::features::theme::view::theme_selector;
10use crate::icons;
11use crate::message::Message;
12use crate::state::GitKraft;
13use crate::theme;
14use crate::view_utils;
15
16/// Render the top toolbar row.
17pub fn view(state: &GitKraft) -> Element<'_, Message> {
18    let tab = state.active_tab();
19    let c = state.colors();
20    let dims = state.layout();
21    let compact = dims.compact_toolbar;
22
23    // ── Repo name ─────────────────────────────────────────────────────────
24    let repo_icon = icon!(icons::FOLDER_OPEN, 14, c.accent);
25
26    let repo_name = text(state.repo_display_name())
27        .size(14)
28        .color(c.text_primary)
29        .wrapping(iced::widget::text::Wrapping::None);
30
31    let separator = || text("|").size(14).color(c.border);
32
33    // ── Branch indicator ──────────────────────────────────────────────────
34    let branch_icon = icon!(icons::GIT_BRANCH, 14, c.green);
35
36    let branch_name_str = tab.current_branch.as_deref().unwrap_or("(detached)");
37
38    let branch_label = text(branch_name_str)
39        .size(14)
40        .color(c.text_primary)
41        .wrapping(iced::widget::text::Wrapping::None);
42
43    // ── Repo state badge (if not clean) ───────────────────────────────────
44    let state_badge: Element<'_, Message> = if let Some(ref info) = tab.repo_info {
45        if info.state != gitkraft_core::RepoState::Clean {
46            text(format!(" [{}]", info.state))
47                .size(12)
48                .color(c.yellow)
49                .into()
50        } else {
51            Space::new().into()
52        }
53    } else {
54        Space::new().into()
55    };
56
57    // ── Fetch button ──────────────────────────────────────────────────────
58    let fetch_icon = icon!(icons::CLOUD_ARROW_DOWN, 14, c.accent);
59
60    let fetch_msg = (!tab.remotes.is_empty()).then_some(Message::Fetch);
61    let fetch_btn = crate::view_utils::on_press_maybe(
62        button(if compact {
63            row![fetch_icon].align_y(Alignment::Center)
64        } else {
65            row![fetch_icon, Space::new().width(4), text("Fetch").size(12)]
66                .align_y(Alignment::Center)
67        })
68        .padding([4, if compact { 8 } else { 10 }])
69        .style(theme::toolbar_button),
70        fetch_msg,
71    );
72
73    // ── Open another repo button ──────────────────────────────────────────
74    let open_icon = icon!(icons::FOLDER_OPEN, 14, c.text_secondary);
75
76    let open_btn = view_utils::toolbar_btn_adaptive(open_icon, "Open", Message::OpenRepo, compact);
77
78    // ── Close repo button (return to welcome screen) ──────────────────────
79    let close_icon = icon!(icons::X_CIRCLE, 14, c.text_secondary);
80
81    let close_btn =
82        view_utils::toolbar_btn_adaptive(close_icon, "Close", Message::CloseRepo, compact);
83
84    // ── Toggle sidebar ────────────────────────────────────────────────────
85    let sidebar_icon_char = if state.sidebar_expanded {
86        icons::CHEVRON_LEFT
87    } else {
88        icons::CHEVRON_RIGHT
89    };
90    let sidebar_icon = icon!(sidebar_icon_char, 14, c.text_secondary);
91
92    let sidebar_btn = button(sidebar_icon)
93        .padding([4, 8])
94        .style(theme::icon_button)
95        .on_press(Message::ToggleSidebar);
96
97    // ── Loading indicator ─────────────────────────────────────────────
98    // Use FluxFrames::CORNERS from tui-spinner so both GUI and TUI share
99    // the same symbol set — no duplication.
100    let spinner_frame: String = {
101        let frames = tui_spinner::FluxFrames::CORNERS; // &'static [char]
102        let ch = frames[state.animation_tick as usize % frames.len()];
103        ch.to_string()
104    };
105
106    let loading_indicator: Element<'_, Message> = if tab.is_loading {
107        let spinner = text(spinner_frame)
108            .size(14)
109            .color(c.accent)
110            .font(iced::Font::MONOSPACE);
111        if compact {
112            spinner.into()
113        } else {
114            row![
115                spinner,
116                iced::widget::Space::new().width(4),
117                text("Loading…").size(12).color(c.yellow),
118            ]
119            .align_y(iced::Alignment::Center)
120            .into()
121        }
122    } else {
123        Space::new().into()
124    };
125
126    // ── Assemble ──────────────────────────────────────────────────────────
127    let left_items = row![
128        sidebar_btn,
129        Space::new().width(8),
130        repo_icon,
131        Space::new().width(6),
132        repo_name,
133        Space::new().width(10),
134        separator(),
135        Space::new().width(10),
136        branch_icon,
137        Space::new().width(6),
138        branch_label,
139        state_badge,
140        Space::new().width(10),
141        separator(),
142        Space::new().width(10),
143        loading_indicator,
144    ]
145    .align_y(Alignment::Center);
146
147    let theme_w = if compact { 140.0 } else { 180.0 };
148    let mut right_items = row![
149        fetch_btn,
150        Space::new().width(4),
151        open_btn,
152        Space::new().width(4),
153        close_btn,
154    ]
155    .align_y(Alignment::Center);
156    if !dims.tiny_toolbar {
157        right_items = right_items.push(Space::new().width(8)).push(theme_selector(
158            state.current_theme_index,
159            state.follow_system,
160            theme_w,
161        ));
162    }
163    if !dims.minimal_toolbar {
164        right_items = right_items
165            .push(Space::new().width(4))
166            .push(editor_selector(&state.editor));
167    }
168
169    let toolbar = row![
170        container(left_items).width(Length::Fill).clip(true),
171        right_items,
172    ]
173    .align_y(Alignment::Center)
174    .padding([
175        if dims.compact_vertical { 2 } else { 6 },
176        if compact { 8 } else { 12 },
177    ])
178    .width(Length::Fill);
179
180    container(toolbar)
181        .width(Length::Fill)
182        .style(theme::header_style)
183        .into()
184}