use iced::widget::{column, container, mouse_area, row, text, Space};
use iced::{Alignment, Element, Length};
use crate::features;
use crate::icons;
use crate::message::Message;
use crate::state::{DragTarget, DragTargetH, GitKraft};
use crate::theme;
use crate::theme::ThemeColors;
use crate::view_utils;
use crate::widgets;
impl GitKraft {
pub fn view(&self) -> Element<'_, Message> {
let c = self.colors();
let tab_bar = widgets::tab_bar::view(self);
if !self.has_repo() {
let welcome = features::repo::view::welcome_view(self);
let outer = column![tab_bar, welcome]
.width(Length::Fill)
.height(Length::Fill);
return container(outer)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::bg_style)
.into();
}
let tab = self.active_tab();
let header = widgets::header::view(self);
let sidebar: Element<'_, Message> = if self.sidebar_expanded {
let branches = features::branches::view::view(self);
let stash = features::stash::view::view(self);
let remotes = features::remotes::view::view(self);
let sidebar_content = container(
column![
branches,
iced::widget::horizontal_rule(1),
stash,
iced::widget::horizontal_rule(1),
remotes
]
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fixed(self.sidebar_width))
.height(Length::Fill)
.style(theme::sidebar_style);
let divider = widgets::divider::vertical_divider(DragTarget::SidebarRight, &c);
row![sidebar_content, divider].height(Length::Fill).into()
} else {
Space::with_width(0).into()
};
let commit_log_content = container(features::commits::view::view(self))
.width(Length::Fixed(self.commit_log_width))
.height(Length::Fill);
let commit_divider = widgets::divider::vertical_divider(DragTarget::CommitLogRight, &c);
let commit_log: Element<'_, Message> = row![commit_log_content, commit_divider]
.height(Length::Fill)
.into();
let diff_viewer = container(features::diff::view::view(self))
.width(Length::Fill)
.height(Length::Fill);
let middle = row![sidebar, commit_log, diff_viewer]
.height(Length::Fill)
.width(Length::Fill);
let h_divider = widgets::divider::horizontal_divider(DragTargetH::StagingTop, &c);
let staging = container(features::staging::view::view(self))
.width(Length::Fill)
.height(Length::Fixed(self.staging_height));
let status_bar = status_bar_view(self);
let mut main_col = column![].width(Length::Fill).height(Length::Fill);
main_col = main_col.push(tab_bar);
if let Some(ref err) = tab.error_message {
main_col = main_col.push(error_banner(err, &c));
}
main_col = main_col
.push(header)
.push(middle)
.push(h_divider)
.push(staging)
.push(status_bar);
let body = container(main_col)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::bg_style);
let ma: Element<'_, Message> = mouse_area(body)
.on_move(|p| Message::PaneDragMove(p.x, p.y))
.on_release(Message::PaneDragEnd)
.into();
if self.active_tab().context_menu.is_some() {
let backdrop = mouse_area(
container(Space::new(Length::Fill, Length::Fill)).style(theme::backdrop_style),
)
.on_press(Message::CloseContextMenu)
.on_right_press(Message::CloseContextMenu);
let (menu_x, menu_y) = context_menu_position(self);
let menu_panel = context_menu_panel(self, &c);
let positioned = column![
Space::with_height(menu_y),
row![Space::with_width(menu_x), menu_panel,],
]
.width(Length::Fill)
.height(Length::Fill);
iced::widget::stack![ma, backdrop, positioned].into()
} else {
ma
}
}
}
fn status_bar_view(state: &GitKraft) -> Element<'_, Message> {
let tab = state.active_tab();
let c = state.colors();
let status_text = if tab.is_loading {
tab.status_message
.as_deref()
.unwrap_or("Loading…")
.to_string()
} else {
tab.status_message.as_deref().unwrap_or("Ready").to_string()
};
let status_label = text(status_text).size(12).color(c.text_secondary);
let branch_info: Element<'_, Message> = if let Some(ref branch) = tab.current_branch {
let icon = icon!(icons::GIT_BRANCH, 12, c.accent);
let label = text(branch.as_str()).size(12).color(c.text_primary);
row![icon, Space::with_width(4), label]
.align_y(Alignment::Center)
.into()
} else {
Space::with_width(0).into()
};
let repo_state_info: Element<'_, Message> = if let Some(ref info) = tab.repo_info {
let state_str = format!("{}", info.state);
if state_str != "Clean" {
text(state_str).size(12).color(c.yellow).into()
} else {
Space::with_width(0).into()
}
} else {
Space::with_width(0).into()
};
let changes_summary = {
let unstaged_count = tab.unstaged_changes.len();
let staged_count = tab.staged_changes.len();
if unstaged_count > 0 || staged_count > 0 {
text(format!("{unstaged_count} unstaged, {staged_count} staged"))
.size(12)
.color(c.muted)
} else {
text("Working tree clean").size(12).color(c.muted)
}
};
let zoom_label: Element<'_, Message> = if (state.ui_scale - 1.0).abs() > 0.01 {
text(format!("{}%", (state.ui_scale * 100.0).round() as u32))
.size(11)
.color(c.muted)
.into()
} else {
Space::with_width(0).into()
};
let bar = row![
status_label,
Space::with_width(Length::Fill),
changes_summary,
Space::with_width(16),
zoom_label,
Space::with_width(16),
repo_state_info,
Space::with_width(16),
branch_info,
]
.align_y(Alignment::Center)
.padding([4, 10])
.width(Length::Fill);
container(bar)
.width(Length::Fill)
.style(theme::header_style)
.into()
}
fn error_banner<'a>(message: &str, c: &ThemeColors) -> Element<'a, Message> {
let icon = icon!(icons::EXCLAMATION_TRIANGLE, 14, c.red);
let msg = text(message.to_string()).size(13).color(c.text_primary);
let dismiss = iced::widget::button(icon!(icons::X_CIRCLE, 14, c.text_secondary))
.padding([2, 6])
.on_press(Message::DismissError);
let banner_row = row![
icon,
Space::with_width(8),
msg,
Space::with_width(Length::Fill),
dismiss,
]
.align_y(Alignment::Center)
.padding([6, 12])
.width(Length::Fill);
container(banner_row)
.width(Length::Fill)
.style(theme::error_banner_style)
.into()
}
fn context_menu_position(state: &GitKraft) -> (f32, f32) {
let (x, y) = state.active_tab().context_menu_pos;
((x + 2.0).max(2.0), (y + 2.0).max(2.0))
}
fn context_menu_panel<'a>(state: &'a GitKraft, c: &ThemeColors) -> Element<'a, Message> {
use iced::widget::{button, column, container, row, text, Space};
use iced::{Alignment, Length};
let text_primary = c.text_primary;
let menu_item = move |label: &str, msg: Message| {
button(
row![
Space::with_width(4),
text(label.to_string()).size(13).color(text_primary),
]
.align_y(Alignment::Center),
)
.padding([7, 12])
.width(Length::Fill)
.style(theme::context_menu_item)
.on_press(msg)
};
let content: Element<'a, Message> = match &state.active_tab().context_menu {
Some(crate::state::ContextMenu::Branch {
name, is_current, ..
}) => {
let tab = state.active_tab();
let remote = tab
.remotes
.first()
.map(|r| r.name.clone())
.unwrap_or_else(|| "origin".to_string());
let tip_oid: Option<String> = tab
.branches
.iter()
.find(|b| &b.name == name)
.and_then(|b| b.target_oid.clone());
let header =
view_utils::context_menu_header::<Message>(format!("Branch: {name}"), c.muted);
let mut col = column![header];
if !is_current {
col = col.push(menu_item("Checkout", Message::CheckoutBranch(name.clone())));
}
let push_label = format!("Push to {remote}");
let pull_label = format!("Pull from {remote} (rebase)");
col = col
.push(menu_item(&push_label, Message::PushBranch(name.clone())))
.push(menu_item(&pull_label, Message::PullBranch(name.clone())));
col = col.push(view_utils::context_menu_separator::<Message>());
let rebase_label = format!("Rebase current onto '{name}'");
col = col.push(menu_item(&rebase_label, Message::RebaseOnto(name.clone())));
if !is_current {
col = col.push(menu_item(
"Merge into current branch",
Message::MergeBranch(name.clone()),
));
}
col = col.push(view_utils::context_menu_separator::<Message>());
col = col
.push(menu_item(
"Rename\u{2026}",
Message::BeginRenameBranch(name.clone()),
))
.push(menu_item("Delete", Message::DeleteBranch(name.clone())));
col = col.push(view_utils::context_menu_separator::<Message>());
col = col.push(menu_item(
"Copy branch name",
Message::CopyText(name.clone()),
));
if let Some(ref oid) = tip_oid {
col = col.push(menu_item(
"Copy tip commit SHA",
Message::CopyText(oid.clone()),
));
}
if tip_oid.is_some() {
col = col.push(view_utils::context_menu_separator::<Message>());
let oid = tip_oid.clone().unwrap();
col = col
.push(menu_item(
"Create tag here",
Message::BeginCreateTag(oid.clone(), false),
))
.push(menu_item(
"Create annotated tag here\u{2026}",
Message::BeginCreateTag(oid, true),
));
}
col.into()
}
Some(crate::state::ContextMenu::RemoteBranch { name }) => {
let (remote, short_name) = name.split_once('/').unwrap_or(("", name.as_str()));
let header =
view_utils::context_menu_header::<Message>(format!("Remote: {name}"), c.muted);
let local_exists =
state.active_tab().branches.iter().any(|b| {
b.branch_type == gitkraft_core::BranchType::Local && b.name == short_name
});
let mut col = column![header];
if !local_exists {
col = col.push(menu_item(
&format!("Checkout as '{short_name}'"),
Message::CheckoutRemoteBranch(name.clone()),
));
}
col = col.push(view_utils::context_menu_separator::<Message>());
col = col.push(menu_item(
&format!("Delete from {remote}"),
Message::DeleteRemoteBranch(name.clone()),
));
col = col.push(view_utils::context_menu_separator::<Message>());
col = col.push(menu_item(
"Copy branch name",
Message::CopyText(name.clone()),
));
col = col.push(menu_item(
&format!("Copy short name '{short_name}'"),
Message::CopyText(short_name.to_string()),
));
let tip_oid: Option<String> = state
.active_tab()
.branches
.iter()
.find(|b| &b.name == name)
.and_then(|b| b.target_oid.clone());
if let Some(ref oid) = tip_oid {
col = col.push(menu_item(
"Copy tip commit SHA",
Message::CopyText(oid.clone()),
));
}
col.into()
}
Some(crate::state::ContextMenu::Commit { index, oid }) => {
let tab = state.active_tab();
let short = gitkraft_core::utils::short_oid_str(oid);
let msg_text = tab
.commits
.get(*index)
.map(|c| c.message.clone())
.unwrap_or_default();
let header =
view_utils::context_menu_header::<Message>(format!("Commit: {short}"), c.muted);
column![
header,
menu_item(
"Checkout (detached HEAD)",
Message::CheckoutCommitDetached(oid.clone()),
),
menu_item(
"Rebase current branch onto this",
Message::RebaseOntoCommit(oid.clone()),
),
menu_item("Revert commit", Message::RevertCommit(oid.clone())),
menu_item(
"Reset here — soft (keep staged)",
Message::ResetSoft(oid.clone())
),
menu_item(
"Reset here — mixed (keep files)",
Message::ResetMixed(oid.clone())
),
menu_item(
"Reset here — hard (discard all)",
Message::ResetHard(oid.clone())
),
menu_item("Copy commit SHA", Message::CopyText(oid.clone())),
menu_item("Copy commit message", Message::CopyText(msg_text)),
]
.into()
}
None => Space::with_width(0).into(),
};
container(content)
.width(280)
.style(theme::context_menu_style)
.into()
}