feat(viewer): restyle toolbar and add line wrap toggle

This commit is contained in:
2026-06-18 23:59:49 -05:00
parent 60d1e67fbe
commit b94ca105de
2 changed files with 102 additions and 44 deletions

View File

@@ -373,6 +373,40 @@ bool compactButton(const char* label, bool active = false) {
return clicked;
}
bool toolbarSegmentButton(const char* label, bool active, float width) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.29f, 0.49f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.12f, 0.22f, 0.39f, 1.0f));
}
const bool clicked = ImGui::Button(label, {width, 0.0f});
if (active) ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
return clicked;
}
int wrappedLineCount(std::string_view text, int wrap_columns) {
if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) return 1;
int lines = 0;
size_t cursor = 0;
while (cursor < text.size()) {
size_t remaining = text.size() - cursor;
if (remaining <= static_cast<size_t>(wrap_columns)) {
++lines;
break;
}
size_t split = cursor + static_cast<size_t>(wrap_columns);
size_t break_at = text.rfind(' ', split);
if (break_at == std::string_view::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
break_at = split;
cursor = break_at;
while (cursor < text.size() && text[cursor] == ' ') ++cursor;
++lines;
}
return std::max(lines, 1);
}
struct MinimapSegment {
float weight = 0.0f;
ImU32 color = syntax_normal;
@@ -545,17 +579,43 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& size, f
void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
float scale, ImU32 background = IM_COL32(0, 0, 0, 0), float left_gutter = 0.0f,
float minimum_width = 0.0f, float custom_row_height = 0.0f) {
const float row_height = custom_row_height > 0.0f ? custom_row_height : scaled(21.0f, scale);
float minimum_width = 0.0f, float custom_row_height = 0.0f, int wrap_columns = 0) {
const float base_row_height = scaled(21.0f, scale);
const int visual_lines = wrappedLineCount(text, wrap_columns);
const float row_height = custom_row_height > 0.0f ? custom_row_height : base_row_height * static_cast<float>(visual_lines);
const ImVec2 start = ImGui::GetCursorScreenPos();
const float width = std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
const float width = minimum_width > 0.0f ? minimum_width :
std::max(minimum_width, ImGui::CalcTextSize(text.c_str()).x + left_gutter + scaled(12.0f, scale));
ImGui::InvisibleButton("##code_line", {width, row_height});
const ImVec2 minimum = ImGui::GetItemRectMin();
const ImVec2 maximum = ImGui::GetItemRectMax();
ImDrawList* draw = ImGui::GetWindowDrawList();
if ((background & IM_COL32_A_MASK) != 0)
draw->AddRectFilled(minimum, maximum, background);
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax);
if (wrap_columns <= 0 || static_cast<int>(text.size()) <= wrap_columns) {
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale)}, text, language, syntax);
return;
}
size_t cursor = 0;
int line_index = 0;
while (cursor < text.size()) {
size_t remaining = text.size() - cursor;
size_t chunk_end = text.size();
if (remaining > static_cast<size_t>(wrap_columns)) {
chunk_end = cursor + static_cast<size_t>(wrap_columns);
size_t break_at = text.rfind(' ', chunk_end);
if (break_at == std::string::npos || break_at < cursor + static_cast<size_t>(wrap_columns / 3))
break_at = chunk_end;
chunk_end = break_at;
}
const std::string chunk = text.substr(cursor, chunk_end - cursor);
drawSyntaxText(draw, {start.x + left_gutter, start.y + scaled(2.0f, scale) + base_row_height * static_cast<float>(line_index)},
chunk, language, syntax);
cursor = chunk_end;
while (cursor < text.size() && text[cursor] == ' ') ++cursor;
++line_index;
}
}
void drawCodeLineNumber(int value, float x, float y, ImU32 color) {
@@ -792,52 +852,43 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_TB_PEN);
ImGui::SameLine(0, scaled(7, scale));
ImGui::TextUnformatted(path_.c_str());
ImGui::SameLine(std::max(scaled(240, scale), ImGui::GetWindowWidth() - scaled(455, scale)));
const bool historical = !commit_id_.empty();
const std::string source_label = historical ? "Commit " + commit_id_.substr(0, 8)
: staged_ ? "Staged" : "Unstaged";
if (compactButton(source_label.c_str(), true)) {}
ImGui::SameLine();
if (compactButton("File View", mode_ == Mode::file)) {
const float top_right_width = scaled(72.0f, scale);
ImGui::SameLine(std::max(scaled(160.0f, scale), ImGui::GetWindowWidth() - top_right_width));
ImGui::TextDisabled("UTF-8");
ImGui::SameLine(0, scaled(10.0f, scale));
if (compactButton(ICON_TB_XMARK)) close();
ImGui::Separator();
const float segment_width = scaled(86.0f, scale);
const float file_group_width = segment_width * 2.0f;
const float blame_group_width = segment_width * 2.0f;
const float wrap_width = scaled(34.0f, scale);
const float between_groups = scaled(22.0f, scale);
const float wrap_gap = scaled(12.0f, scale);
const float toolbar_width = file_group_width + between_groups + blame_group_width + wrap_gap + wrap_width;
ImGui::SetCursorPosX(std::max(0.0f, (ImGui::GetWindowWidth() - toolbar_width) * 0.5f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {0.0f, ImGui::GetStyle().ItemSpacing.y});
if (toolbarSegmentButton("File View", mode_ == Mode::file, segment_width)) {
mode_ = Mode::file; loadSupplement(repository, manager, mode_, notice);
}
ImGui::SameLine();
if (compactButton("Diff View", mode_ == Mode::diff)) mode_ = Mode::diff;
ImGui::SameLine(0, scaled(28, scale));
if (compactButton("Blame", mode_ == Mode::blame)) {
if (toolbarSegmentButton("Diff View", mode_ == Mode::diff, segment_width)) mode_ = Mode::diff;
ImGui::SameLine(0, between_groups);
if (toolbarSegmentButton("Blame", mode_ == Mode::blame, segment_width)) {
mode_ = Mode::blame; loadSupplement(repository, manager, mode_, notice);
}
ImGui::SameLine();
if (compactButton("History", mode_ == Mode::history)) {
if (toolbarSegmentButton("History", mode_ == Mode::history, segment_width)) {
mode_ = Mode::history; loadSupplement(repository, manager, mode_, notice);
}
if (!historical) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1));
if (compactButton(staged_ ? "Unstage File" : "Stage File")) {
const bool changed = staged_ ? manager.unstageFile(repository, path_, notice)
: manager.stageFile(repository, path_, notice);
if (changed) { staged_ = !staged_; reload(repository, manager, notice); }
}
ImGui::PopStyleColor();
}
ImGui::SameLine();
if (compactButton(ICON_TB_XMARK)) close();
ImGui::PopStyleVar();
ImGui::SameLine(0, wrap_gap);
toolbarSegmentButton(ICON_TB_BARS, line_wrap_, wrap_width);
if (ImGui::IsItemClicked()) line_wrap_ = !line_wrap_;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort))
ImGui::SetTooltip("%s", line_wrap_ ? "Disable line wrap" : "Enable line wrap");
ImGui::Separator();
if (!path_.empty()) {
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - scaled(116, scale));
ImGui::TextDisabled("UTF-8");
if (!historical && !staged_) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
if (compactButton(ICON_TB_TRASH_CAN)) {
if (manager.discardFile(repository, path_, notice)) close();
}
ImGui::PopStyleColor();
}
ImGui::Separator();
}
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file || mode_ == Mode::blame || mode_ == Mode::history;
const float minimap_width = scaled(168.0f, scale);
@@ -877,7 +928,8 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
}
}
ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
ImGuiWindowFlags content_flags = line_wrap_ ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar;
ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, content_flags);
const bool use_code_font = code_font && mode_ != Mode::history;
if (use_code_font) ImGui::PushFont(code_font, 0.0f);
const float row_height = scaled(21, scale);
@@ -886,6 +938,11 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
const float content_width = std::max(
ImGui::GetContentRegionAvail().x - (show_minimap ? minimap_width + minimap_gap + scrollbar_width + scaled(6.0f, scale) : 0.0f),
scaled(200.0f, scale));
const float wrapable_text_width = std::max(scaled(32.0f, scale), content_width - scaled(12.0f, scale));
const float code_character_width = ImGui::CalcTextSize("M").x;
const int wrap_columns = line_wrap_ && code_character_width > 0.0f
? std::max(12, static_cast<int>(wrapable_text_width / code_character_width))
: 0;
// Keep the first source row clear of the toolbar and aligned with a normal row inset.
ImGui::Dummy({0.0f, row_height});
@@ -938,7 +995,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
line.kind == LineKind::removed ? IM_COL32(86, 38, 42, 170) : IM_COL32(0, 0, 0, 0);
drawCodeLine(line.text, language,
line.kind == LineKind::removed ? old_syntax : new_syntax,
scale, background, code_gutter, content_width);
scale, background, code_gutter, content_width, 0.0f, wrap_columns);
const float text_y = line_minimum.y + scaled(2.0f, scale);
drawCodeLineNumber(line.old_number, line_minimum.x + scaled(6.0f, scale), text_y,
IM_COL32(126, 132, 142, 255));
@@ -970,7 +1027,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
const ImU32 background = line.show_attribution ? IM_COL32(39, 42, 50, 150) : IM_COL32(31, 34, 40, 105);
const float blame_row_height = line.show_attribution ? scaled(28.0f, scale) : scaled(21.0f, scale);
drawCodeLine(line.text, language, syntax, scale, background, code_gutter, content_width,
blame_row_height);
blame_row_height, wrap_columns);
ImDrawList* draw = ImGui::GetWindowDrawList();
const ImVec2 line_maximum = ImGui::GetItemRectMax();
draw->AddRectFilled({line_minimum.x, line_minimum.y},
@@ -1013,7 +1070,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
ImGui::PushID(static_cast<int>(index));
const ImVec2 line_minimum = ImGui::GetCursorScreenPos();
drawCodeLine((*lines)[index], language, syntax, scale, IM_COL32(0, 0, 0, 0),
code_gutter, content_width);
code_gutter, content_width, 0.0f, wrap_columns);
drawCodeLineNumber(static_cast<int>(index + 1), line_minimum.x + scaled(6.0f, scale),
line_minimum.y + scaled(2.0f, scale), IM_COL32(126, 132, 142, 255));
ImGui::PopID();

View File

@@ -60,6 +60,7 @@ private:
std::vector<std::string> file_lines_;
std::vector<BlameLine> blame_lines_;
std::vector<HistoryEntry> history_entries_;
bool line_wrap_ = false;
void reload(RepositoryView& repository, GitManager& manager, std::string& notice);
void loadSupplement(RepositoryView& repository, GitManager& manager, Mode mode, std::string& notice);