180 lines
8.3 KiB
C++
180 lines
8.3 KiB
C++
#include "ui/graph_renderer.h"
|
|
|
|
#include "managers/avatar_cache.h"
|
|
#include "models/repository.h"
|
|
#include <IconsFontAwesome6.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
|
|
void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& parent,
|
|
ImU32 color, float scale, int route_slot) {
|
|
const float thickness = 2.0f * scale;
|
|
const auto stroke = [&](ImU32 stroke_color, float stroke_thickness) {
|
|
draw->PathClear();
|
|
draw->PathLineTo(child);
|
|
const float horizontal = parent.x - child.x;
|
|
const float vertical = parent.y - child.y;
|
|
if (std::abs(horizontal) < 0.5f * scale || vertical <= 0.0f) {
|
|
draw->PathLineTo(parent);
|
|
draw->PathStroke(stroke_color, stroke_thickness);
|
|
return;
|
|
}
|
|
|
|
// First-parent lane changes turn on the parent commit row. Additional merge
|
|
// parents leave horizontally on the child row. Both routes terminate as
|
|
// square T-junctions instead of bending through the front or back of a node.
|
|
if (route_slot == 0) {
|
|
draw->PathLineTo({child.x, parent.y});
|
|
draw->PathLineTo(parent);
|
|
} else {
|
|
draw->PathLineTo({parent.x, child.y});
|
|
draw->PathLineTo(parent);
|
|
}
|
|
draw->PathStroke(stroke_color, stroke_thickness);
|
|
};
|
|
|
|
stroke(IM_COL32(17, 21, 27, 255), thickness + 2.4f * scale);
|
|
stroke(color, thickness);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ImU32 GraphRenderer::laneColor(int lane, int alpha) {
|
|
static constexpr ImVec4 colors[] = {
|
|
{0.08f, 0.70f, 0.83f, 1.0f},
|
|
{0.23f, 0.45f, 0.92f, 1.0f},
|
|
{0.78f, 0.25f, 0.78f, 1.0f},
|
|
{0.94f, 0.31f, 0.35f, 1.0f},
|
|
{1.00f, 0.48f, 0.16f, 1.0f},
|
|
{0.94f, 0.73f, 0.18f, 1.0f},
|
|
{0.25f, 0.73f, 0.50f, 1.0f},
|
|
};
|
|
ImVec4 color = colors[static_cast<size_t>(std::abs(lane)) % std::size(colors)];
|
|
color.w = static_cast<float>(alpha) / 255.0f;
|
|
return ImGui::ColorConvertFloat4ToU32(color);
|
|
}
|
|
|
|
float GraphRenderer::requiredWidth(const std::vector<CommitInfo>& commits, float scale) {
|
|
int maximum_lane = 0;
|
|
for (const auto& commit : commits) maximum_lane = std::max(maximum_lane, commit.lane);
|
|
return std::max((46.0f + maximum_lane * 22.0f) * scale, 60.0f * scale);
|
|
}
|
|
|
|
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
|
const std::vector<CommitInfo>& commits, const std::vector<float>& row_heights,
|
|
const std::vector<std::vector<int>>& parent_rows, AvatarCache* avatars,
|
|
const ImVec2* ref_connector_start) const {
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
const float row_height = row_heights[static_cast<size_t>(row)];
|
|
const float content_height = std::max(px(1.0f), row_height - ImGui::GetStyle().CellPadding.y * 2.0f);
|
|
const float lane_spacing = px(22.0f);
|
|
const float x = origin.x + px(17.0f) + lane_spacing * commit.lane;
|
|
const float y = origin.y + content_height * 0.5f;
|
|
const float cell_right = origin.x + ImGui::GetContentRegionAvail().x;
|
|
const float row_clip_padding = ImGui::GetStyle().CellPadding.y + px(1.0f);
|
|
|
|
// Start the lane ribbon at the profile circle's centerline. The opaque node backing
|
|
// masks its left edge, while the vertical inset keeps neighboring row tints separate.
|
|
const float ribbon_left = std::min(x, cell_right);
|
|
const float ribbon_top = origin.y + px(1.0f);
|
|
const float ribbon_bottom = origin.y + content_height - px(1.0f);
|
|
draw->AddRectFilled(
|
|
{ribbon_left, ribbon_top},
|
|
{cell_right, ribbon_bottom},
|
|
laneColor(commit.graph_color, 38));
|
|
if (ref_connector_start) {
|
|
const ImVec2 end{x, y};
|
|
const float turn_x = end.x - px(8.0f);
|
|
const auto stroke_connector = [&](ImU32 color, float thickness) {
|
|
draw->PathClear();
|
|
draw->PathLineTo(*ref_connector_start);
|
|
draw->PathLineTo({turn_x, ref_connector_start->y});
|
|
draw->PathLineTo({turn_x, end.y});
|
|
draw->PathLineTo(end);
|
|
draw->PathStroke(color, thickness);
|
|
};
|
|
draw->PushClipRectFullScreen();
|
|
stroke_connector(IM_COL32(17, 21, 27, 255), px(3.8f));
|
|
stroke_connector(laneColor(commit.graph_color, 175), px(1.6f));
|
|
draw->PopClipRect();
|
|
}
|
|
|
|
std::vector<float> row_offsets(row_heights.size() + 1, 0.0f);
|
|
for (size_t index = 0; index < row_heights.size(); ++index)
|
|
row_offsets[index + 1] = row_offsets[index] + row_heights[index];
|
|
const float graph_top = origin.y - row_offsets[static_cast<size_t>(row)];
|
|
const auto center_y = [&](int index) {
|
|
return graph_top + row_offsets[static_cast<size_t>(index)] +
|
|
(row_heights[static_cast<size_t>(index)] - ImGui::GetStyle().CellPadding.y * 2.0f) * 0.5f;
|
|
};
|
|
|
|
// Every row redraws edges crossing its clip rectangle. This keeps long paths continuous
|
|
// without allowing table row clipping to cut out intermediate segments.
|
|
draw->PushClipRect(
|
|
{origin.x, origin.y - row_clip_padding},
|
|
{origin.x + ImGui::GetContentRegionAvail().x, origin.y + content_height + row_clip_padding}, true);
|
|
for (int child_row = 0; child_row < static_cast<int>(commits.size()); ++child_row) {
|
|
if (row_heights[static_cast<size_t>(child_row)] <= 0.0f) continue;
|
|
const CommitInfo& child = commits[static_cast<size_t>(child_row)];
|
|
const auto& child_parents = parent_rows[static_cast<size_t>(child_row)];
|
|
for (size_t parent_index = 0; parent_index < child_parents.size(); ++parent_index) {
|
|
const int parent_row = child_parents[parent_index];
|
|
if (parent_row <= child_row || row < child_row || row > parent_row ||
|
|
row_heights[static_cast<size_t>(parent_row)] <= 0.0f) continue;
|
|
const CommitInfo& parent = commits[static_cast<size_t>(parent_row)];
|
|
|
|
const float child_x = origin.x + px(17.0f) + lane_spacing * child.lane;
|
|
const float parent_x = origin.x + px(17.0f) + lane_spacing * parent.lane;
|
|
const float child_y = center_y(child_row);
|
|
const float parent_y = center_y(parent_row);
|
|
// An edge belongs to the branch it leaves, including its square lane transition,
|
|
// so its color stays consistent with the child node/ref chip.
|
|
const int edge_color = parent_index == 0 ? child.graph_color : parent.graph_color;
|
|
drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
|
|
laneColor(edge_color, 235), scale_, static_cast<int>(parent_index));
|
|
}
|
|
}
|
|
draw->PopClipRect();
|
|
|
|
const ImU32 lane_color = laneColor(commit.graph_color);
|
|
const auto show_identity_tooltip = [&](float hit_radius) {
|
|
if (!ImGui::IsWindowHovered()) return;
|
|
const ImVec2 mouse = ImGui::GetIO().MousePos;
|
|
const float delta_x = mouse.x - x;
|
|
const float delta_y = mouse.y - y;
|
|
if (delta_x * delta_x + delta_y * delta_y > hit_radius * hit_radius) return;
|
|
if (commit.email.empty()) ImGui::SetTooltip("%s", commit.author.c_str());
|
|
else ImGui::SetTooltip("%s <%s>", commit.author.c_str(), commit.email.c_str());
|
|
};
|
|
if (commit.parents > 1) {
|
|
draw->AddCircleFilled({x, y}, px(6.0f), lane_color);
|
|
draw->AddCircle({x, y}, px(7.5f), laneColor(commit.graph_color, 130), 0, px(1.2f));
|
|
show_identity_tooltip(px(9.0f));
|
|
ImGui::Dummy({0.0f, content_height});
|
|
return;
|
|
}
|
|
|
|
const float radius = px(9.6f);
|
|
// The opaque backing masks every lane segment before the avatar is painted.
|
|
draw->AddCircleFilled({x, y}, radius + px(1.8f), IM_COL32(19, 24, 31, 255));
|
|
draw->AddCircle({x, y}, radius + px(0.8f), lane_color, 0, px(2.0f));
|
|
draw->AddCircleFilled({x, y}, radius - px(1.0f), IM_COL32(232, 238, 242, 255));
|
|
|
|
const unsigned int texture = avatars ? avatars->textureFor(commit.email) : 0;
|
|
if (texture) {
|
|
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(texture)),
|
|
{x - radius + px(1.5f), y - radius + px(1.5f)},
|
|
{x + radius - px(1.5f), y + radius - px(1.5f)},
|
|
{0, 0}, {1, 1}, IM_COL32_WHITE, radius);
|
|
} else {
|
|
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_FA_USER);
|
|
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_FA_USER);
|
|
}
|
|
show_identity_tooltip(radius + px(2.0f));
|
|
ImGui::Dummy({0.0f, content_height});
|
|
}
|