refactor(core): separate managers models and UI
This commit is contained in:
@@ -50,14 +50,19 @@ find_package(OpenGL REQUIRED)
|
||||
|
||||
add_executable(gitree WIN32
|
||||
src/main.cpp
|
||||
src/window_manager.cpp
|
||||
src/window_manager.h
|
||||
src/user_data.cpp
|
||||
src/user_data.h
|
||||
src/avatar_cache.cpp
|
||||
src/avatar_cache.h
|
||||
src/ui/gitree_ui.cpp
|
||||
src/ui/gitree_ui.h
|
||||
src/managers/git_manager.cpp
|
||||
src/managers/git_manager.h
|
||||
src/managers/window_manager.cpp
|
||||
src/managers/window_manager.h
|
||||
src/managers/user_data.cpp
|
||||
src/managers/user_data.h
|
||||
src/managers/avatar_cache.cpp
|
||||
src/managers/avatar_cache.h
|
||||
src/models/repository.h
|
||||
)
|
||||
target_include_directories(gitree PRIVATE vendor/libgit2/include vendor/icons)
|
||||
target_include_directories(gitree PRIVATE src vendor/libgit2/include vendor/icons)
|
||||
target_link_libraries(gitree PRIVATE imgui libgit2package iZo::izo OpenGL::GL)
|
||||
target_compile_definitions(gitree PRIVATE
|
||||
GITREE_VERSION="${PROJECT_VERSION}"
|
||||
|
||||
@@ -33,3 +33,10 @@ User settings, ImGui layout state, and recently closed repository history are st
|
||||
`%APPDATA%\Identity\Gitree` on Windows. The directory is created automatically.
|
||||
Commit avatars are resolved from normalized author emails through Gravatar and cached in
|
||||
the `avatars` subdirectory; an identicon is used when an account has no custom image.
|
||||
|
||||
## Source layout
|
||||
|
||||
- `src/managers/` owns Git, window, user-data, and avatar services.
|
||||
- `src/models/` contains repository and commit data models.
|
||||
- `src/ui/` contains the Dear ImGui application interface.
|
||||
- `src/main.cpp` is the minimal process entry point.
|
||||
|
||||
1015
src/main.cpp
1015
src/main.cpp
File diff suppressed because it is too large
Load Diff
214
src/managers/git_manager.cpp
Normal file
214
src/managers/git_manager.cpp
Normal file
@@ -0,0 +1,214 @@
|
||||
#include "managers/git_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
|
||||
namespace {
|
||||
void addBadge(RepositoryView& repository, const git_oid* oid, RefBadge badge) {
|
||||
if (!oid) return;
|
||||
const auto commit = std::find_if(repository.commits.begin(), repository.commits.end(),
|
||||
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
|
||||
if (commit != repository.commits.end()) commit->refs.push_back(std::move(badge));
|
||||
}
|
||||
|
||||
int submoduleCallback(git_submodule* submodule, const char*, void* payload) {
|
||||
static_cast<std::vector<std::string>*>(payload)->emplace_back(git_submodule_name(submodule));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
GitManager::GitManager() { git_libgit2_init(); }
|
||||
GitManager::~GitManager() { git_libgit2_shutdown(); }
|
||||
|
||||
std::string GitManager::lastError(const char* fallback) {
|
||||
const git_error* error = git_error_last();
|
||||
return error && error->message ? error->message : fallback;
|
||||
}
|
||||
|
||||
std::string GitManager::formatTime(git_time_t value, int offset_minutes) {
|
||||
std::time_t adjusted = static_cast<std::time_t>(value + offset_minutes * 60);
|
||||
std::tm tm{};
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &adjusted);
|
||||
#else
|
||||
gmtime_r(&adjusted, &tm);
|
||||
#endif
|
||||
char buffer[48]{};
|
||||
std::strftime(buffer, sizeof(buffer), "%m/%d/%Y %I:%M %p", &tm);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void GitManager::readBranches(RepositoryView& repository, git_branch_t type,
|
||||
std::vector<std::string>& output) {
|
||||
git_branch_iterator* iterator = nullptr;
|
||||
if (git_branch_iterator_new(&iterator, repository.repo, type) != 0) return;
|
||||
git_reference* reference = nullptr;
|
||||
git_branch_t found{};
|
||||
while (git_branch_next(&reference, &found, iterator) == 0) {
|
||||
const char* name = nullptr;
|
||||
if (git_branch_name(&name, reference) == 0 && name) output.emplace_back(name);
|
||||
git_reference_free(reference);
|
||||
}
|
||||
git_branch_iterator_free(iterator);
|
||||
}
|
||||
|
||||
void GitManager::loadRefBadges(RepositoryView& repository) {
|
||||
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
|
||||
git_branch_iterator* iterator = nullptr;
|
||||
if (git_branch_iterator_new(&iterator, repository.repo, type) != 0) continue;
|
||||
git_reference* reference = nullptr;
|
||||
git_branch_t found{};
|
||||
while (git_branch_next(&reference, &found, iterator) == 0) {
|
||||
const char* name = nullptr;
|
||||
if (git_branch_name(&name, reference) == 0 && name) {
|
||||
RefBadge badge;
|
||||
badge.name = name;
|
||||
badge.kind = type == GIT_BRANCH_LOCAL ? RefKind::local : RefKind::remote;
|
||||
badge.current = type == GIT_BRANCH_LOCAL && badge.name == repository.branch;
|
||||
badge.worktree = type == GIT_BRANCH_LOCAL &&
|
||||
(badge.current || repository.worktree_branches.contains(badge.name));
|
||||
if (type == GIT_BRANCH_LOCAL) {
|
||||
git_reference* upstream = nullptr;
|
||||
badge.upstream = git_branch_upstream(&upstream, reference) == 0;
|
||||
git_reference_free(upstream);
|
||||
}
|
||||
git_object* object = nullptr;
|
||||
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
|
||||
addBadge(repository, git_object_id(object), std::move(badge));
|
||||
git_object_free(object);
|
||||
}
|
||||
}
|
||||
git_reference_free(reference);
|
||||
}
|
||||
git_branch_iterator_free(iterator);
|
||||
}
|
||||
|
||||
git_reference_iterator* tags = nullptr;
|
||||
if (git_reference_iterator_glob_new(&tags, repository.repo, "refs/tags/*") != 0) return;
|
||||
git_reference* reference = nullptr;
|
||||
while (git_reference_next(&reference, tags) == 0) {
|
||||
git_object* object = nullptr;
|
||||
if (git_reference_peel(&object, reference, GIT_OBJECT_COMMIT) == 0) {
|
||||
const char* name = git_reference_shorthand(reference);
|
||||
addBadge(repository, git_object_id(object), {name ? name : "tag", RefKind::tag});
|
||||
git_object_free(object);
|
||||
}
|
||||
git_reference_free(reference);
|
||||
}
|
||||
git_reference_iterator_free(tags);
|
||||
}
|
||||
|
||||
void GitManager::computeGraphLanes(RepositoryView& repository) {
|
||||
std::vector<git_oid> lanes;
|
||||
for (auto& commit : repository.commits) {
|
||||
auto current = std::find_if(lanes.begin(), lanes.end(), [&commit](const git_oid& oid) {
|
||||
return git_oid_equal(&oid, &commit.oid) != 0;
|
||||
});
|
||||
if (current == lanes.end()) { lanes.push_back(commit.oid); current = std::prev(lanes.end()); }
|
||||
const size_t lane = static_cast<size_t>(std::distance(lanes.begin(), current));
|
||||
commit.lane = static_cast<int>(lane);
|
||||
if (commit.parent_ids.empty()) { lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(lane)); continue; }
|
||||
const git_oid first = commit.parent_ids.front();
|
||||
const auto existing = std::find_if(lanes.begin(), lanes.end(), [&first](const git_oid& oid) {
|
||||
return git_oid_equal(&oid, &first) != 0;
|
||||
});
|
||||
if (existing != lanes.end() && existing != current) lanes.erase(lanes.begin() + static_cast<std::ptrdiff_t>(lane));
|
||||
else lanes[lane] = first;
|
||||
for (size_t parent = 1; parent < commit.parent_ids.size(); ++parent) {
|
||||
const auto found = std::find_if(lanes.begin(), lanes.end(), [&commit, parent](const git_oid& oid) {
|
||||
return git_oid_equal(&oid, &commit.parent_ids[parent]) != 0;
|
||||
});
|
||||
if (found == lanes.end()) lanes.push_back(commit.parent_ids[parent]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool GitManager::loadRepositoryData(RepositoryView& repository, std::string& error) {
|
||||
repository.local_branches.clear(); repository.remote_branches.clear(); repository.remotes.clear();
|
||||
repository.worktrees.clear(); repository.worktree_branches.clear(); repository.submodules.clear();
|
||||
repository.commits.clear(); repository.selected_commit = 0;
|
||||
const char* workdir = git_repository_workdir(repository.repo);
|
||||
repository.path = workdir ? workdir : git_repository_path(repository.repo);
|
||||
std::filesystem::path path(repository.path);
|
||||
repository.name = path.filename().string();
|
||||
if (repository.name.empty()) repository.name = path.parent_path().filename().string();
|
||||
|
||||
git_reference* head = nullptr;
|
||||
if (git_repository_head(&head, repository.repo) == 0) {
|
||||
if (const char* name = git_reference_shorthand(head)) repository.branch = name;
|
||||
git_reference_free(head);
|
||||
}
|
||||
readBranches(repository, GIT_BRANCH_LOCAL, repository.local_branches);
|
||||
readBranches(repository, GIT_BRANCH_REMOTE, repository.remote_branches);
|
||||
|
||||
git_strarray names{};
|
||||
if (git_remote_list(&names, repository.repo) == 0) {
|
||||
for (size_t i = 0; i < names.count; ++i) repository.remotes.emplace_back(names.strings[i]);
|
||||
git_strarray_dispose(&names);
|
||||
}
|
||||
if (git_worktree_list(&names, repository.repo) == 0) {
|
||||
for (size_t i = 0; i < names.count; ++i) {
|
||||
repository.worktrees.emplace_back(names.strings[i]);
|
||||
git_worktree* worktree = nullptr;
|
||||
git_repository* worktree_repository = nullptr;
|
||||
git_reference* worktree_head = nullptr;
|
||||
if (git_worktree_lookup(&worktree, repository.repo, names.strings[i]) == 0 &&
|
||||
git_repository_open_from_worktree(&worktree_repository, worktree) == 0 &&
|
||||
git_repository_head(&worktree_head, worktree_repository) == 0) {
|
||||
if (const char* branch = git_reference_shorthand(worktree_head))
|
||||
repository.worktree_branches.emplace(branch);
|
||||
}
|
||||
git_reference_free(worktree_head);
|
||||
git_repository_free(worktree_repository);
|
||||
git_worktree_free(worktree);
|
||||
}
|
||||
git_strarray_dispose(&names);
|
||||
}
|
||||
git_submodule_foreach(repository.repo, submoduleCallback, &repository.submodules);
|
||||
|
||||
git_revwalk* walk = nullptr;
|
||||
if (git_revwalk_new(&walk, repository.repo) != 0) { error = lastError("Unable to read history"); return false; }
|
||||
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
|
||||
if (git_revwalk_push_head(walk) != 0) { git_revwalk_free(walk); return true; }
|
||||
git_oid oid{};
|
||||
while (repository.commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
|
||||
git_commit* commit = nullptr;
|
||||
if (git_commit_lookup(&commit, repository.repo, &oid) != 0) continue;
|
||||
CommitInfo item; item.oid = oid;
|
||||
char id[GIT_OID_SHA1_HEXSIZE + 1]{}; git_oid_tostr(id, sizeof(id), &oid); item.short_id.assign(id, 8);
|
||||
item.summary = git_commit_summary(commit) ? git_commit_summary(commit) : "(no commit message)";
|
||||
if (const git_signature* author = git_commit_author(commit)) {
|
||||
item.author = author->name ? author->name : "Unknown";
|
||||
item.email = author->email ? author->email : "";
|
||||
item.date = formatTime(author->when.time, author->when.offset);
|
||||
}
|
||||
item.parents = static_cast<int>(git_commit_parentcount(commit));
|
||||
for (size_t i = 0; i < git_commit_parentcount(commit); ++i)
|
||||
if (const git_oid* parent = git_commit_parent_id(commit, static_cast<unsigned int>(i))) item.parent_ids.push_back(*parent);
|
||||
repository.commits.push_back(std::move(item));
|
||||
git_commit_free(commit);
|
||||
}
|
||||
git_revwalk_free(walk);
|
||||
computeGraphLanes(repository);
|
||||
loadRefBadges(repository);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::openRepository(RepositoryView& repository, const std::string& path, std::string& error) {
|
||||
git_repository* opened = nullptr;
|
||||
if (git_repository_open_ext(&opened, path.c_str(), 0, nullptr) != 0) { error = lastError("Unable to open repository"); return false; }
|
||||
repository.close(); repository.repo = opened;
|
||||
return loadRepositoryData(repository, error);
|
||||
}
|
||||
|
||||
bool GitManager::initializeRepository(RepositoryView& repository, const std::string& path, std::string& error) {
|
||||
git_repository* created = nullptr;
|
||||
if (git_repository_init(&created, path.c_str(), 0) != 0) { error = lastError("Unable to initialize repository"); return false; }
|
||||
git_repository_free(created);
|
||||
return openRepository(repository, path, error);
|
||||
}
|
||||
|
||||
bool GitManager::reload(RepositoryView& repository, std::string& error) {
|
||||
return repository.repo && loadRepositoryData(repository, error);
|
||||
}
|
||||
22
src/managers/git_manager.h
Normal file
22
src/managers/git_manager.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/repository.h"
|
||||
#include <string>
|
||||
|
||||
class GitManager {
|
||||
public:
|
||||
GitManager();
|
||||
~GitManager();
|
||||
|
||||
bool openRepository(RepositoryView& repository, const std::string& path, std::string& error);
|
||||
bool initializeRepository(RepositoryView& repository, const std::string& path, std::string& error);
|
||||
bool reload(RepositoryView& repository, std::string& error);
|
||||
|
||||
private:
|
||||
static std::string lastError(const char* fallback);
|
||||
static std::string formatTime(git_time_t value, int offset_minutes);
|
||||
void readBranches(RepositoryView& repository, git_branch_t type, std::vector<std::string>& output);
|
||||
void loadRefBadges(RepositoryView& repository);
|
||||
void computeGraphLanes(RepositoryView& repository);
|
||||
bool loadRepositoryData(RepositoryView& repository, std::string& error);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
@@ -18,8 +19,10 @@ bool WindowManager::create(const char* title, int width, int height) {
|
||||
#ifdef _WIN32
|
||||
using SetDpiAwarenessContext = BOOL(WINAPI*)(HANDLE);
|
||||
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
const auto set_dpi_awareness = reinterpret_cast<SetDpiAwarenessContext>(
|
||||
GetProcAddress(user32, "SetProcessDpiAwarenessContext"));
|
||||
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
|
||||
#endif
|
||||
if (!glfwInit()) return false;
|
||||
54
src/models/repository.h
Normal file
54
src/models/repository.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <git2.h>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class RefKind { local, remote, tag };
|
||||
|
||||
struct RefBadge {
|
||||
std::string name;
|
||||
RefKind kind = RefKind::local;
|
||||
bool current = false;
|
||||
bool worktree = false;
|
||||
bool upstream = false;
|
||||
};
|
||||
|
||||
struct CommitInfo {
|
||||
git_oid oid{};
|
||||
std::string short_id;
|
||||
std::string summary;
|
||||
std::string author;
|
||||
std::string email;
|
||||
std::string date;
|
||||
int parents = 0;
|
||||
int lane = 0;
|
||||
std::vector<git_oid> parent_ids;
|
||||
std::vector<RefBadge> refs;
|
||||
};
|
||||
|
||||
struct RepositoryView {
|
||||
git_repository* repo = nullptr;
|
||||
std::string path;
|
||||
std::string name = "New Tab";
|
||||
std::string branch = "detached";
|
||||
std::vector<std::string> local_branches;
|
||||
std::vector<std::string> remote_branches;
|
||||
std::vector<std::string> remotes;
|
||||
std::vector<std::string> worktrees;
|
||||
std::set<std::string> worktree_branches;
|
||||
std::vector<std::string> submodules;
|
||||
std::vector<CommitInfo> commits;
|
||||
int selected_commit = 0;
|
||||
|
||||
RepositoryView() = default;
|
||||
~RepositoryView() { close(); }
|
||||
RepositoryView(const RepositoryView&) = delete;
|
||||
RepositoryView& operator=(const RepositoryView&) = delete;
|
||||
|
||||
void close() {
|
||||
if (repo) git_repository_free(repo);
|
||||
repo = nullptr;
|
||||
}
|
||||
};
|
||||
749
src/ui/gitree_ui.cpp
Normal file
749
src/ui/gitree_ui.cpp
Normal file
@@ -0,0 +1,749 @@
|
||||
#include <git2.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_glfw.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <izo/dialogs.hpp>
|
||||
#include <IconsFontAwesome6.h>
|
||||
#include "ui/gitree_ui.h"
|
||||
#include "managers/avatar_cache.h"
|
||||
#include "managers/git_manager.h"
|
||||
#include "managers/user_data.h"
|
||||
#include "managers/window_manager.h"
|
||||
#include "models/repository.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::unique_ptr<RepositoryView>> g_tabs;
|
||||
size_t g_active_tab = 0;
|
||||
UserData* g_user_data = nullptr;
|
||||
AvatarCache* g_avatar_cache = nullptr;
|
||||
std::array<char, 1024> g_path{};
|
||||
std::array<char, 256> g_filter{};
|
||||
std::string g_notice;
|
||||
bool g_init_popup = false;
|
||||
bool g_about_popup = false;
|
||||
bool g_licenses_popup = false;
|
||||
float g_ui_scale = 1.0f;
|
||||
float g_sidebar_width = 230.0f;
|
||||
WindowManager* g_window_manager = nullptr;
|
||||
GitManager* g_git_manager = nullptr;
|
||||
|
||||
float ui(float value) { return value * g_ui_scale; }
|
||||
RepositoryView& repo() { return *g_tabs.at(g_active_tab); }
|
||||
|
||||
void create_new_tab() {
|
||||
g_tabs.push_back(std::make_unique<RepositoryView>());
|
||||
g_active_tab = g_tabs.size() - 1;
|
||||
}
|
||||
|
||||
void close_tab(size_t index) {
|
||||
if (index >= g_tabs.size()) return;
|
||||
if (g_user_data && !g_tabs[index]->path.empty()) g_user_data->addRecentlyClosed(g_tabs[index]->path);
|
||||
g_tabs.erase(g_tabs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (g_tabs.empty()) create_new_tab();
|
||||
else if (g_active_tab >= g_tabs.size()) g_active_tab = g_tabs.size() - 1;
|
||||
else if (index < g_active_tab) --g_active_tab;
|
||||
}
|
||||
|
||||
bool open_repository(const char* path) {
|
||||
return g_git_manager->openRepository(repo(), path, g_notice);
|
||||
}
|
||||
|
||||
void pick_and_open_repository() {
|
||||
izo::dialog_options options;
|
||||
options.title = "Open Git repository";
|
||||
if (!repo().path.empty()) options.initial_path = repo().path;
|
||||
else options.initial_path = std::filesystem::current_path();
|
||||
|
||||
const izo::dialog_result result = izo::pick_folder(options);
|
||||
if (result.status == izo::dialog_status::cancelled) return;
|
||||
if (result.status == izo::dialog_status::error) {
|
||||
g_notice = result.error_message.empty() ? "Unable to open the folder picker" : result.error_message;
|
||||
return;
|
||||
}
|
||||
if (result.paths.empty()) {
|
||||
g_notice = "The folder picker returned no path";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto utf8 = result.paths.front().u8string();
|
||||
const std::string path(reinterpret_cast<const char*>(utf8.data()), utf8.size());
|
||||
if (repo().repo) create_new_tab();
|
||||
open_repository(path.c_str());
|
||||
}
|
||||
|
||||
bool init_repository(const char* path) {
|
||||
return g_git_manager->initializeRepository(repo(), path, g_notice);
|
||||
}
|
||||
|
||||
void apply_style(float scale) {
|
||||
g_ui_scale = scale;
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style = ImGuiStyle();
|
||||
style.WindowRounding = 0.0f;
|
||||
style.ChildRounding = 3.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.ScrollbarRounding = 3.0f;
|
||||
style.FramePadding = {8.0f, 5.0f};
|
||||
style.ItemSpacing = {8.0f, 6.0f};
|
||||
style.WindowPadding = {8.0f, 8.0f};
|
||||
style.Colors[ImGuiCol_Text] = ImVec4(0.89f, 0.91f, 0.94f, 1.0f);
|
||||
style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.58f, 0.66f, 1.0f);
|
||||
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.100f, 0.116f, 0.145f, 1.0f);
|
||||
style.Colors[ImGuiCol_ChildBg] = ImVec4(0.116f, 0.133f, 0.165f, 1.0f);
|
||||
style.Colors[ImGuiCol_PopupBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
|
||||
style.Colors[ImGuiCol_Border] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
|
||||
style.Colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
|
||||
style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.24f, 0.29f, 1.0f);
|
||||
style.Colors[ImGuiCol_Button] = ImVec4(0.16f, 0.19f, 0.23f, 1.0f);
|
||||
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.10f, 0.44f, 0.56f, 1.0f);
|
||||
style.Colors[ImGuiCol_Header] = ImVec4(0.08f, 0.40f, 0.50f, 1.0f);
|
||||
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.10f, 0.48f, 0.59f, 1.0f);
|
||||
style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.07f, 0.35f, 0.45f, 1.0f);
|
||||
style.Colors[ImGuiCol_Tab] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
|
||||
style.Colors[ImGuiCol_TabSelected] = ImVec4(0.19f, 0.22f, 0.27f, 1.0f);
|
||||
style.Colors[ImGuiCol_Separator] = ImVec4(0.24f, 0.28f, 0.34f, 1.0f);
|
||||
style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.13f, 0.15f, 0.19f, 1.0f);
|
||||
style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.125f, 0.143f, 0.177f, 1.0f);
|
||||
style.ScaleAllSizes(scale);
|
||||
}
|
||||
|
||||
void load_fonts(float scale) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.Fonts->Clear();
|
||||
ImFontConfig config;
|
||||
config.OversampleH = 2;
|
||||
config.OversampleV = 2;
|
||||
config.PixelSnapH = false;
|
||||
config.RasterizerDensity = 1.0f;
|
||||
const float size = 15.0f * scale;
|
||||
if (!io.Fonts->AddFontFromFileTTF(GITREE_ASSET_DIR "/InterVariable.ttf", size, &config))
|
||||
io.Fonts->AddFontDefault();
|
||||
|
||||
static constexpr ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_16_FA, 0};
|
||||
ImFontConfig icon_config;
|
||||
icon_config.MergeMode = true;
|
||||
icon_config.PixelSnapH = true;
|
||||
icon_config.GlyphMinAdvanceX = size;
|
||||
io.Fonts->AddFontFromFileTTF(
|
||||
GITREE_ASSET_DIR "/fa-solid-900.ttf", size, &icon_config, icon_ranges);
|
||||
apply_style(scale);
|
||||
}
|
||||
|
||||
bool sidebar_collapse_row(const char* id, const std::string& label, bool default_open, float reserved_width) {
|
||||
ImGui::PushID(id);
|
||||
const ImGuiID state_id = ImGui::GetID("collapse_state");
|
||||
ImGuiStorage* storage = ImGui::GetStateStorage();
|
||||
bool open = storage->GetBool(state_id, default_open);
|
||||
const std::string row = std::string(open ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_RIGHT) +
|
||||
" " + label + "##collapse";
|
||||
if (ImGui::Selectable(row.c_str(), false, ImGuiSelectableFlags_AllowOverlap,
|
||||
{std::max(ui(24.0f), ImGui::GetContentRegionAvail().x - reserved_width), 0})) {
|
||||
open = !open;
|
||||
storage->SetBool(state_id, open);
|
||||
}
|
||||
ImGui::PopID();
|
||||
return open;
|
||||
}
|
||||
|
||||
bool sidebar_section_header(const char* label, int count, const char* add_tooltip, const char* add_notice) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
|
||||
const bool open = sidebar_collapse_row(label, label, true, ui(62.0f));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SameLine(0, ui(5.0f));
|
||||
ImGui::TextDisabled("%d", count);
|
||||
ImGui::SameLine(0, ui(7.0f));
|
||||
ImGui::PushID(label);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.43f, 0.90f, 0.51f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.72f, 0.35f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(5.0f), ui(1.0f)});
|
||||
if (ImGui::SmallButton(ICON_FA_PLUS "##add")) g_notice = add_notice;
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) ImGui::SetTooltip("%s", add_tooltip);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopID();
|
||||
return open;
|
||||
}
|
||||
|
||||
void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
|
||||
const char* add_tooltip, const char* add_notice) {
|
||||
const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
|
||||
if (open) {
|
||||
for (const auto& item : items) {
|
||||
ImGui::Selectable((std::string(" ") + item_icon + " " + item).c_str());
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
struct BranchNode {
|
||||
std::map<std::string, BranchNode> children;
|
||||
std::string full_name;
|
||||
bool branch = false;
|
||||
};
|
||||
|
||||
void add_branch_node(BranchNode& root, const std::string& branch) {
|
||||
BranchNode* node = &root;
|
||||
size_t start = 0;
|
||||
while (start < branch.size()) {
|
||||
const size_t slash = branch.find('/', start);
|
||||
const std::string part = branch.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
|
||||
node = &node->children[part];
|
||||
if (slash == std::string::npos) break;
|
||||
start = slash + 1;
|
||||
}
|
||||
node->branch = true;
|
||||
node->full_name = branch;
|
||||
}
|
||||
|
||||
void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const char* root_group_icon,
|
||||
const std::string& id_path = {}, int depth = 0) {
|
||||
for (const auto& [name, node] : parent.children) {
|
||||
const std::string id = id_path + "/" + name;
|
||||
if (!node.children.empty()) {
|
||||
const char* group_icon = depth == 0 ? root_group_icon : ICON_FA_FOLDER;
|
||||
const bool open = sidebar_collapse_row(id.c_str(), std::string(group_icon) + " " + name, true, 0);
|
||||
if (open) {
|
||||
ImGui::Indent(ui(17.0f));
|
||||
if (node.branch) ImGui::Selectable((std::string(branch_icon) + " " + name + "##branch" + id).c_str());
|
||||
draw_branch_nodes(node, branch_icon, root_group_icon, id, depth + 1);
|
||||
ImGui::Unindent(ui(17.0f));
|
||||
}
|
||||
} else {
|
||||
ImGui::Selectable((std::string(branch_icon) + " " + name + "##" + id).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void branch_section(const char* label, const std::vector<std::string>& branches, const char* branch_icon,
|
||||
const char* group_icon, const char* add_tooltip, const char* add_notice) {
|
||||
const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
|
||||
if (open) {
|
||||
BranchNode root;
|
||||
for (const auto& branch : branches) {
|
||||
if (g_filter[0] && branch.find(g_filter.data()) == std::string::npos) continue;
|
||||
add_branch_node(root, branch);
|
||||
}
|
||||
ImGui::Indent(ui(17.0f));
|
||||
draw_branch_nodes(root, branch_icon, group_icon, label);
|
||||
ImGui::Unindent(ui(17.0f));
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
void draw_sidebar(float width) {
|
||||
ImGui::BeginChild("sidebar", {width, -ui(28.0f)}, ImGuiChildFlags_Borders);
|
||||
if (ImGui::Button(ICON_FA_LIST " List", {ui(98), ui(30)})) {}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(ICON_FA_LAYER_GROUP " Workspace", {ui(102), ui(30)})) {}
|
||||
ImGui::TextDisabled(ICON_FA_EYE " VIEWING %d", static_cast<int>(repo().local_branches.size() + repo().remote_branches.size()));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##filter", ICON_FA_MAGNIFYING_GLASS " Search branches...", g_filter.data(), g_filter.size());
|
||||
ImGui::Spacing();
|
||||
branch_section(ICON_FA_HOUSE " LOCAL", repo().local_branches, ICON_FA_CODE_BRANCH, ICON_FA_FOLDER,
|
||||
"Create local branch", "Branch creation is not wired yet");
|
||||
branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE,
|
||||
"Add remote", "Remote creation is not wired yet");
|
||||
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER,
|
||||
"Add worktree", "Worktree creation is not wired yet");
|
||||
section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES,
|
||||
"Add submodule", "Submodule creation is not wired yet");
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ImU32 graph_lane_color(int lane) {
|
||||
static constexpr ImU32 colors[] = {
|
||||
IM_COL32(20, 179, 211, 255), IM_COL32(220, 38, 190, 255),
|
||||
IM_COL32(244, 54, 74, 255), IM_COL32(255, 92, 28, 255),
|
||||
IM_COL32(242, 196, 36, 255), IM_COL32(61, 191, 123, 255),
|
||||
IM_COL32(82, 126, 235, 255),
|
||||
};
|
||||
return colors[static_cast<size_t>(std::abs(lane)) % std::size(colors)];
|
||||
}
|
||||
|
||||
void draw_ref_badge(const RefBadge& badge, int index, int lane) {
|
||||
std::string text;
|
||||
if (badge.current) text += ICON_FA_CHECK " ";
|
||||
text += badge.name;
|
||||
if (badge.kind == RefKind::local) text += std::string(" ") + ICON_FA_HOUSE;
|
||||
if (badge.worktree) text += std::string(" ") + ICON_FA_COMPUTER;
|
||||
if (badge.kind == RefKind::remote || badge.upstream) text += std::string(" ") + ICON_FA_CLOUD;
|
||||
if (badge.kind == RefKind::tag) text += std::string(" ") + ICON_FA_TAG;
|
||||
|
||||
const ImVec4 lane_color = ImGui::ColorConvertU32ToFloat4(graph_lane_color(lane));
|
||||
const ImVec4 color = ImVec4(lane_color.x * 0.42f, lane_color.y * 0.42f, lane_color.z * 0.42f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, color);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x + 0.06f, color.y + 0.06f, color.z + 0.06f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, color);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ui(3.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ui(6.0f), ui(2.0f)});
|
||||
const std::string id = text + "##ref_badge_" + std::to_string(index);
|
||||
ImGui::Button(id.c_str());
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
void draw_graph_cell(int row, const CommitInfo& commit) {
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
constexpr float row_height = 28.0f;
|
||||
const float lane_spacing = ui(22.0f);
|
||||
const float x = p.x + ui(16.0f) + lane_spacing * commit.lane;
|
||||
const float y = p.y + ImGui::GetTextLineHeight() * 0.5f;
|
||||
const ImU32 color = graph_lane_color(commit.lane);
|
||||
|
||||
if (!commit.refs.empty()) draw->AddLine({p.x, y}, {x, y}, color, ui(2.0f));
|
||||
for (const git_oid& parent_id : commit.parent_ids) {
|
||||
const auto parent = std::find_if(repo().commits.begin(), repo().commits.end(), [&parent_id](const CommitInfo& item) {
|
||||
return git_oid_equal(&item.oid, &parent_id) != 0;
|
||||
});
|
||||
if (parent == repo().commits.end()) continue;
|
||||
const int parent_row = static_cast<int>(std::distance(repo().commits.begin(), parent));
|
||||
const float parent_x = p.x + ui(16.0f) + lane_spacing * parent->lane;
|
||||
const float parent_y = y + ui(row_height) * (parent_row - row);
|
||||
if (parent->lane == commit.lane) {
|
||||
draw->AddLine({x, y}, {parent_x, parent_y}, color, ui(2.0f));
|
||||
} else {
|
||||
const float bend_y = y + std::min(ui(18.0f), (parent_y - y) * 0.5f);
|
||||
draw->AddBezierCubic({x, y}, {x, bend_y}, {parent_x, bend_y}, {parent_x, parent_y},
|
||||
graph_lane_color(parent->lane), ui(2.0f));
|
||||
}
|
||||
}
|
||||
|
||||
const float radius = ui(9.0f);
|
||||
draw->AddCircleFilled({x, y}, radius + ui(2.0f), color);
|
||||
draw->AddCircleFilled({x, y}, radius, IM_COL32(238, 243, 246, 255));
|
||||
const unsigned int texture = g_avatar_cache ? g_avatar_cache->textureFor(commit.email) : 0;
|
||||
if (texture) {
|
||||
draw->AddImageRounded(ImTextureRef(static_cast<ImTextureID>(texture)),
|
||||
{x - radius + ui(1), y - radius + ui(1)}, {x + radius - ui(1), y + radius - ui(1)},
|
||||
{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}, color, ICON_FA_USER);
|
||||
}
|
||||
draw->AddCircle({x, y}, radius, IM_COL32(255, 255, 255, 255), 0, ui(1.0f));
|
||||
ImGui::Dummy({ui(190.0f), ui(row_height - 4.0f)});
|
||||
}
|
||||
|
||||
void draw_commit_table() {
|
||||
ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
|
||||
ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp;
|
||||
if (!ImGui::BeginTable("commits", 4, flags, {-1, -1})) return;
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("BRANCH / TAG", ImGuiTableColumnFlags_WidthFixed, ui(210.0f));
|
||||
ImGui::TableSetupColumn("GRAPH", ImGuiTableColumnFlags_WidthFixed, ui(210.0f));
|
||||
ImGui::TableSetupColumn("COMMIT MESSAGE", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("COMMIT DATE / TIME", ImGuiTableColumnFlags_WidthFixed, ui(180.0f));
|
||||
ImGui::TableHeadersRow();
|
||||
for (int i = 0; i < static_cast<int>(repo().commits.size()); ++i) {
|
||||
const auto& commit = repo().commits[i];
|
||||
if (g_filter[0]) {
|
||||
const bool message_matches = commit.summary.find(g_filter.data()) != std::string::npos;
|
||||
const bool ref_matches = std::any_of(commit.refs.begin(), commit.refs.end(), [](const RefBadge& ref) {
|
||||
return ref.name.find(g_filter.data()) != std::string::npos;
|
||||
});
|
||||
if (!message_matches && !ref_matches) continue;
|
||||
}
|
||||
ImGui::TableNextRow(0, ui(28.0f));
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
std::string row_id = "##commit" + std::to_string(i);
|
||||
if (ImGui::Selectable(row_id.c_str(), repo().selected_commit == i,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) repo().selected_commit = i;
|
||||
ImGui::SameLine(0, ui(3));
|
||||
for (int ref_index = 0; ref_index < static_cast<int>(commit.refs.size()); ++ref_index) {
|
||||
if (ref_index > 0) ImGui::SameLine(0, ui(4));
|
||||
draw_ref_badge(commit.refs[ref_index], i * 1000 + ref_index, commit.lane);
|
||||
}
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
draw_graph_cell(i, commit);
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::TextUnformatted(commit.summary.c_str());
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextDisabled("%s", commit.date.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void draw_details() {
|
||||
ImGui::BeginChild("details", {ui(360.0f), -ui(28.0f)}, ImGuiChildFlags_Borders);
|
||||
if (repo().commits.empty()) {
|
||||
ImGui::TextDisabled("Select a repository with commits");
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
const auto& commit = repo().commits[std::clamp(repo().selected_commit, 0, static_cast<int>(repo().commits.size() - 1))];
|
||||
ImGui::TextDisabled("COMMIT");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", commit.short_id.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy({0, 6});
|
||||
ImGui::PushTextWrapPos();
|
||||
ImGui::Text("%s", commit.summary.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy({0, 26});
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.25f, 0.80f, 0.86f, 1), "%s", commit.author.c_str());
|
||||
ImGui::TextDisabled("%s", commit.email.c_str());
|
||||
ImGui::TextDisabled("authored %s", commit.date.c_str());
|
||||
ImGui::Dummy({0, 24});
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.68f, 0.22f, 1), "%d parent%s", commit.parents, commit.parents == 1 ? "" : "s");
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Changed files and diffs will appear here");
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
bool toolbar_action(const char* id, const char* icon, const char* tooltip) {
|
||||
ImGui::PushID(id);
|
||||
const bool clicked = ImGui::Button(icon, {ui(36), ui(36)});
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(tooltip);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void external_link(const char* label, const char* url) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.31f, 0.76f, 0.92f, 1.0f));
|
||||
if (ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups)) {
|
||||
std::string error;
|
||||
if (!izo::open_path(url, &error)) g_notice = error.empty() ? "Unable to open link" : error;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", url);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_about_popup() {
|
||||
if (g_about_popup) {
|
||||
ImGui::OpenPopup("About Gitree");
|
||||
g_about_popup = false;
|
||||
}
|
||||
if (!ImGui::BeginPopupModal("About Gitree", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
|
||||
ImGui::SetWindowFontScale(1.35f);
|
||||
ImGui::TextUnformatted("Gitree");
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::TextDisabled("Version %s", GITREE_VERSION);
|
||||
ImGui::Dummy({ui(460), ui(8)});
|
||||
ImGui::TextWrapped("A fast native Git client built with C++, Dear ImGui, GLFW, and libgit2.");
|
||||
ImGui::Spacing();
|
||||
external_link("Gitree project page", "https://dock-it.dev/iDENTITY-Technology/Gitree");
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Licenses", {ui(110), 0})) {
|
||||
g_licenses_popup = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void draw_licenses_popup() {
|
||||
if (g_licenses_popup) {
|
||||
ImGui::OpenPopup("Licenses and dependencies");
|
||||
g_licenses_popup = false;
|
||||
}
|
||||
if (!ImGui::BeginPopupModal("Licenses and dependencies", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
|
||||
ImGui::TextUnformatted("Gitree");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Creative Commons Attribution-ShareAlike 4.0");
|
||||
ImGui::TextDisabled("Third-party software included with Gitree:");
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::BeginTable("dependency_licenses", 2,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit,
|
||||
{ui(680), ui(270)})) {
|
||||
ImGui::TableSetupColumn("Dependency", ImGuiTableColumnFlags_WidthFixed, ui(170));
|
||||
ImGui::TableSetupColumn("License and source", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
struct Dependency { const char* name; const char* license; const char* url; };
|
||||
constexpr Dependency dependencies[] = {
|
||||
{"libgit2 1.9.4", "GPL-2.0 with linking exception", "https://github.com/libgit2/libgit2"},
|
||||
{"Dear ImGui 1.92.8", "MIT License", "https://github.com/ocornut/imgui"},
|
||||
{"GLFW 3.4", "zlib/libpng License", "https://github.com/glfw/glfw"},
|
||||
{"iZo 0.1.0", "MIT License", "https://dock-it.dev/Idea-Studios/iZo"},
|
||||
{"Inter", "SIL Open Font License 1.1", "https://github.com/rsms/inter"},
|
||||
{"Font Awesome Free", "SIL Open Font License 1.1", "https://github.com/FortAwesome/Font-Awesome"},
|
||||
};
|
||||
for (const auto& dependency : dependencies) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::TextUnformatted(dependency.name);
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextDisabled("%s", dependency.license);
|
||||
external_link(dependency.url, dependency.url);
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Complete license texts are included in the source tree with each dependency.");
|
||||
if (ImGui::Button("Back", {ui(90), 0})) {
|
||||
g_about_popup = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close", {ui(90), 0})) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void draw_popups() {
|
||||
if (g_init_popup) { ImGui::OpenPopup("Create repository"); g_init_popup = false; }
|
||||
if (ImGui::BeginPopupModal("Create repository", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextUnformatted("Repository folder");
|
||||
ImGui::SetNextItemWidth(ui(520.0f));
|
||||
const bool enter = ImGui::InputText("##path", g_path.data(), g_path.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
if (enter || ImGui::Button("Create", {ui(90), 0})) {
|
||||
if (init_repository(g_path.data())) ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", {ui(90), 0})) ImGui::CloseCurrentPopup();
|
||||
if (!g_notice.empty()) ImGui::TextColored(ImVec4(0.96f, 0.55f, 0.35f, 1), "%s", g_notice.c_str());
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
draw_about_popup();
|
||||
draw_licenses_popup();
|
||||
}
|
||||
|
||||
void draw_footer() {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("%s", g_notice.empty() ? "Ready" : g_notice.c_str());
|
||||
const char* version = "Gitree " GITREE_VERSION;
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(version).x - ui(18.0f));
|
||||
ImGui::TextDisabled("%s", version);
|
||||
}
|
||||
|
||||
void draw_new_tab() {
|
||||
ImGui::BeginChild("new_tab", {-1, -ui(28.0f)}, ImGuiChildFlags_Borders);
|
||||
ImGui::Dummy({0, ui(36)});
|
||||
ImGui::SetCursorPosX(ui(42));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.38f, 0.88f, 0.94f, 1.0f));
|
||||
ImGui::SetWindowFontScale(1.45f);
|
||||
ImGui::TextUnformatted(ICON_FA_CODE_BRANCH " New Tab");
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SetCursorPosX(ui(42));
|
||||
ImGui::TextDisabled("Open a repository or return to a recently closed tab.");
|
||||
ImGui::SetCursorPosX(ui(42));
|
||||
if (ImGui::Button(ICON_FA_FOLDER_OPEN " Open repository", {ui(190), ui(38)}))
|
||||
pick_and_open_repository();
|
||||
|
||||
ImGui::Dummy({0, ui(24)});
|
||||
ImGui::SetCursorPosX(ui(42));
|
||||
ImGui::TextUnformatted("RECENTLY CLOSED");
|
||||
ImGui::SetCursorPosX(ui(42));
|
||||
ImGui::BeginChild("recently_closed", {-ui(42), ui(300)}, ImGuiChildFlags_Borders);
|
||||
const auto& recent = g_user_data->recentlyClosed();
|
||||
if (recent.empty()) {
|
||||
ImGui::TextDisabled("No recently closed repositories yet.");
|
||||
} else {
|
||||
for (int i = 0; i < static_cast<int>(recent.size()); ++i) {
|
||||
const std::filesystem::path path(recent[i]);
|
||||
std::string name = path.filename().string();
|
||||
if (name.empty()) name = path.parent_path().filename().string();
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable((std::string(ICON_FA_CLOCK_ROTATE_LEFT) + " " + name).c_str(), false,
|
||||
ImGuiSelectableFlags_AllowDoubleClick, {0, ui(42)})) {
|
||||
open_repository(recent[i].c_str());
|
||||
}
|
||||
ImGui::SameLine(ui(260));
|
||||
ImGui::TextDisabled("%s", recent[i].c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void draw_app() {
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
ImGui::Begin("Gitree", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar);
|
||||
|
||||
if (ImGui::BeginMenuBar()) {
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
if (ImGui::MenuItem("Open repository...", "Ctrl+O")) pick_and_open_repository();
|
||||
if (ImGui::MenuItem("Create repository...", "Ctrl+N")) g_init_popup = true;
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Exit") && g_window_manager) g_window_manager->requestClose();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Edit")) { ImGui::MenuItem("Preferences", nullptr, false, false); ImGui::EndMenu(); }
|
||||
if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Refresh", "F5"); ImGui::EndMenu(); }
|
||||
if (ImGui::BeginMenu("Help")) {
|
||||
if (ImGui::MenuItem("About Gitree")) g_about_popup = true;
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
size_t tab_to_close = g_tabs.size();
|
||||
bool add_tab = false;
|
||||
if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable)) {
|
||||
for (size_t i = 0; i < g_tabs.size(); ++i) {
|
||||
bool open = true;
|
||||
const std::string label = g_tabs[i]->name + "###repo_tab_" + std::to_string(i);
|
||||
const ImGuiTabItemFlags flags = i == g_active_tab ? ImGuiTabItemFlags_SetSelected : ImGuiTabItemFlags_None;
|
||||
if (ImGui::BeginTabItem(label.c_str(), &open, flags)) {
|
||||
g_active_tab = i;
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (!open) tab_to_close = i;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(ICON_FA_PLUS "##new_tab")) add_tab = true;
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
if (tab_to_close < g_tabs.size()) close_tab(tab_to_close);
|
||||
if (add_tab) create_new_tab();
|
||||
ImGui::Separator();
|
||||
|
||||
if (!repo().repo) {
|
||||
draw_new_tab();
|
||||
draw_footer();
|
||||
draw_popups();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginChild("repo_toolbar", {-1, ui(56)}, ImGuiChildFlags_Borders);
|
||||
ImGui::TextDisabled("repository");
|
||||
ImGui::SameLine(ui(145)); ImGui::TextDisabled("branch");
|
||||
ImGui::SetCursorPosY(ui(26));
|
||||
ImGui::Text("%s", repo().name.c_str());
|
||||
ImGui::SameLine(ui(145));
|
||||
ImGui::SetNextItemWidth(ui(180));
|
||||
if (ImGui::BeginCombo("##branch", repo().branch.c_str())) {
|
||||
for (const auto& branch : repo().local_branches) ImGui::Selectable(branch.c_str(), branch == repo().branch);
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
constexpr int action_count = 6;
|
||||
const float action_size = ui(36.0f);
|
||||
const float action_spacing = ui(7.0f);
|
||||
const float action_group_width = action_size * action_count + action_spacing * (action_count - 1);
|
||||
const float centered_x = (ImGui::GetWindowWidth() - action_group_width) * 0.5f;
|
||||
ImGui::SetCursorPos({std::max(ui(350.0f), centered_x), ui(10.0f)});
|
||||
|
||||
if (toolbar_action("pull", ICON_FA_DOWNLOAD, "Pull from remote")) g_notice = "Pull is not wired yet";
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("push", ICON_FA_UPLOAD, "Push to remote")) g_notice = "Push is not wired yet";
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("branch", ICON_FA_CODE_BRANCH, "Create branch")) g_notice = "Branch creation is not wired yet";
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("stash", ICON_FA_BOX_ARCHIVE, "Stash changes")) g_notice = "Stash is not wired yet";
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("pop", ICON_FA_BOX_OPEN, "Pop latest stash")) g_notice = "Stash pop is not wired yet";
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("refresh", ICON_FA_ROTATE, "Refresh repository"))
|
||||
g_git_manager->reload(repo(), g_notice);
|
||||
ImGui::EndChild();
|
||||
|
||||
draw_sidebar(ui(g_sidebar_width));
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::InvisibleButton("##sidebar_splitter", {ui(6.0f), -ui(28.0f)});
|
||||
if (ImGui::IsItemHovered() || ImGui::IsItemActive()) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
|
||||
if (ImGui::IsItemActive()) {
|
||||
g_sidebar_width = std::clamp(g_sidebar_width + ImGui::GetIO().MouseDelta.x / g_ui_scale, 180.0f, 520.0f);
|
||||
}
|
||||
const ImVec2 splitter_min = ImGui::GetItemRectMin();
|
||||
const ImVec2 splitter_max = ImGui::GetItemRectMax();
|
||||
ImGui::GetWindowDrawList()->AddLine(
|
||||
{(splitter_min.x + splitter_max.x) * 0.5f, splitter_min.y},
|
||||
{(splitter_min.x + splitter_max.x) * 0.5f, splitter_max.y},
|
||||
ImGui::IsItemHovered() || ImGui::IsItemActive() ? IM_COL32(36, 178, 205, 255) : IM_COL32(62, 72, 88, 255),
|
||||
ui(1.0f));
|
||||
ImGui::SameLine(0, 0);
|
||||
const float detail_width = ui(368.0f);
|
||||
ImGui::BeginChild("center", {ImGui::GetContentRegionAvail().x - detail_width, -24.0f}, false);
|
||||
draw_commit_table();
|
||||
ImGui::EndChild();
|
||||
ImGui::SameLine();
|
||||
draw_details();
|
||||
|
||||
draw_footer();
|
||||
draw_popups();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int runGitree(int argc, char** argv) {
|
||||
GitManager git_manager;
|
||||
g_git_manager = &git_manager;
|
||||
UserData user_data;
|
||||
g_user_data = &user_data;
|
||||
g_sidebar_width = user_data.sidebarWidth();
|
||||
create_new_tab();
|
||||
|
||||
WindowManager window_manager;
|
||||
if (!window_manager.create("Gitree", 1500, 900)) return 1;
|
||||
g_window_manager = &window_manager;
|
||||
AvatarCache avatar_cache(user_data.directory());
|
||||
g_avatar_cache = &avatar_cache;
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
ImGui::GetIO().IniFilename = user_data.imguiIniPath().c_str();
|
||||
load_fonts(window_manager.dpiScale());
|
||||
ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
|
||||
ImGui_ImplOpenGL3_Init("#version 330");
|
||||
|
||||
if (argc > 1) open_repository(argv[1]);
|
||||
|
||||
while (!window_manager.shouldClose()) {
|
||||
window_manager.pollEvents();
|
||||
if (window_manager.consumeDpiChange()) load_fonts(window_manager.dpiScale());
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
avatar_cache.update();
|
||||
draw_app();
|
||||
ImGui::Render();
|
||||
int width = 0, height = 0;
|
||||
glfwGetFramebufferSize(window_manager.nativeWindow(), &width, &height);
|
||||
glViewport(0, 0, width, height);
|
||||
glClearColor(0.10f, 0.116f, 0.145f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
window_manager.swapBuffers();
|
||||
}
|
||||
|
||||
avatar_cache.shutdown();
|
||||
g_avatar_cache = nullptr;
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
user_data.setSidebarWidth(g_sidebar_width);
|
||||
user_data.save();
|
||||
ImGui::DestroyContext();
|
||||
g_window_manager = nullptr;
|
||||
g_tabs.clear();
|
||||
g_user_data = nullptr;
|
||||
g_git_manager = nullptr;
|
||||
return 0;
|
||||
}
|
||||
3
src/ui/gitree_ui.h
Normal file
3
src/ui/gitree_ui.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int runGitree(int argc, char** argv);
|
||||
Reference in New Issue
Block a user