feat(ui): refine graph, viewer, and repository workflows
This commit is contained in:
@@ -81,6 +81,7 @@ target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo ikv::ikv Open
|
||||
target_compile_definitions(gitree PRIVATE
|
||||
GITREE_VERSION="${PROJECT_VERSION}"
|
||||
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
|
||||
GITREE_IMAGE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||||
$<$<PLATFORM_ID:Windows>:NOMINMAX;WIN32_LEAN_AND_MEAN>
|
||||
)
|
||||
|
||||
|
||||
@@ -179,7 +179,14 @@ void GitManager::readBranches(RepositoryView &repository, git_branch_t type,
|
||||
{
|
||||
const char *name = nullptr;
|
||||
if (git_branch_name(&name, reference) == 0 && name)
|
||||
{
|
||||
if (type == GIT_BRANCH_REMOTE && std::string_view(name).ends_with("/HEAD"))
|
||||
{
|
||||
git_reference_free(reference);
|
||||
continue;
|
||||
}
|
||||
output.emplace_back(name);
|
||||
}
|
||||
git_reference_free(reference);
|
||||
}
|
||||
git_branch_iterator_free(iterator);
|
||||
@@ -297,23 +304,21 @@ void GitManager::computeGraphLanes(RepositoryView &repository)
|
||||
struct Lane
|
||||
{
|
||||
git_oid expected{};
|
||||
int color = 0;
|
||||
};
|
||||
std::vector<Lane> lanes;
|
||||
int next_color = 0;
|
||||
for (auto &commit : repository.commits)
|
||||
{
|
||||
auto current = std::find_if(lanes.begin(), lanes.end(), [&commit](const Lane &lane)
|
||||
{ return git_oid_equal(&lane.expected, &commit.oid) != 0; });
|
||||
if (current == lanes.end())
|
||||
{
|
||||
lanes.push_back({commit.oid, next_color++});
|
||||
lanes.push_back({commit.oid});
|
||||
current = std::prev(lanes.end());
|
||||
}
|
||||
const size_t commit_lane = static_cast<size_t>(std::distance(lanes.begin(), current));
|
||||
size_t active_lane = commit_lane;
|
||||
commit.lane = static_cast<int>(commit_lane);
|
||||
commit.graph_color = current->color;
|
||||
commit.graph_color = commit.lane;
|
||||
for (size_t duplicate = lanes.size(); duplicate-- > 0;)
|
||||
{
|
||||
if (duplicate != active_lane &&
|
||||
@@ -347,7 +352,7 @@ void GitManager::computeGraphLanes(RepositoryView &repository)
|
||||
if (found == lanes.end())
|
||||
lanes.insert(lanes.begin() + static_cast<std::ptrdiff_t>(
|
||||
std::min(active_lane + parent, lanes.size())),
|
||||
{commit.parent_ids[parent], next_color++});
|
||||
{commit.parent_ids[parent]});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,10 +576,15 @@ bool GitManager::openRepository(RepositoryView &repository, const std::string &p
|
||||
return loadRepositoryData(repository, error);
|
||||
}
|
||||
|
||||
bool GitManager::initializeRepository(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
bool GitManager::initializeRepository(RepositoryView &repository, const std::string &path,
|
||||
const std::string &initial_branch, std::string &error)
|
||||
{
|
||||
git_repository *created = nullptr;
|
||||
if (git_repository_init(&created, path.c_str(), 0) != 0)
|
||||
git_repository_init_options options{};
|
||||
git_repository_init_options_init(&options, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
|
||||
options.flags = GIT_REPOSITORY_INIT_MKPATH;
|
||||
options.initial_head = initial_branch.empty() ? "main" : initial_branch.c_str();
|
||||
if (git_repository_init_ext(&created, path.c_str(), &options) != 0)
|
||||
{
|
||||
error = lastError("Unable to initialize repository");
|
||||
return false;
|
||||
@@ -583,6 +593,24 @@ bool GitManager::initializeRepository(RepositoryView &repository, const std::str
|
||||
return openRepository(repository, path, error);
|
||||
}
|
||||
|
||||
bool GitManager::cloneRepository(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, bool shallow, std::string &error)
|
||||
{
|
||||
git_clone_options options{};
|
||||
git_clone_options_init(&options, GIT_CLONE_OPTIONS_VERSION);
|
||||
if (shallow)
|
||||
options.fetch_opts.depth = 1;
|
||||
git_repository *cloned = nullptr;
|
||||
if (git_clone(&cloned, url.c_str(), path.c_str(), &options) != 0)
|
||||
{
|
||||
error = lastError("Unable to clone repository");
|
||||
return false;
|
||||
}
|
||||
repository.close();
|
||||
repository.repo = cloned;
|
||||
return loadRepositoryData(repository, error);
|
||||
}
|
||||
|
||||
bool GitManager::reload(RepositoryView &repository, std::string &error)
|
||||
{
|
||||
return repository.repo && loadRepositoryData(repository, error);
|
||||
@@ -813,6 +841,54 @@ bool GitManager::push(RepositoryView &repository, std::string &error)
|
||||
"Push complete; upstream configured", error);
|
||||
}
|
||||
|
||||
bool GitManager::pushBranch(RepositoryView &repository, const std::string &branch, std::string &error)
|
||||
{
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
|
||||
git_reference *reference = nullptr;
|
||||
if (git_branch_lookup(&reference, repository.repo, branch.c_str(), GIT_BRANCH_LOCAL) != 0)
|
||||
{
|
||||
error = lastError("Unable to find branch");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string remote_name;
|
||||
std::string remote_branch_name;
|
||||
git_reference *upstream = nullptr;
|
||||
if (git_branch_upstream(&upstream, reference) == 0)
|
||||
{
|
||||
const char *upstream_name = nullptr;
|
||||
if (git_branch_name(&upstream_name, upstream) == 0 && upstream_name)
|
||||
{
|
||||
remote_branch_name = upstream_name;
|
||||
const size_t slash = remote_branch_name.find('/');
|
||||
remote_name = slash == std::string::npos ? remote_branch_name : remote_branch_name.substr(0, slash);
|
||||
if (slash != std::string::npos)
|
||||
remote_branch_name.erase(0, slash + 1);
|
||||
}
|
||||
git_reference_free(upstream);
|
||||
}
|
||||
git_reference_free(reference);
|
||||
|
||||
if (!remote_name.empty() && !remote_branch_name.empty())
|
||||
return runGit(repository, {"push", remote_name, branch + ":" + remote_branch_name},
|
||||
"Push complete", error);
|
||||
|
||||
if (repository.remotes.empty())
|
||||
{
|
||||
error = "No remote is configured for this repository";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string remote = std::find(repository.remotes.begin(), repository.remotes.end(), "origin") !=
|
||||
repository.remotes.end()
|
||||
? "origin"
|
||||
: repository.remotes.front();
|
||||
return runGit(repository, {"push", "--set-upstream", remote, branch + ":" + branch},
|
||||
"Push complete; upstream configured", error);
|
||||
}
|
||||
|
||||
bool GitManager::stash(RepositoryView &repository, std::string &error)
|
||||
{
|
||||
return runGit(repository, {"stash", "push", "--include-untracked", "-m", "Gitree stash"},
|
||||
@@ -839,6 +915,16 @@ bool GitManager::stageAll(RepositoryView &repository, std::string &error)
|
||||
return runGit(repository, {"add", "--all"}, "All changes staged", error);
|
||||
}
|
||||
|
||||
bool GitManager::unstageAll(RepositoryView &repository, std::string &error)
|
||||
{
|
||||
if (git_repository_head_unborn(repository.repo) == 1)
|
||||
return runGit(repository, {"rm", "--cached", "-r", "--ignore-unmatch", "--", "."},
|
||||
"All changes unstaged", error);
|
||||
if (runGit(repository, {"restore", "--staged", "--", "."}, "All changes unstaged", error))
|
||||
return true;
|
||||
return runGit(repository, {"reset", "HEAD", "--", "."}, "All changes unstaged", error);
|
||||
}
|
||||
|
||||
bool GitManager::stageFile(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
{
|
||||
return runGit(repository, {"add", "--", path}, "File staged", error);
|
||||
@@ -846,6 +932,9 @@ bool GitManager::stageFile(RepositoryView &repository, const std::string &path,
|
||||
|
||||
bool GitManager::unstageFile(RepositoryView &repository, const std::string &path, std::string &error)
|
||||
{
|
||||
if (git_repository_head_unborn(repository.repo) == 1)
|
||||
return runGit(repository, {"rm", "--cached", "--ignore-unmatch", "--", path},
|
||||
"File unstaged", error);
|
||||
if (runGit(repository, {"restore", "--staged", "--", path}, "File unstaged", error))
|
||||
return true;
|
||||
return runGit(repository, {"reset", "HEAD", "--", path}, "File unstaged", error);
|
||||
|
||||
@@ -11,18 +11,23 @@ public:
|
||||
~GitManager();
|
||||
|
||||
bool openRepository(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool initializeRepository(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool initializeRepository(RepositoryView &repository, const std::string &path,
|
||||
const std::string &initial_branch, std::string &error);
|
||||
bool cloneRepository(RepositoryView &repository, const std::string &url,
|
||||
const std::string &path, bool shallow, std::string &error);
|
||||
bool reload(RepositoryView &repository, std::string &error);
|
||||
bool loadMoreCommits(RepositoryView &repository, size_t page_size, std::string &error);
|
||||
bool checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool fetch(RepositoryView &repository, const std::string &remote, std::string &error);
|
||||
bool pull(RepositoryView &repository, int mode, std::string &error);
|
||||
bool push(RepositoryView &repository, std::string &error);
|
||||
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool stash(RepositoryView &repository, std::string &error);
|
||||
bool popStash(RepositoryView &repository, std::string &error);
|
||||
bool undoCommit(RepositoryView &repository, std::string &error);
|
||||
bool redoCommit(RepositoryView &repository, std::string &error);
|
||||
bool stageAll(RepositoryView &repository, std::string &error);
|
||||
bool unstageAll(RepositoryView &repository, std::string &error);
|
||||
bool stageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool unstageFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
bool discardFile(RepositoryView &repository, const std::string &path, std::string &error);
|
||||
|
||||
@@ -56,6 +56,8 @@ void UserData::load()
|
||||
details_width_ = static_cast<float>(ikv_as_float(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
|
||||
pull_mode_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
|
||||
zoom_percent_ = static_cast<int>(ikv_as_int(value));
|
||||
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(
|
||||
@@ -66,7 +68,7 @@ void UserData::load()
|
||||
}
|
||||
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
|
||||
{
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 12);
|
||||
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
|
||||
for (uint32_t index = 0; index < count; ++index)
|
||||
{
|
||||
const char *path = ikv_as_string(ikv_array_get(history, index));
|
||||
@@ -91,6 +93,7 @@ void UserData::load()
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &height : sidebar_section_heights_)
|
||||
height = std::clamp(height, 42.0f, 500.0f);
|
||||
if (open_repositories_.empty())
|
||||
@@ -111,6 +114,8 @@ void UserData::load()
|
||||
settings >> details_width_;
|
||||
else if (key == "pull_mode")
|
||||
settings >> pull_mode_;
|
||||
else if (key == "zoom_percent")
|
||||
settings >> zoom_percent_;
|
||||
else if (key.rfind("sidebar_section_", 0) == 0)
|
||||
{
|
||||
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
|
||||
@@ -123,6 +128,7 @@ void UserData::load()
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
||||
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
||||
for (float &height : sidebar_section_heights_)
|
||||
height = std::clamp(height, 42.0f, 500.0f);
|
||||
|
||||
@@ -132,7 +138,7 @@ void UserData::load()
|
||||
{
|
||||
if (!path.empty())
|
||||
recently_closed_.push_back(path);
|
||||
if (recently_closed_.size() == 12)
|
||||
if (recently_closed_.size() == 100)
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -152,11 +158,21 @@ void UserData::addRecentlyClosed(const std::string &path)
|
||||
return;
|
||||
std::erase(recently_closed_, path);
|
||||
recently_closed_.insert(recently_closed_.begin(), path);
|
||||
if (recently_closed_.size() > 12)
|
||||
recently_closed_.resize(12);
|
||||
if (recently_closed_.size() > 100)
|
||||
recently_closed_.resize(100);
|
||||
save();
|
||||
}
|
||||
|
||||
std::string UserData::takeRecentlyClosed()
|
||||
{
|
||||
if (recently_closed_.empty())
|
||||
return {};
|
||||
std::string path = std::move(recently_closed_.front());
|
||||
recently_closed_.erase(recently_closed_.begin());
|
||||
save();
|
||||
return path;
|
||||
}
|
||||
|
||||
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
|
||||
{
|
||||
open_repositories_ = std::move(paths);
|
||||
@@ -177,6 +193,7 @@ void UserData::save() const
|
||||
ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
|
||||
ikv_object_set_float(settings, "details_width", details_width_);
|
||||
ikv_object_set_int(settings, "pull_mode", pull_mode_);
|
||||
ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
|
||||
ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
|
||||
for (const float height : sidebar_section_heights_)
|
||||
ikv_array_add_float(heights, height);
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
[[nodiscard]] float detailsWidth() const { return details_width_; }
|
||||
[[nodiscard]] float sidebarSectionHeight(size_t index) const { return sidebar_section_heights_.at(index); }
|
||||
[[nodiscard]] int pullMode() const { return pull_mode_; }
|
||||
[[nodiscard]] int zoomPercent() const { return zoom_percent_; }
|
||||
|
||||
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
||||
void setDetailsWidth(float width) { details_width_ = width; }
|
||||
@@ -29,7 +30,13 @@ public:
|
||||
pull_mode_ = mode;
|
||||
save();
|
||||
}
|
||||
void setZoomPercent(int percent)
|
||||
{
|
||||
zoom_percent_ = percent;
|
||||
save();
|
||||
}
|
||||
void addRecentlyClosed(const std::string &path);
|
||||
std::string takeRecentlyClosed();
|
||||
void setRepositorySession(std::vector<std::string> paths, size_t active_repository);
|
||||
void save() const;
|
||||
|
||||
@@ -45,4 +52,5 @@ private:
|
||||
float details_width_ = 368.0f;
|
||||
std::array<float, 4> sidebar_section_heights_ = {110.0f, 220.0f, 90.0f, 150.0f};
|
||||
int pull_mode_ = 1;
|
||||
int zoom_percent_ = 100;
|
||||
};
|
||||
|
||||
@@ -2,36 +2,155 @@
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
#include <GLFW/glfw3native.h>
|
||||
#include <dwmapi.h>
|
||||
#include <windows.h>
|
||||
#include <wincodec.h>
|
||||
#endif
|
||||
|
||||
WindowManager::~WindowManager() {
|
||||
#ifdef _WIN32
|
||||
namespace
|
||||
{
|
||||
HICON loadPngIcon(const std::filesystem::path &path, UINT size, bool rounded)
|
||||
{
|
||||
const HRESULT com_result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
const bool uninitialize_com = SUCCEEDED(com_result);
|
||||
|
||||
IWICImagingFactory *factory = nullptr;
|
||||
IWICBitmapDecoder *decoder = nullptr;
|
||||
IWICBitmapFrameDecode *frame = nullptr;
|
||||
IWICBitmapScaler *scaler = nullptr;
|
||||
IWICFormatConverter *converter = nullptr;
|
||||
HICON icon = nullptr;
|
||||
|
||||
if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&factory))) ||
|
||||
FAILED(factory->CreateDecoderFromFilename(path.c_str(), nullptr, GENERIC_READ,
|
||||
WICDecodeMetadataCacheOnLoad, &decoder)) ||
|
||||
FAILED(decoder->GetFrame(0, &frame)) ||
|
||||
FAILED(factory->CreateBitmapScaler(&scaler)) ||
|
||||
FAILED(scaler->Initialize(frame, size, size, WICBitmapInterpolationModeFant)) ||
|
||||
FAILED(factory->CreateFormatConverter(&converter)) ||
|
||||
FAILED(converter->Initialize(scaler, GUID_WICPixelFormat32bppBGRA,
|
||||
WICBitmapDitherTypeNone, nullptr, 0.0,
|
||||
WICBitmapPaletteTypeCustom)))
|
||||
{
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<BYTE> pixels(static_cast<size_t>(size) * size * 4);
|
||||
if (FAILED(converter->CopyPixels(nullptr, size * 4,
|
||||
static_cast<UINT>(pixels.size()), pixels.data())))
|
||||
{
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (rounded)
|
||||
{
|
||||
const float radius = static_cast<float>(size) * 0.22f;
|
||||
const float maximum = static_cast<float>(size) - radius - 0.5f;
|
||||
const float minimum = radius - 0.5f;
|
||||
for (UINT y = 0; y < size; ++y)
|
||||
{
|
||||
for (UINT x = 0; x < size; ++x)
|
||||
{
|
||||
const float nearest_x = std::clamp(static_cast<float>(x), minimum, maximum);
|
||||
const float nearest_y = std::clamp(static_cast<float>(y), minimum, maximum);
|
||||
const float dx = static_cast<float>(x) - nearest_x;
|
||||
const float dy = static_cast<float>(y) - nearest_y;
|
||||
const float coverage = std::clamp(radius + 0.5f - std::sqrt(dx * dx + dy * dy),
|
||||
0.0f, 1.0f);
|
||||
pixels[(static_cast<size_t>(y) * size + x) * 4 + 3] =
|
||||
static_cast<BYTE>(coverage * 255.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BITMAPV5HEADER header{};
|
||||
header.bV5Size = sizeof(header);
|
||||
header.bV5Width = static_cast<LONG>(size);
|
||||
header.bV5Height = -static_cast<LONG>(size);
|
||||
header.bV5Planes = 1;
|
||||
header.bV5BitCount = 32;
|
||||
header.bV5Compression = BI_BITFIELDS;
|
||||
header.bV5RedMask = 0x00ff0000;
|
||||
header.bV5GreenMask = 0x0000ff00;
|
||||
header.bV5BlueMask = 0x000000ff;
|
||||
header.bV5AlphaMask = 0xff000000;
|
||||
|
||||
void *bitmap_bits = nullptr;
|
||||
const HDC screen = GetDC(nullptr);
|
||||
const HBITMAP color = CreateDIBSection(screen, reinterpret_cast<BITMAPINFO *>(&header),
|
||||
DIB_RGB_COLORS, &bitmap_bits, nullptr, 0);
|
||||
ReleaseDC(nullptr, screen);
|
||||
const HBITMAP mask = CreateBitmap(size, size, 1, 1, nullptr);
|
||||
if (color && mask && bitmap_bits)
|
||||
{
|
||||
std::memcpy(bitmap_bits, pixels.data(), pixels.size());
|
||||
ICONINFO info{};
|
||||
info.fIcon = TRUE;
|
||||
info.hbmColor = color;
|
||||
info.hbmMask = mask;
|
||||
icon = CreateIconIndirect(&info);
|
||||
}
|
||||
if (color)
|
||||
DeleteObject(color);
|
||||
if (mask)
|
||||
DeleteObject(mask);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (converter)
|
||||
converter->Release();
|
||||
if (scaler)
|
||||
scaler->Release();
|
||||
if (frame)
|
||||
frame->Release();
|
||||
if (decoder)
|
||||
decoder->Release();
|
||||
if (factory)
|
||||
factory->Release();
|
||||
if (uninitialize_com)
|
||||
CoUninitialize();
|
||||
return icon;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
WindowManager::~WindowManager()
|
||||
{
|
||||
destroy();
|
||||
}
|
||||
|
||||
bool WindowManager::create(const char* title, int width, int height) {
|
||||
bool WindowManager::create(const char *title, int width, int height)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
using SetDpiAwarenessContext = BOOL(WINAPI*)(HANDLE);
|
||||
using SetDpiAwarenessContext = BOOL(WINAPI *)(HANDLE);
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
const FARPROC dpi_address = GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
SetDpiAwarenessContext set_dpi_awareness = nullptr;
|
||||
static_assert(sizeof(set_dpi_awareness) == sizeof(dpi_address));
|
||||
std::memcpy(&set_dpi_awareness, &dpi_address, sizeof(set_dpi_awareness));
|
||||
if (set_dpi_awareness) set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
|
||||
if (set_dpi_awareness)
|
||||
set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
|
||||
#endif
|
||||
if (!glfwInit()) return false;
|
||||
if (!glfwInit())
|
||||
return false;
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
|
||||
window_ = glfwCreateWindow(width, height, title, nullptr, nullptr);
|
||||
if (!window_) {
|
||||
if (!window_)
|
||||
{
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
@@ -45,13 +164,17 @@ bool WindowManager::create(const char* title, int width, int height) {
|
||||
glfwGetWindowContentScale(window_, &x_scale, &y_scale);
|
||||
dpi_scale_ = std::max(x_scale, y_scale);
|
||||
applyNativeCaption();
|
||||
applyNativeIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowManager::destroy() {
|
||||
if (!window_) return;
|
||||
void WindowManager::destroy()
|
||||
{
|
||||
if (!window_)
|
||||
return;
|
||||
glfwDestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
destroyNativeIcons();
|
||||
glfwTerminate();
|
||||
}
|
||||
|
||||
@@ -60,39 +183,126 @@ void WindowManager::swapBuffers() { glfwSwapBuffers(window_); }
|
||||
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
||||
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }
|
||||
|
||||
bool WindowManager::consumeDpiChange() {
|
||||
bool WindowManager::consumeDpiChange()
|
||||
{
|
||||
const bool changed = dpi_changed_;
|
||||
dpi_changed_ = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void WindowManager::contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale) {
|
||||
auto* manager = static_cast<WindowManager*>(glfwGetWindowUserPointer(window));
|
||||
if (manager) manager->updateDpi(std::max(x_scale, y_scale));
|
||||
void WindowManager::contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale)
|
||||
{
|
||||
auto *manager = static_cast<WindowManager *>(glfwGetWindowUserPointer(window));
|
||||
if (manager)
|
||||
manager->updateDpi(std::max(x_scale, y_scale));
|
||||
}
|
||||
|
||||
void WindowManager::updateDpi(float scale) {
|
||||
void WindowManager::updateDpi(float scale)
|
||||
{
|
||||
scale = std::clamp(scale, 1.0f, 4.0f);
|
||||
if (scale == dpi_scale_) return;
|
||||
if (scale == dpi_scale_)
|
||||
return;
|
||||
dpi_scale_ = scale;
|
||||
dpi_changed_ = true;
|
||||
applyNativeCaption();
|
||||
}
|
||||
|
||||
void WindowManager::applyNativeCaption() const {
|
||||
void WindowManager::applyNativeCaption() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!window_)
|
||||
return;
|
||||
|
||||
const HWND hwnd = glfwGetWin32Window(window_);
|
||||
if (!hwnd)
|
||||
return;
|
||||
|
||||
const BOOL dark = TRUE;
|
||||
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark)); // DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
const DWORD square_corners = 1; // DWMWCP_DONOTROUND
|
||||
DwmSetWindowAttribute(hwnd, 33, &square_corners, sizeof(square_corners));
|
||||
|
||||
// DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark));
|
||||
|
||||
DWORD corner_pref = 0;
|
||||
|
||||
switch (corner_mode_)
|
||||
{
|
||||
case WindowCornerMode::Default:
|
||||
corner_pref = 0; // DWMWCP_DEFAULT
|
||||
break;
|
||||
|
||||
case WindowCornerMode::DoNotRound:
|
||||
corner_pref = 1; // DWMWCP_DONOTROUND
|
||||
break;
|
||||
|
||||
case WindowCornerMode::Round:
|
||||
corner_pref = 2; // DWMWCP_ROUND
|
||||
break;
|
||||
|
||||
case WindowCornerMode::RoundSmall:
|
||||
corner_pref = 3; // DWMWCP_ROUNDSMALL
|
||||
break;
|
||||
}
|
||||
|
||||
// DWMWA_WINDOW_CORNER_PREFERENCE
|
||||
DwmSetWindowAttribute(hwnd, 33, &corner_pref, sizeof(corner_pref));
|
||||
|
||||
// Windows 11 caption customization. Older versions safely ignore these.
|
||||
const COLORREF caption = RGB(32, 32, 32);
|
||||
const COLORREF caption = static_cast<COLORREF>(caption_color_);
|
||||
const COLORREF border = RGB(51, 55, 63);
|
||||
const COLORREF text = RGB(199, 203, 209);
|
||||
DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption)); // DWMWA_CAPTION_COLOR
|
||||
DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border)); // DWMWA_BORDER_COLOR
|
||||
DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text)); // DWMWA_TEXT_COLOR
|
||||
|
||||
// DWMWA_BORDER_COLOR
|
||||
DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border));
|
||||
|
||||
// DWMWA_CAPTION_COLOR
|
||||
DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption));
|
||||
|
||||
// DWMWA_TEXT_COLOR
|
||||
DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text));
|
||||
#endif
|
||||
}
|
||||
|
||||
void WindowManager::applyNativeIcons()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
destroyNativeIcons();
|
||||
const HWND hwnd = glfwGetWin32Window(window_);
|
||||
const auto asset_dir = std::filesystem::path(GITREE_IMAGE_ASSET_DIR);
|
||||
window_icon_ = loadPngIcon(asset_dir / L"gitree_logo_no_bg.png", 32, false);
|
||||
taskbar_icon_ = loadPngIcon(asset_dir / L"gitree_logo.png", 64, true);
|
||||
|
||||
if (window_icon_)
|
||||
{
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(window_icon_));
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_SMALL2, reinterpret_cast<LPARAM>(window_icon_));
|
||||
}
|
||||
if (taskbar_icon_)
|
||||
{
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(taskbar_icon_));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WindowManager::destroyNativeIcons()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (window_icon_)
|
||||
DestroyIcon(static_cast<HICON>(window_icon_));
|
||||
if (taskbar_icon_)
|
||||
DestroyIcon(static_cast<HICON>(taskbar_icon_));
|
||||
#endif
|
||||
window_icon_ = nullptr;
|
||||
taskbar_icon_ = nullptr;
|
||||
}
|
||||
|
||||
void WindowManager::setCornerMode(WindowCornerMode mode)
|
||||
{
|
||||
corner_mode_ = mode;
|
||||
applyNativeCaption();
|
||||
}
|
||||
|
||||
void WindowManager::setCaptionColor(std::uint32_t color)
|
||||
{
|
||||
caption_color_ = color;
|
||||
applyNativeCaption();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct GLFWwindow;
|
||||
|
||||
class WindowManager
|
||||
{
|
||||
public:
|
||||
enum class WindowCornerMode
|
||||
{
|
||||
Default,
|
||||
DoNotRound,
|
||||
Round,
|
||||
RoundSmall
|
||||
};
|
||||
|
||||
public:
|
||||
WindowManager() = default;
|
||||
~WindowManager();
|
||||
@@ -16,17 +27,36 @@ public:
|
||||
void pollEvents();
|
||||
void swapBuffers();
|
||||
void requestClose();
|
||||
|
||||
[[nodiscard]] bool shouldClose() const;
|
||||
[[nodiscard]] GLFWwindow *nativeWindow() const { return window_; }
|
||||
[[nodiscard]] float dpiScale() const { return dpi_scale_; }
|
||||
|
||||
bool consumeDpiChange();
|
||||
|
||||
void setCornerMode(WindowCornerMode mode);
|
||||
[[nodiscard]] WindowCornerMode cornerMode() const { return corner_mode_; }
|
||||
|
||||
void setCaptionColor(std::uint32_t color);
|
||||
[[nodiscard]] std::uint32_t captionColor() const { return caption_color_; }
|
||||
|
||||
private:
|
||||
static void contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale);
|
||||
|
||||
void updateDpi(float scale);
|
||||
void applyNativeCaption() const;
|
||||
void applyNativeIcons();
|
||||
void destroyNativeIcons();
|
||||
|
||||
GLFWwindow *window_ = nullptr;
|
||||
void *window_icon_ = nullptr;
|
||||
void *taskbar_icon_ = nullptr;
|
||||
|
||||
float dpi_scale_ = 1.0f;
|
||||
bool dpi_changed_ = false;
|
||||
|
||||
WindowCornerMode corner_mode_ = WindowCornerMode::Round;
|
||||
|
||||
// 0x00BBGGRR, same layout Windows COLORREF uses.
|
||||
std::uint32_t caption_color_ = 0x00201B19;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
#include "ui/diff_viewer.h"
|
||||
|
||||
#include "managers/avatar_cache.h"
|
||||
#include "managers/git_manager.h"
|
||||
#include "models/repository.h"
|
||||
|
||||
#include <IconsFontAwesome6.h>
|
||||
#include <IconsTabler.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -48,7 +50,7 @@ struct SyntaxState {
|
||||
SyntaxContinuation continuation = SyntaxContinuation::none;
|
||||
};
|
||||
|
||||
ImU32 blameColor(std::string_view hash, int alpha = 255) {
|
||||
[[maybe_unused]] ImU32 blameColor(std::string_view hash, int alpha = 255) {
|
||||
static constexpr ImU32 colors[] = {
|
||||
IM_COL32(24, 181, 204, 255), IM_COL32(73, 123, 235, 255), IM_COL32(200, 64, 200, 255),
|
||||
IM_COL32(239, 79, 89, 255), IM_COL32(255, 122, 41, 255), IM_COL32(240, 186, 46, 255),
|
||||
@@ -59,7 +61,7 @@ ImU32 blameColor(std::string_view hash, int alpha = 255) {
|
||||
return (colors[value % std::size(colors)] & ~IM_COL32_A_MASK) | (static_cast<ImU32>(alpha) << IM_COL32_A_SHIFT);
|
||||
}
|
||||
|
||||
SyntaxLanguage languageForPath(const std::string& path) {
|
||||
[[maybe_unused]] SyntaxLanguage languageForPath(const std::string& path) {
|
||||
std::string extension = std::filesystem::path(path).extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) {
|
||||
return static_cast<char>(std::tolower(value));
|
||||
@@ -108,7 +110,49 @@ bool isTypeWord(SyntaxLanguage language, std::string_view word) {
|
||||
return wordIs(word, {"bool", "byte", "char", "decimal", "double", "dynamic", "float", "int", "int8_t", "int16_t", "int32_t", "int64_t", "long", "nint", "nuint", "object", "sbyte", "short", "signed", "size_t", "string", "uint", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "ulong", "unsigned", "ushort", "var", "void", "wchar_t"});
|
||||
}
|
||||
|
||||
void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
bool isBuiltin(SyntaxLanguage language, std::string_view word) {
|
||||
switch (language) {
|
||||
case SyntaxLanguage::cpp:
|
||||
return wordIs(word, {"std", "move", "forward", "make_shared", "make_unique", "optional", "string_view", "vector"});
|
||||
case SyntaxLanguage::csharp:
|
||||
return wordIs(word, {"Console", "DateTime", "Dictionary", "Enumerable", "List", "Task", "ValueTask"});
|
||||
case SyntaxLanguage::lua:
|
||||
return wordIs(word, {"assert", "error", "ipairs", "io", "math", "os", "pairs", "pcall", "print", "require", "string", "table", "tonumber", "tostring", "type"});
|
||||
case SyntaxLanguage::python:
|
||||
return wordIs(word, {"abs", "all", "any", "dict", "enumerate", "filter", "float", "int", "len", "list", "map", "max", "min", "object", "open", "print", "range", "reversed", "set", "str", "sum", "super", "tuple", "zip"});
|
||||
case SyntaxLanguage::javascript:
|
||||
return wordIs(word, {"Array", "Boolean", "console", "Date", "Error", "JSON", "Map", "Math", "Number", "Object", "process", "Promise", "RegExp", "require", "Set", "String", "Symbol"});
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isLiteral(std::string_view word) {
|
||||
return wordIs(word, {"false", "False", "nil", "null", "nullptr", "None", "true", "True", "undefined"});
|
||||
}
|
||||
|
||||
bool isDeclarationKeyword(std::string_view word) {
|
||||
return wordIs(word, {"class", "concept", "delegate", "enum", "interface", "namespace", "record", "struct", "type"});
|
||||
}
|
||||
|
||||
bool isFunctionDeclarationKeyword(std::string_view word) {
|
||||
return word == "def" || word == "function";
|
||||
}
|
||||
|
||||
bool isMacroName(std::string_view word) {
|
||||
bool has_letter = false;
|
||||
for (const unsigned char character : word) {
|
||||
if (std::isalpha(character)) {
|
||||
has_letter = true;
|
||||
if (std::islower(character)) return false;
|
||||
} else if (!std::isdigit(character) && character != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return has_letter && word.size() > 1;
|
||||
}
|
||||
|
||||
[[maybe_unused]] void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
SyntaxLanguage language, SyntaxState& state) {
|
||||
constexpr ImU32 normal = IM_COL32(218, 221, 226, 255);
|
||||
constexpr ImU32 keyword = IM_COL32(198, 139, 230, 255);
|
||||
@@ -118,6 +162,12 @@ void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
constexpr ImU32 comment = IM_COL32(112, 153, 105, 255);
|
||||
constexpr ImU32 function = IM_COL32(220, 199, 128, 255);
|
||||
constexpr ImU32 preprocessor = IM_COL32(205, 157, 222, 255);
|
||||
constexpr ImU32 member = IM_COL32(122, 184, 225, 255);
|
||||
constexpr ImU32 builtin = IM_COL32(103, 172, 232, 255);
|
||||
constexpr ImU32 constant = IM_COL32(214, 139, 102, 255);
|
||||
constexpr ImU32 operator_color = IM_COL32(105, 180, 210, 255);
|
||||
constexpr ImU32 decorator = IM_COL32(226, 190, 105, 255);
|
||||
constexpr ImU32 macro = IM_COL32(215, 128, 180, 255);
|
||||
|
||||
const auto drawSpan = [&](size_t begin, size_t end, ImU32 color) {
|
||||
if (end <= begin) return;
|
||||
@@ -140,6 +190,7 @@ void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
}
|
||||
|
||||
size_t cursor = 0;
|
||||
std::string_view previous_word;
|
||||
while (cursor < text.size()) {
|
||||
if (state.continuation == SyntaxContinuation::block_comment) {
|
||||
const size_t end = text.find("*/", cursor);
|
||||
@@ -249,15 +300,49 @@ void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if (text[cursor] == '@' && cursor + 1 < text.size() &&
|
||||
(std::isalpha(static_cast<unsigned char>(text[cursor + 1])) || text[cursor + 1] == '_')) {
|
||||
size_t end = cursor + 2;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) ||
|
||||
text[end] == '_' || text[end] == '.')) ++end;
|
||||
drawSpan(cursor, end, decorator);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if (std::isalpha(static_cast<unsigned char>(text[cursor])) || text[cursor] == '_') {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() && (std::isalnum(static_cast<unsigned char>(text[end])) || text[end] == '_')) ++end;
|
||||
const std::string_view word(text.data() + cursor, end - cursor);
|
||||
size_t next = end;
|
||||
while (next < text.size() && std::isspace(static_cast<unsigned char>(text[next]))) ++next;
|
||||
const ImU32 color = isKeyword(language, word) ? keyword : isTypeWord(language, word) ? type :
|
||||
size_t previous = cursor;
|
||||
while (previous > 0 && std::isspace(static_cast<unsigned char>(text[previous - 1]))) --previous;
|
||||
const bool member_access = previous > 0 && (text[previous - 1] == '.' ||
|
||||
(text[previous - 1] == '>' && previous > 1 && text[previous - 2] == '-') ||
|
||||
(text[previous - 1] == ':' && previous > 1 && text[previous - 2] == ':'));
|
||||
const bool declared_name = isDeclarationKeyword(previous_word);
|
||||
const bool declared_function = isFunctionDeclarationKeyword(previous_word);
|
||||
const bool capitalized_type =
|
||||
(language == SyntaxLanguage::cpp || language == SyntaxLanguage::csharp) &&
|
||||
std::isupper(static_cast<unsigned char>(word.front())) && !member_access &&
|
||||
(next >= text.size() || text[next] != '(');
|
||||
const bool likely_type = declared_name || isTypeWord(language, word) ||
|
||||
capitalized_type;
|
||||
const ImU32 color = isLiteral(word) ? constant : isKeyword(language, word) ? keyword :
|
||||
declared_function ? function : declared_name || likely_type ? type :
|
||||
isMacroName(word) ? macro : isBuiltin(language, word) ? builtin : member_access ?
|
||||
(next < text.size() && text[next] == '(' ? function : member) :
|
||||
next < text.size() && text[next] == '(' ? function : normal;
|
||||
drawSpan(cursor, end, color);
|
||||
previous_word = word;
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if (std::string_view("+-*/%=!<>&|^~?:").find(text[cursor]) != std::string_view::npos) {
|
||||
size_t end = cursor + 1;
|
||||
while (end < text.size() &&
|
||||
std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos) ++end;
|
||||
drawSpan(cursor, end, operator_color);
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
@@ -267,7 +352,8 @@ void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
const bool token_start = std::isalnum(value) || text[end] == '_' || text[end] == '\'' ||
|
||||
text[end] == '"' || (language == SyntaxLanguage::javascript && text[end] == '`') ||
|
||||
(slash_comments && text[end] == '/') || (language == SyntaxLanguage::python && text[end] == '#') ||
|
||||
(language == SyntaxLanguage::lua && text[end] == '-');
|
||||
(language == SyntaxLanguage::lua && text[end] == '-') || text[end] == '@' ||
|
||||
std::string_view("+-*/%=!<>&|^~?:").find(text[end]) != std::string_view::npos;
|
||||
if (token_start) break;
|
||||
++end;
|
||||
}
|
||||
@@ -278,10 +364,77 @@ void drawSyntaxText(ImDrawList* draw, ImVec2 position, const std::string& text,
|
||||
|
||||
bool compactButton(const char* label, bool active = false) {
|
||||
if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.25f, 0.43f, 1.0f));
|
||||
const bool clicked = ImGui::SmallButton(label);
|
||||
const bool icon_only = label && std::strlen(label) <= 4;
|
||||
const bool clicked = icon_only
|
||||
? ImGui::Button(label, {ImGui::GetFrameHeight(), ImGui::GetFrameHeight()})
|
||||
: ImGui::SmallButton(label);
|
||||
if (active) ImGui::PopStyleColor();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
std::string joinLines(const std::vector<std::string>& lines) {
|
||||
std::string text;
|
||||
for (size_t index = 0; index < lines.size(); ++index) {
|
||||
if (index) text += '\n';
|
||||
text += lines[index];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
bool drawSelectableTextBlock(const char* id, const std::string& text, const ImVec2& size) {
|
||||
std::vector<char> buffer(text.begin(), text.end());
|
||||
buffer.push_back('\0');
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.06f, 0.07f, 0.09f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.18f, 0.20f, 0.24f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {4.0f, 4.0f});
|
||||
const bool changed = ImGui::InputTextMultiline(id, buffer.data(), buffer.size(), size,
|
||||
ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_AllowTabInput);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(2);
|
||||
return changed;
|
||||
}
|
||||
|
||||
struct MinimapEntry {
|
||||
size_t length = 0;
|
||||
ImU32 color = IM_COL32(120, 126, 136, 255);
|
||||
};
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, float scale,
|
||||
float visible_ratio, float scroll_ratio) {
|
||||
const ImVec2 minimum = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 size = ImGui::GetContentRegionAvail();
|
||||
ImGui::InvisibleButton("##code_minimap", size);
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
draw->AddRectFilled(minimum, {minimum.x + size.x, minimum.y + size.y}, IM_COL32(38, 40, 47, 255));
|
||||
draw->AddRectFilled({minimum.x + size.x - scaled(10.0f, scale), minimum.y},
|
||||
{minimum.x + size.x, minimum.y + size.y}, IM_COL32(64, 66, 74, 255));
|
||||
|
||||
if (entries.empty() || size.y <= 2.0f) return;
|
||||
|
||||
const float content_left = minimum.x + scaled(4.0f, scale);
|
||||
const float content_right = minimum.x + size.x - scaled(14.0f, scale);
|
||||
const float content_width = std::max(1.0f, content_right - content_left);
|
||||
const float line_step = size.y / static_cast<float>(entries.size());
|
||||
const float line_height = std::max(1.0f, std::min(scaled(2.0f, scale), line_step));
|
||||
constexpr float max_reference_length = 160.0f;
|
||||
for (size_t index = 0; index < entries.size(); ++index) {
|
||||
const float normalized = std::clamp(static_cast<float>(entries[index].length) / max_reference_length, 0.08f, 1.0f);
|
||||
const float width = content_width * normalized;
|
||||
const float y = minimum.y + index * line_step;
|
||||
draw->AddRectFilled({content_left, y}, {content_left + width, y + line_height},
|
||||
entries[index].color, scaled(1.0f, scale));
|
||||
}
|
||||
|
||||
const float viewport_height = std::clamp(size.y * visible_ratio, scaled(18.0f, scale), size.y);
|
||||
const float viewport_y = minimum.y + (size.y - viewport_height) * std::clamp(scroll_ratio, 0.0f, 1.0f);
|
||||
draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(11.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 45), scaled(2.0f, scale));
|
||||
draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(11.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 160), scaled(2.0f, scale));
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::open(RepositoryView& repository, GitManager& manager, const std::string& path,
|
||||
@@ -378,6 +531,11 @@ void DiffViewer::parseBlame(const std::string& text) {
|
||||
const std::string_view key(field.data(), separator == std::string::npos ? field.size() : separator);
|
||||
const std::string value = separator == std::string::npos ? std::string{} : field.substr(separator + 1);
|
||||
if (key == "author") line.author = value;
|
||||
else if (key == "author-mail") {
|
||||
line.email = value;
|
||||
if (line.email.size() >= 2 && line.email.front() == '<' && line.email.back() == '>')
|
||||
line.email = line.email.substr(1, line.email.size() - 2);
|
||||
}
|
||||
else if (key == "author-time") {
|
||||
try { author_time = static_cast<std::time_t>(std::stoll(value)); }
|
||||
catch (...) { author_time = 0; }
|
||||
@@ -474,11 +632,13 @@ void DiffViewer::loadSupplement(RepositoryView& repository, GitManager& manager,
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice) {
|
||||
void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
|
||||
float scale, ImFont* code_font, std::string& notice) {
|
||||
(void)avatars;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {scaled(8, scale), scaled(5, scale)});
|
||||
ImGui::BeginChild("diff_viewer", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_FA_PEN);
|
||||
ImGui::TextColored(ImVec4(0.94f, 0.66f, 0.25f, 1), ICON_TB_PEN);
|
||||
ImGui::SameLine(0, scaled(7, scale));
|
||||
ImGui::TextUnformatted(path_.c_str());
|
||||
|
||||
@@ -512,7 +672,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (compactButton(ICON_FA_XMARK)) close();
|
||||
if (compactButton(ICON_TB_XMARK)) close();
|
||||
ImGui::Separator();
|
||||
if (!path_.empty()) {
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - scaled(116, scale));
|
||||
@@ -520,7 +680,7 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
if (!historical && !staged_) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.94f, 0.44f, 0.44f, 1));
|
||||
if (compactButton(ICON_FA_TRASH_CAN)) {
|
||||
if (compactButton(ICON_TB_TRASH_CAN)) {
|
||||
if (manager.discardFile(repository, path_, notice)) close();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
@@ -528,37 +688,37 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::BeginChild("diff_content", {-1, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
const bool show_minimap = mode_ == Mode::diff || mode_ == Mode::file;
|
||||
const float minimap_width = scaled(56.0f, scale);
|
||||
float main_scroll_y = 0.0f;
|
||||
float main_scroll_max_y = 0.0f;
|
||||
float main_window_height = 0.0f;
|
||||
std::vector<MinimapEntry> minimap_entries;
|
||||
if (show_minimap) {
|
||||
if (mode_ == Mode::diff) {
|
||||
for (size_t hunk_index = 0; hunk_index < hunks_.size(); ++hunk_index) {
|
||||
minimap_entries.push_back({hunks_[hunk_index].header.size(), IM_COL32(128, 133, 141, 255)});
|
||||
for (const Line& line : hunks_[hunk_index].lines) {
|
||||
const ImU32 color = line.kind == LineKind::added ? IM_COL32(87, 190, 112, 255) :
|
||||
line.kind == LineKind::removed ? IM_COL32(220, 97, 97, 255) :
|
||||
IM_COL32(112, 118, 128, 255);
|
||||
minimap_entries.push_back({line.text.size() + 1, color});
|
||||
}
|
||||
minimap_entries.push_back({0, IM_COL32(0, 0, 0, 0)});
|
||||
}
|
||||
} else {
|
||||
minimap_entries.reserve(file_lines_.size());
|
||||
for (const std::string& line : file_lines_)
|
||||
minimap_entries.push_back({line.size(), IM_COL32(112, 118, 128, 255)});
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginChild("diff_content_main",
|
||||
show_minimap ? ImVec2{-minimap_width - scaled(6.0f, scale), -1} : ImVec2{-1, -1},
|
||||
ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
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);
|
||||
const float number_width = scaled(48, scale);
|
||||
const SyntaxLanguage language = languageForPath(path_);
|
||||
auto draw_line = [&](const std::string& text, int old_number, int new_number, LineKind kind,
|
||||
SyntaxLanguage line_language, SyntaxState& syntax_state) {
|
||||
ImGui::InvisibleButton("##line", {std::max(ImGui::GetContentRegionAvail().x, scaled(900, scale)), row_height});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
if (kind == LineKind::added) draw->AddRectFilled(minimum, maximum, IM_COL32(31, 65, 43, 225));
|
||||
else if (kind == LineKind::removed) draw->AddRectFilled(minimum, maximum, IM_COL32(70, 38, 40, 225));
|
||||
draw->AddRectFilled(minimum, {minimum.x + number_width * 2, maximum.y}, IM_COL32(31, 34, 40, 255));
|
||||
char old_buffer[16]{};
|
||||
char new_buffer[16]{};
|
||||
if (old_number) std::snprintf(old_buffer, sizeof(old_buffer), "%d", old_number);
|
||||
if (new_number) std::snprintf(new_buffer, sizeof(new_buffer), "%d", new_number);
|
||||
draw->AddText({minimum.x + scaled(5, scale), minimum.y + scaled(2, scale)},
|
||||
IM_COL32(158, 164, 174, 255), old_buffer);
|
||||
draw->AddText({minimum.x + number_width + scaled(5, scale), minimum.y + scaled(2, scale)},
|
||||
IM_COL32(158, 164, 174, 255), new_buffer);
|
||||
const char marker = kind == LineKind::added ? '+' : kind == LineKind::removed ? '-' : ' ';
|
||||
char marker_text[2]{marker, 0};
|
||||
draw->AddText({minimum.x + number_width * 2 + scaled(5, scale), minimum.y + scaled(2, scale)},
|
||||
kind == LineKind::added ? IM_COL32(87, 190, 112, 255) :
|
||||
kind == LineKind::removed ? IM_COL32(220, 97, 97, 255) : IM_COL32(148, 154, 164, 255), marker_text);
|
||||
drawSyntaxText(draw,
|
||||
{minimum.x + number_width * 2 + scaled(22, scale), minimum.y + scaled(2, scale)},
|
||||
text, line_language, syntax_state);
|
||||
};
|
||||
|
||||
// Keep the first source row clear of the toolbar and aligned with a normal row inset.
|
||||
ImGui::Dummy({0.0f, row_height});
|
||||
@@ -573,7 +733,12 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
SyntaxState old_syntax;
|
||||
SyntaxState new_syntax;
|
||||
ImGui::PushID(static_cast<int>(hunk_index));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.56f, 0.70f, 0.90f, 1));
|
||||
if (hunk_index > 0) {
|
||||
ImGui::Dummy({0.0f, scaled(11.0f, scale)});
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy({0.0f, scaled(8.0f, scale)});
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.62f, 0.64f, 0.68f, 1.0f));
|
||||
ImGui::TextUnformatted(hunks_[hunk_index].header.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine(std::max(scaled(220, scale), ImGui::GetWindowWidth() - scaled(205, scale)));
|
||||
@@ -595,13 +760,17 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
pending_hunk = static_cast<int>(hunk_index);
|
||||
pending_action = HunkAction::unstage;
|
||||
}
|
||||
for (const Line& line : hunks_[hunk_index].lines) {
|
||||
ImGui::PushID(line_id++);
|
||||
SyntaxState& syntax_state = line.kind == LineKind::removed ? old_syntax : new_syntax;
|
||||
draw_line(line.text, line.old_number, line.new_number, line.kind, language, syntax_state);
|
||||
if (line.kind == LineKind::context) old_syntax = new_syntax;
|
||||
ImGui::PopID();
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy({0.0f, scaled(4.0f, scale)});
|
||||
std::string hunk_text;
|
||||
for (size_t line_index = 0; line_index < hunks_[hunk_index].lines.size(); ++line_index) {
|
||||
if (line_index) hunk_text += '\n';
|
||||
hunk_text += hunks_[hunk_index].lines[line_index].raw;
|
||||
}
|
||||
const float hunk_height = std::max(row_height * 3.0f,
|
||||
row_height * static_cast<float>(hunks_[hunk_index].lines.size()) + scaled(10.0f, scale));
|
||||
drawSelectableTextBlock(("##diff_hunk_text" + std::to_string(line_id++)).c_str(),
|
||||
hunk_text, {-1, hunk_height});
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (pending_hunk >= 0 && pending_hunk < static_cast<int>(hunks_.size())) {
|
||||
@@ -611,71 +780,43 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, float sca
|
||||
reload(repository, manager, notice);
|
||||
}
|
||||
} else if (mode_ == Mode::blame) {
|
||||
SyntaxState syntax_state;
|
||||
const float blame_number_width = scaled(42.0f, scale);
|
||||
const float attribution_width = scaled(285.0f, scale);
|
||||
std::ostringstream blame_text;
|
||||
for (size_t index = 0; index < blame_lines_.size(); ++index) {
|
||||
const BlameLine& line = blame_lines_[index];
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
ImGui::InvisibleButton("##blame_line",
|
||||
{std::max(ImGui::GetContentRegionAvail().x, scaled(900.0f, scale)), row_height});
|
||||
const ImVec2 minimum = ImGui::GetItemRectMin();
|
||||
const ImVec2 maximum = ImGui::GetItemRectMax();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = blameColor(line.hash);
|
||||
draw->AddRectFilled(minimum,
|
||||
{minimum.x + blame_number_width + attribution_width, maximum.y}, blameColor(line.hash, 18));
|
||||
draw->AddRectFilled(
|
||||
{minimum.x + blame_number_width + attribution_width - scaled(2.0f, scale), minimum.y},
|
||||
{minimum.x + blame_number_width + attribution_width, maximum.y}, color);
|
||||
|
||||
char line_number[16]{};
|
||||
std::snprintf(line_number, sizeof(line_number), "%d", line.line_number);
|
||||
draw->AddText({minimum.x + scaled(6.0f, scale), minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(151, 158, 169, 255), line_number);
|
||||
|
||||
if (index) blame_text << '\n';
|
||||
blame_text << line.line_number << " ";
|
||||
if (line.show_attribution) {
|
||||
const float attribution_left = minimum.x + blame_number_width + scaled(7.0f, scale);
|
||||
const float attribution_right = minimum.x + blame_number_width + attribution_width - scaled(8.0f, scale);
|
||||
draw->AddCircleFilled(
|
||||
{attribution_left + scaled(4.0f, scale), minimum.y + row_height * 0.5f},
|
||||
scaled(3.5f, scale), color);
|
||||
const float date_width = ImGui::CalcTextSize(line.date.c_str()).x;
|
||||
draw->PushClipRect(
|
||||
{attribution_left + scaled(14.0f, scale), minimum.y},
|
||||
{attribution_right - date_width - scaled(9.0f, scale), maximum.y}, true);
|
||||
draw->AddText({attribution_left + scaled(14.0f, scale), minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(205, 211, 220, 255), line.summary.c_str());
|
||||
draw->PopClipRect();
|
||||
draw->AddText({attribution_right - date_width, minimum.y + scaled(2.0f, scale)},
|
||||
IM_COL32(139, 147, 159, 255), line.date.c_str());
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) &&
|
||||
ImGui::GetIO().MousePos.x < minimum.x + blame_number_width + attribution_width) {
|
||||
ImGui::SetTooltip("%s\n%s\n%s %s",
|
||||
line.summary.c_str(), line.author.c_str(), line.hash.substr(0, 10).c_str(), line.date.c_str());
|
||||
blame_text << line.summary;
|
||||
if (!line.date.empty()) blame_text << " " << line.date;
|
||||
blame_text << " ";
|
||||
} else {
|
||||
blame_text << "| ";
|
||||
}
|
||||
blame_text << line.text;
|
||||
}
|
||||
drawSyntaxText(draw,
|
||||
{minimum.x + blame_number_width + attribution_width + scaled(10.0f, scale),
|
||||
minimum.y + scaled(2.0f, scale)},
|
||||
line.text, language, syntax_state);
|
||||
ImGui::PopID();
|
||||
}
|
||||
drawSelectableTextBlock("##blame_text", blame_text.str(), {-1, -1});
|
||||
if (blame_lines_.empty()) ImGui::TextDisabled("No blame data is available for this file.");
|
||||
} else {
|
||||
const std::vector<std::string>* lines = mode_ == Mode::file ? &file_lines_ : &history_lines_;
|
||||
SyntaxState syntax_state;
|
||||
const SyntaxLanguage line_language = mode_ == Mode::file ? language : SyntaxLanguage::plain;
|
||||
for (size_t index = 0; index < lines->size(); ++index) {
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
draw_line((*lines)[index], mode_ == Mode::file ? static_cast<int>(index + 1) : 0,
|
||||
mode_ == Mode::file ? static_cast<int>(index + 1) : 0, LineKind::context,
|
||||
line_language, syntax_state);
|
||||
ImGui::PopID();
|
||||
}
|
||||
drawSelectableTextBlock(mode_ == Mode::file ? "##file_text" : "##history_text",
|
||||
joinLines(*lines), {-1, -1});
|
||||
if (lines->empty()) ImGui::TextDisabled("No data is available for this view.");
|
||||
}
|
||||
main_scroll_y = ImGui::GetScrollY();
|
||||
main_scroll_max_y = ImGui::GetScrollMaxY();
|
||||
main_window_height = ImGui::GetWindowHeight();
|
||||
if (use_code_font) ImGui::PopFont();
|
||||
ImGui::EndChild();
|
||||
if (show_minimap) {
|
||||
ImGui::SameLine(0, scaled(6.0f, scale));
|
||||
ImGui::BeginChild("diff_content_minimap", {minimap_width, -1}, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
const float visible_ratio = main_scroll_max_y <= 0.0f
|
||||
? 1.0f : std::clamp(main_window_height / (main_window_height + main_scroll_max_y), 0.0f, 1.0f);
|
||||
const float scroll_ratio = main_scroll_max_y <= 0.0f ? 0.0f : main_scroll_y / main_scroll_max_y;
|
||||
drawMinimap(minimap_entries, scale, visible_ratio, scroll_ratio);
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <vector>
|
||||
|
||||
class GitManager;
|
||||
class AvatarCache;
|
||||
struct RepositoryView;
|
||||
struct ImFont;
|
||||
|
||||
class DiffViewer {
|
||||
public:
|
||||
@@ -14,7 +16,8 @@ public:
|
||||
const std::string& commit_id, std::string& notice);
|
||||
void close();
|
||||
bool isOpen() const { return !path_.empty(); }
|
||||
void draw(RepositoryView& repository, GitManager& manager, float scale, std::string& notice);
|
||||
void draw(RepositoryView& repository, GitManager& manager, AvatarCache* avatars,
|
||||
float scale, ImFont* code_font, std::string& notice);
|
||||
|
||||
private:
|
||||
enum class Mode { diff, file, blame, history };
|
||||
@@ -34,6 +37,7 @@ private:
|
||||
struct BlameLine {
|
||||
std::string hash;
|
||||
std::string author;
|
||||
std::string email;
|
||||
std::string date;
|
||||
std::string summary;
|
||||
std::string text;
|
||||
|
||||
1513
src/ui/gitree_ui.cpp
1513
src/ui/gitree_ui.cpp
File diff suppressed because it is too large
Load Diff
@@ -2,42 +2,95 @@
|
||||
|
||||
#include "managers/avatar_cache.h"
|
||||
#include "models/repository.h"
|
||||
#include <IconsFontAwesome6.h>
|
||||
#include <IconsTabler.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
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) {
|
||||
template <size_t Count>
|
||||
void drawRoundedPolyline(ImDrawList* draw, const std::array<ImVec2, Count>& points,
|
||||
float radius, ImU32 color, float thickness) {
|
||||
static_assert(Count >= 2);
|
||||
draw->PathClear();
|
||||
draw->PathLineTo(child);
|
||||
draw->PathLineTo(points.front());
|
||||
for (size_t index = 1; index + 1 < Count; ++index) {
|
||||
const ImVec2 previous = points[index - 1];
|
||||
const ImVec2 corner = points[index];
|
||||
const ImVec2 next = points[index + 1];
|
||||
const float incoming_length = std::hypot(corner.x - previous.x, corner.y - previous.y);
|
||||
const float outgoing_length = std::hypot(next.x - corner.x, next.y - corner.y);
|
||||
if (incoming_length < 0.01f || outgoing_length < 0.01f) continue;
|
||||
const float turn_radius = std::min({radius, incoming_length * 0.5f, outgoing_length * 0.5f});
|
||||
const ImVec2 incoming{
|
||||
(corner.x - previous.x) / incoming_length,
|
||||
(corner.y - previous.y) / incoming_length,
|
||||
};
|
||||
const ImVec2 outgoing{
|
||||
(next.x - corner.x) / outgoing_length,
|
||||
(next.y - corner.y) / outgoing_length,
|
||||
};
|
||||
const ImVec2 before{
|
||||
corner.x - incoming.x * turn_radius,
|
||||
corner.y - incoming.y * turn_radius,
|
||||
};
|
||||
const ImVec2 after{
|
||||
corner.x + outgoing.x * turn_radius,
|
||||
corner.y + outgoing.y * turn_radius,
|
||||
};
|
||||
constexpr float control = 0.55228475f;
|
||||
draw->PathLineTo(before);
|
||||
draw->PathBezierCubicCurveTo(
|
||||
{before.x + incoming.x * turn_radius * control,
|
||||
before.y + incoming.y * turn_radius * control},
|
||||
{after.x - outgoing.x * turn_radius * control,
|
||||
after.y - outgoing.y * turn_radius * control},
|
||||
after);
|
||||
}
|
||||
draw->PathLineTo(points.back());
|
||||
draw->PathStroke(color, ImDrawFlags_None, thickness);
|
||||
}
|
||||
|
||||
void drawOrthogonalEdge(ImDrawList* draw, const ImVec2& child, const ImVec2& parent,
|
||||
ImU32 color, float scale, int route_slot, float detour_x) {
|
||||
const float thickness = 2.0f * scale;
|
||||
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);
|
||||
if (vertical <= 0.0f) {
|
||||
draw->AddLine(child, parent, color, 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);
|
||||
if (std::isfinite(detour_x)) {
|
||||
if (std::abs(detour_x - child.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, parent.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
} else if (std::abs(detour_x - parent.x) < 0.5f * scale) {
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{detour_x, child.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
} else {
|
||||
draw->PathLineTo({parent.x, child.y});
|
||||
draw->PathLineTo(parent);
|
||||
drawRoundedPolyline(draw,
|
||||
std::array{child, ImVec2{detour_x, child.y}, ImVec2{detour_x, parent.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
}
|
||||
return;
|
||||
}
|
||||
draw->PathStroke(stroke_color, stroke_thickness);
|
||||
};
|
||||
|
||||
stroke(IM_COL32(17, 21, 27, 255), thickness + 2.4f * scale);
|
||||
stroke(color, thickness);
|
||||
if (std::abs(horizontal) < 0.5f * scale) {
|
||||
draw->AddLine(child, parent, color, thickness);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bends remain anchored to commit rows, but use a compact quarter-round turn.
|
||||
if (route_slot == 0)
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{child.x, parent.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
else
|
||||
drawRoundedPolyline(draw, std::array{child, ImVec2{parent.x, child.y}, parent},
|
||||
7.0f * scale, color, thickness);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -60,7 +113,8 @@ ImU32 GraphRenderer::laneColor(int lane, int alpha) {
|
||||
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);
|
||||
// Keep one spare lane available for edges that must route around occupied commit lanes.
|
||||
return std::max((68.0f + maximum_lane * 22.0f) * scale, 82.0f * scale);
|
||||
}
|
||||
|
||||
void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
@@ -86,20 +140,18 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
{ribbon_left, ribbon_top},
|
||||
{cell_right, ribbon_bottom},
|
||||
laneColor(commit.graph_color, 38));
|
||||
draw->AddLine(
|
||||
{cell_right - px(1.0f), ribbon_top},
|
||||
{cell_right - px(1.0f), ribbon_bottom},
|
||||
laneColor(commit.graph_color, 215), px(2.0f));
|
||||
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));
|
||||
drawRoundedPolyline(draw,
|
||||
std::array{*ref_connector_start, ImVec2{turn_x, ref_connector_start->y},
|
||||
ImVec2{turn_x, end.y}, end},
|
||||
px(5.0f), laneColor(commit.graph_color, 190), px(1.8f));
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
@@ -131,11 +183,44 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
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;
|
||||
const int preferred_lane = parent_index == 0 ? child.lane : parent.lane;
|
||||
const auto lane_is_blocked = [&](int candidate_lane) {
|
||||
for (int intermediate = child_row + 1; intermediate < parent_row; ++intermediate) {
|
||||
if (row_heights[static_cast<size_t>(intermediate)] > 0.0f &&
|
||||
commits[static_cast<size_t>(intermediate)].lane == candidate_lane)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
float detour_x = std::numeric_limits<float>::quiet_NaN();
|
||||
if (lane_is_blocked(preferred_lane)) {
|
||||
int maximum_lane = 0;
|
||||
for (int intermediate = child_row; intermediate <= parent_row; ++intermediate) {
|
||||
if (row_heights[static_cast<size_t>(intermediate)] > 0.0f)
|
||||
maximum_lane = std::max(maximum_lane,
|
||||
commits[static_cast<size_t>(intermediate)].lane);
|
||||
}
|
||||
int detour_lane = maximum_lane + 1;
|
||||
for (int distance = 1; distance <= maximum_lane + 1; ++distance) {
|
||||
const int left = preferred_lane - distance;
|
||||
const int right = preferred_lane + distance;
|
||||
if (left >= 0 && !lane_is_blocked(left)) {
|
||||
detour_lane = left;
|
||||
break;
|
||||
}
|
||||
if (right <= maximum_lane && !lane_is_blocked(right)) {
|
||||
detour_lane = right;
|
||||
break;
|
||||
}
|
||||
}
|
||||
detour_x = origin.x + px(17.0f) + lane_spacing * detour_lane;
|
||||
}
|
||||
// Colors are lane-position based, so edges use the visible lane slot they route through.
|
||||
const int edge_color = std::isfinite(detour_x)
|
||||
? static_cast<int>(std::lround((detour_x - (origin.x + px(17.0f))) / lane_spacing))
|
||||
: preferred_lane;
|
||||
drawOrthogonalEdge(draw, {child_x, child_y}, {parent_x, parent_y},
|
||||
laneColor(edge_color, 235), scale_, static_cast<int>(parent_index));
|
||||
laneColor(edge_color, 235), scale_, static_cast<int>(parent_index), detour_x);
|
||||
}
|
||||
}
|
||||
draw->PopClipRect();
|
||||
@@ -171,8 +256,8 @@ void GraphRenderer::drawRow(int row, const CommitInfo& commit,
|
||||
{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);
|
||||
const ImVec2 icon_size = ImGui::CalcTextSize(ICON_TB_USER);
|
||||
draw->AddText({x - icon_size.x * 0.5f, y - icon_size.y * 0.5f}, lane_color, ICON_TB_USER);
|
||||
}
|
||||
show_identity_tooltip(radius + px(2.0f));
|
||||
ImGui::Dummy({0.0f, content_height});
|
||||
|
||||
BIN
vendor/fonts/Inter-Regular.ttf
vendored
Normal file
BIN
vendor/fonts/Inter-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
vendor/fonts/Inter-SemiBold.ttf
vendored
Normal file
BIN
vendor/fonts/Inter-SemiBold.ttf
vendored
Normal file
Binary file not shown.
BIN
vendor/fonts/tabler-icons-outline.ttf
vendored
BIN
vendor/fonts/tabler-icons-outline.ttf
vendored
Binary file not shown.
59
vendor/icons/IconsTabler.h
vendored
Normal file
59
vendor/icons/IconsTabler.h
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// Tabler Icons 3.44.0 outline webfont glyphs used by Gitree.
|
||||
#define ICON_MIN_TB 0xEA06
|
||||
#define ICON_MAX_TB 0xFAF7
|
||||
|
||||
#define ICON_TB_ANGLE_RIGHT "\xee\xa9\xa1"
|
||||
#define ICON_TB_ARROW_DOWN "\xee\xa8\x96"
|
||||
#define ICON_TB_ARROW_DOWN_A_Z "\xee\xbc\x98"
|
||||
#define ICON_TB_ARROW_DOWN_Z_A "\xee\xbc\x9a"
|
||||
#define ICON_TB_ARROW_RIGHT_ARROW_LEFT "\xef\x87\xb4"
|
||||
#define ICON_TB_ARROW_UP "\xee\xa8\xa5"
|
||||
#define ICON_TB_ARROW_UP_RIGHT_FROM_SQUARE "\xee\xaa\x99"
|
||||
#define ICON_TB_BARS "\xee\xb1\x82"
|
||||
#define ICON_TB_BOX_ARCHIVE "\xee\xa8\x8b"
|
||||
#define ICON_TB_BOX_OPEN "\xef\x81\xba"
|
||||
#define ICON_TB_CARET_DOWN "\xee\xad\x9d"
|
||||
#define ICON_TB_CHECK "\xee\xa9\x9e"
|
||||
#define ICON_TB_CHEVRON_DOWN "\xee\xa9\x9f"
|
||||
#define ICON_TB_CHEVRON_RIGHT "\xee\xa9\xa1"
|
||||
#define ICON_TB_CIRCLE_CHEVRON_LEFT "\xef\x98\xa3"
|
||||
#define ICON_TB_CIRCLE_CHEVRON_RIGHT "\xef\x98\xa4"
|
||||
#define ICON_TB_CIRCLE_DOT "\xee\xbe\xb1"
|
||||
#define ICON_TB_CIRCLE_NODES "\xee\xaa\xb5"
|
||||
#define ICON_TB_CLOCK_ROTATE_LEFT "\xee\xaf\xaa"
|
||||
#define ICON_TB_CLOUD "\xee\xa9\xb6"
|
||||
#define ICON_TB_CODE "\xee\xa9\xb7"
|
||||
#define ICON_TB_CODE_BRANCH "\xee\xaa\xb2"
|
||||
#define ICON_TB_COMPUTER "\xee\xaa\x89"
|
||||
#define ICON_TB_COPY "\xee\xa9\xba"
|
||||
#define ICON_TB_CUBES "\xee\xb8\x97"
|
||||
#define ICON_TB_DESKTOP "\xee\xaa\x89"
|
||||
#define ICON_TB_DEVICE_LAPTOP "\xee\xad\xa4"
|
||||
#define ICON_TB_DOWNLOAD "\xee\xaa\x96"
|
||||
#define ICON_TB_EYE "\xee\xaa\x9a"
|
||||
#define ICON_TB_FILE "\xee\xaa\xa4"
|
||||
#define ICON_TB_FOLDER "\xee\xaa\xad"
|
||||
#define ICON_TB_FOLDER_OPEN "\xef\xab\xb7"
|
||||
#define ICON_TB_FOLDER_TREE "\xee\xba\x9d"
|
||||
#define ICON_TB_GLOBE "\xee\xad\x94"
|
||||
#define ICON_TB_JET_FIGHTER_UP "\xef\x87\xad"
|
||||
#define ICON_TB_LAYERS_LINKED "\xee\xba\xa1"
|
||||
#define ICON_TB_MAGNIFYING_GLASS "\xee\xac\x9c"
|
||||
#define ICON_TB_MINUS "\xee\xab\xb2"
|
||||
#define ICON_TB_PEN "\xee\xac\x84"
|
||||
#define ICON_TB_PLUS "\xee\xac\x8b"
|
||||
#define ICON_TB_ROBOT "\xef\x80\x8b"
|
||||
#define ICON_TB_ROTATE_LEFT "\xee\xac\x96"
|
||||
#define ICON_TB_ROTATE_RIGHT "\xee\xac\x95"
|
||||
#define ICON_TB_SERVER "\xee\xac\x9f"
|
||||
#define ICON_TB_TAG "\xee\xbe\x86"
|
||||
#define ICON_TB_TERMINAL "\xee\xaf\xaf"
|
||||
#define ICON_TB_TRASH_CAN "\xee\xad\x81"
|
||||
#define ICON_TB_TREE "\xee\xbc\x81"
|
||||
#define ICON_TB_TRIANGLE_EXCLAMATION "\xee\xa8\x86"
|
||||
#define ICON_TB_UPLOAD "\xee\xad\x87"
|
||||
#define ICON_TB_USER "\xee\xad\x8d"
|
||||
#define ICON_TB_WINDOW_MAXIMIZE "\xee\xab\xaa"
|
||||
#define ICON_TB_XMARK "\xee\xad\x95"
|
||||
Reference in New Issue
Block a user