use iced::widget::{button, column, container, mouse_area, row, scrollable, text, Row, Space};
use iced::{Alignment, Color, Element, Length};
use crate::icons;
use crate::message::Message;
use crate::state::{GitKraft, RepoTab};
use crate::theme;
use crate::theme::ThemeColors;
use crate::view_utils;
use crate::view_utils::truncate_to_fit;
const ROW_HEIGHT: f32 = 26.0;
const OVERSCAN: usize = 8;
const VISIBLE_ROWS: usize = 50;
pub fn commit_log_scroll_id(tab_index: usize) -> scrollable::Id {
scrollable::Id::new(format!("commit_log_{tab_index}"))
}
fn graph_cell<'a>(
graph_row: &gitkraft_core::GraphRow,
graph_colors: &[Color; 8],
) -> Row<'a, Message> {
let width = graph_row.width;
let len = graph_colors.len();
if width == 0 {
return Row::new().push(
text("● ")
.font(iced::Font::MONOSPACE)
.size(12)
.color(graph_colors[graph_row.node_color % len]),
);
}
let mut column_passthrough: Vec<Option<usize>> = vec![None; width];
let mut has_left_cross = false;
let mut has_right_cross = false;
let mut left_cross_color: usize = 0;
let mut right_cross_color: usize = 0;
let mut cross_left_col: usize = graph_row.node_column;
let mut cross_right_col: usize = graph_row.node_column;
for edge in &graph_row.edges {
if edge.from_column == edge.to_column {
column_passthrough[edge.to_column] = Some(edge.color_index);
} else {
let target = edge.to_column;
if target < graph_row.node_column {
has_left_cross = true;
left_cross_color = edge.color_index;
if target < cross_left_col {
cross_left_col = target;
}
} else if target > graph_row.node_column {
has_right_cross = true;
right_cross_color = edge.color_index;
if target > cross_right_col {
cross_right_col = target;
}
}
}
}
let mut cells: Vec<Element<'a, Message>> = Vec::with_capacity(width);
for col in 0..width {
if col == graph_row.node_column {
let color = graph_colors[graph_row.node_color % len];
cells.push(
text("● ")
.font(iced::Font::MONOSPACE)
.size(12)
.color(color)
.into(),
);
} else if let Some(ci) = column_passthrough.get(col).copied().flatten() {
let in_left = has_left_cross && col >= cross_left_col && col < graph_row.node_column;
let in_right = has_right_cross && col > graph_row.node_column && col <= cross_right_col;
if in_left || in_right {
let cross_ci = if in_left {
left_cross_color
} else {
right_cross_color
};
cells.push(
text("├─")
.font(iced::Font::MONOSPACE)
.size(12)
.color(graph_colors[cross_ci % len])
.into(),
);
} else {
cells.push(
text("│ ")
.font(iced::Font::MONOSPACE)
.size(12)
.color(graph_colors[ci % len])
.into(),
);
}
} else {
let in_left = has_left_cross && col >= cross_left_col && col < graph_row.node_column;
let in_right = has_right_cross && col > graph_row.node_column && col <= cross_right_col;
if in_left {
let color = graph_colors[left_cross_color % len];
if col == cross_left_col {
cells.push(
text("╭─")
.font(iced::Font::MONOSPACE)
.size(12)
.color(color)
.into(),
);
} else {
cells.push(
text("──")
.font(iced::Font::MONOSPACE)
.size(12)
.color(color)
.into(),
);
}
} else if in_right {
let color = graph_colors[right_cross_color % len];
if col == cross_right_col {
cells.push(
text("─╮")
.font(iced::Font::MONOSPACE)
.size(12)
.color(color)
.into(),
);
} else {
cells.push(
text("──")
.font(iced::Font::MONOSPACE)
.size(12)
.color(color)
.into(),
);
}
} else {
cells.push(text(" ").font(iced::Font::MONOSPACE).size(12).into());
}
}
}
Row::with_children(cells).align_y(Alignment::Center)
}
fn commit_row_element<'a>(
tab: &'a RepoTab,
idx: usize,
c: &ThemeColors,
available_summary_px: f32,
) -> Element<'a, Message> {
let commit = &tab.commits[idx];
let is_selected = tab.selected_commit == Some(idx);
let graph_elem: Element<'_, Message> = if let Some(grow) = tab.graph_rows.get(idx) {
graph_cell(grow, &c.graph_colors).into()
} else {
text("").into()
};
let oid_label = text(commit.short_oid.as_str())
.size(12)
.color(c.accent)
.font(iced::Font::MONOSPACE);
let (summary_str, time_str, author_str) = tab
.commit_display
.get(idx)
.map(|(s, t, a)| (s.as_str(), t.as_str(), a.as_str()))
.unwrap_or((commit.summary.as_str(), "", commit.author_name.as_str()));
let display_summary = truncate_to_fit(summary_str, available_summary_px, 7.0);
let summary_label = container(
text(display_summary)
.size(12)
.color(c.text_primary)
.wrapping(iced::widget::text::Wrapping::None),
)
.width(Length::Fill)
.clip(true);
let author_label = container(
text(author_str)
.size(11)
.color(c.text_secondary)
.wrapping(iced::widget::text::Wrapping::None),
)
.width(90)
.clip(true);
let time_label = container(
text(time_str)
.size(11)
.color(c.muted)
.wrapping(iced::widget::text::Wrapping::None),
)
.width(72)
.clip(true);
let row_content = row![
graph_elem,
oid_label,
Space::with_width(6),
summary_label,
Space::with_width(8),
author_label,
Space::with_width(8),
time_label,
]
.align_y(Alignment::Center)
.padding([3, 8]);
let style_fn = if is_selected {
theme::selected_row_style as fn(&iced::Theme) -> iced::widget::container::Style
} else {
theme::surface_style as fn(&iced::Theme) -> iced::widget::container::Style
};
mouse_area(
container(
button(row_content)
.padding(0)
.width(Length::Fill)
.on_press(Message::SelectCommit(idx))
.style(theme::ghost_button),
)
.width(Length::Fill)
.height(Length::Fixed(ROW_HEIGHT))
.clip(true)
.style(style_fn),
)
.on_right_press(Message::OpenCommitContextMenu(idx))
.into()
}
pub fn view(state: &GitKraft) -> Element<'_, Message> {
let tab = state.active_tab();
let c = state.colors();
let header_icon = icon!(icons::CLOCK, 14, c.accent);
let header_text = text("Commit Log").size(14).color(c.text_primary);
let commit_count = text(format!("({})", tab.commits.len()))
.size(12)
.color(c.muted);
let header_row = row![
header_icon,
Space::with_width(6),
header_text,
Space::with_width(6),
commit_count,
]
.align_y(Alignment::Center)
.padding([8, 10]);
if tab.commits.is_empty() {
let empty_msg = text("No commits yet.").size(14).color(c.muted);
let content = column![
header_row,
container(empty_msg)
.width(Length::Fill)
.padding(20)
.center_x(Length::Fill),
]
.width(Length::Fill)
.height(Length::Fill);
return view_utils::surface_panel(content, Length::Fill);
}
let total = tab.commits.len();
let scroll_y = tab.commit_scroll_offset;
let first = ((scroll_y / ROW_HEIGHT) as usize).saturating_sub(OVERSCAN);
let last = (first + VISIBLE_ROWS + 2 * OVERSCAN).min(total);
let top_space = first as f32 * ROW_HEIGHT;
let bottom_space = (total - last) as f32 * ROW_HEIGHT;
let mut list_col = column![].width(Length::Fill);
if top_space > 0.0 {
list_col = list_col.push(Space::with_height(top_space));
}
let available_summary_px = (state.commit_log_width - 280.0).max(40.0);
for idx in first..last {
list_col = list_col.push(commit_row_element(tab, idx, &c, available_summary_px));
}
if bottom_space > 0.0 {
list_col = list_col.push(Space::with_height(bottom_space));
}
if tab.is_loading_more_commits {
list_col = list_col.push(
container(text("Loading more commits…").size(12).color(c.muted))
.width(Length::Fill)
.center_x(Length::Fill)
.padding([10, 0]),
);
}
if !tab.has_more_commits {
list_col = list_col.push(
container(text("— end of history —").size(11).color(c.muted))
.width(Length::Fill)
.center_x(Length::Fill)
.padding([10, 0]),
);
}
let commit_scroll = scrollable(list_col)
.height(Length::Fill)
.id(commit_log_scroll_id(state.active_tab))
.on_scroll(|vp| Message::CommitLogScrolled(vp.absolute_offset().y, vp.relative_offset().y))
.direction(view_utils::thin_scrollbar())
.style(crate::theme::overlay_scrollbar);
let content = column![header_row, commit_scroll]
.width(Length::Fill)
.height(Length::Fill);
view_utils::surface_panel(content, Length::Fill)
}