feat(app): scaffold native Git client interface
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -32,3 +32,9 @@
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build output and editor state
|
||||
/build/
|
||||
/.vs/
|
||||
/.vscode/
|
||||
/imgui.ini
|
||||
|
||||
|
||||
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
[submodule "vendor/libgit2"]
|
||||
path = vendor/libgit2
|
||||
url = https://github.com/libgit2/libgit2.git
|
||||
[submodule "vendor/imgui"]
|
||||
path = vendor/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
[submodule "vendor/glfw"]
|
||||
path = vendor/glfw
|
||||
url = https://github.com/glfw/glfw.git
|
||||
[submodule "vendor/iZo"]
|
||||
path = vendor/iZo
|
||||
url = https://dock-it.dev/Idea-Studios/iZo
|
||||
72
CMakeLists.txt
Normal file
72
CMakeLists.txt
Normal file
@@ -0,0 +1,72 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Gitree VERSION 0.1.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build all dependencies as static libraries" FORCE)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
|
||||
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui/imgui.cpp")
|
||||
message(FATAL_ERROR "Vendor dependencies are missing. Run: git submodule update --init --recursive")
|
||||
endif()
|
||||
|
||||
# GLFW
|
||||
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/glfw EXCLUDE_FROM_ALL)
|
||||
|
||||
# Dear ImGui does not ship a CMake target, so keep its static-library recipe here.
|
||||
add_library(imgui STATIC
|
||||
vendor/imgui/imgui.cpp
|
||||
vendor/imgui/imgui_draw.cpp
|
||||
vendor/imgui/imgui_tables.cpp
|
||||
vendor/imgui/imgui_widgets.cpp
|
||||
vendor/imgui/backends/imgui_impl_glfw.cpp
|
||||
vendor/imgui/backends/imgui_impl_opengl3.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC vendor/imgui vendor/imgui/backends)
|
||||
target_link_libraries(imgui PUBLIC glfw)
|
||||
|
||||
# libgit2
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_FUZZERS OFF CACHE BOOL "" FORCE)
|
||||
set(USE_SSH OFF CACHE STRING "" FORCE)
|
||||
if(WIN32)
|
||||
set(USE_HTTPS WinHTTP CACHE STRING "" FORCE)
|
||||
endif()
|
||||
add_subdirectory(vendor/libgit2 EXCLUDE_FROM_ALL)
|
||||
|
||||
# Native platform file and folder dialogs.
|
||||
set(IZO_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/iZo EXCLUDE_FROM_ALL)
|
||||
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
add_executable(gitree WIN32
|
||||
src/main.cpp
|
||||
src/window_manager.cpp
|
||||
src/window_manager.h
|
||||
)
|
||||
target_include_directories(gitree PRIVATE 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}"
|
||||
GITREE_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/vendor/fonts"
|
||||
$<$<PLATFORM_ID:Windows>:NOMINMAX;WIN32_LEAN_AND_MEAN>
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(gitree PRIVATE dwmapi)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(gitree PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(gitree PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
29
README.md
29
README.md
@@ -1,3 +1,30 @@
|
||||
# Gitree
|
||||
|
||||
An ImGui Based Git GUI git client.
|
||||
A native Dear ImGui Git client built with libgit2 and GLFW.
|
||||
|
||||
## Build on Windows
|
||||
|
||||
Clone with submodules, then run `run.bat`. The script configures a Release build,
|
||||
builds Gitree and launches it. You can pass a repository path to open it directly:
|
||||
|
||||
```bat
|
||||
run.bat C:\path\to\repository
|
||||
```
|
||||
|
||||
Manual build:
|
||||
|
||||
```bat
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --parallel
|
||||
```
|
||||
|
||||
Vendored dependencies are pinned under `vendor/` and compiled as static libraries:
|
||||
|
||||
- libgit2 1.9.4
|
||||
- Dear ImGui 1.92.8 (docking branch)
|
||||
- GLFW 3.4
|
||||
- iZo 0.1.0
|
||||
|
||||
The bundled Inter variable font is licensed under the SIL Open Font License.
|
||||
Font Awesome Free icons are bundled from Attascii and merged into the ImGui font atlas.
|
||||
|
||||
28
run.bat
Normal file
28
run.bat
Normal file
@@ -0,0 +1,28 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist vendor\imgui\imgui.cpp (
|
||||
echo Initializing vendor dependencies...
|
||||
git submodule update --init --recursive || exit /b 1
|
||||
)
|
||||
|
||||
where ninja >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
set "GENERATOR=-G Ninja"
|
||||
) else (
|
||||
set "GENERATOR="
|
||||
)
|
||||
|
||||
cmake -S . -B build %GENERATOR% -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
||||
cmake --build build --config Release --parallel || exit /b 1
|
||||
|
||||
if exist build\bin\gitree.exe (
|
||||
start "" build\bin\gitree.exe %*
|
||||
) else if exist build\bin\Release\gitree.exe (
|
||||
start "" build\bin\Release\gitree.exe %*
|
||||
) else (
|
||||
echo Build succeeded, but gitree.exe was not found.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
779
src/main.cpp
Normal file
779
src/main.cpp
Normal file
@@ -0,0 +1,779 @@
|
||||
#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 "window_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
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;
|
||||
std::vector<RefBadge> refs;
|
||||
};
|
||||
|
||||
struct RepositoryView {
|
||||
git_repository* repo = nullptr;
|
||||
std::string path;
|
||||
std::string name = "No repository";
|
||||
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() { close(); }
|
||||
void close() {
|
||||
if (repo) git_repository_free(repo);
|
||||
repo = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
RepositoryView g_repo;
|
||||
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;
|
||||
|
||||
float ui(float value) { return value * g_ui_scale; }
|
||||
|
||||
std::string last_error(const char* fallback) {
|
||||
const git_error* error = git_error_last();
|
||||
return error && error->message ? error->message : fallback;
|
||||
}
|
||||
|
||||
std::string format_time(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 read_branches(git_branch_t type, std::vector<std::string>& output) {
|
||||
git_branch_iterator* iterator = nullptr;
|
||||
if (git_branch_iterator_new(&iterator, g_repo.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 add_badge(const git_oid* oid, RefBadge badge) {
|
||||
if (!oid) return;
|
||||
const auto commit = std::find_if(g_repo.commits.begin(), g_repo.commits.end(),
|
||||
[oid](const CommitInfo& item) { return git_oid_equal(&item.oid, oid) != 0; });
|
||||
if (commit != g_repo.commits.end()) commit->refs.push_back(std::move(badge));
|
||||
}
|
||||
|
||||
void load_ref_badges() {
|
||||
for (const git_branch_t type : {GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE}) {
|
||||
git_branch_iterator* iterator = nullptr;
|
||||
if (git_branch_iterator_new(&iterator, g_repo.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 == g_repo.branch;
|
||||
badge.worktree = type == GIT_BRANCH_LOCAL &&
|
||||
(badge.current || g_repo.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) {
|
||||
add_badge(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, g_repo.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* shorthand = git_reference_shorthand(reference);
|
||||
add_badge(git_object_id(object), {shorthand ? shorthand : "tag", RefKind::tag});
|
||||
git_object_free(object);
|
||||
}
|
||||
git_reference_free(reference);
|
||||
}
|
||||
git_reference_iterator_free(tags);
|
||||
}
|
||||
|
||||
int submodule_callback(git_submodule* submodule, const char*, void* payload) {
|
||||
auto* names = static_cast<std::vector<std::string>*>(payload);
|
||||
names->emplace_back(git_submodule_name(submodule));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void load_repository_data() {
|
||||
g_repo.local_branches.clear();
|
||||
g_repo.remote_branches.clear();
|
||||
g_repo.remotes.clear();
|
||||
g_repo.worktrees.clear();
|
||||
g_repo.worktree_branches.clear();
|
||||
g_repo.submodules.clear();
|
||||
g_repo.commits.clear();
|
||||
g_repo.selected_commit = 0;
|
||||
|
||||
const char* workdir = git_repository_workdir(g_repo.repo);
|
||||
const char* repo_path = workdir ? workdir : git_repository_path(g_repo.repo);
|
||||
g_repo.path = repo_path ? repo_path : "";
|
||||
std::filesystem::path p(g_repo.path);
|
||||
g_repo.name = p.filename().string();
|
||||
if (g_repo.name.empty()) g_repo.name = p.parent_path().filename().string();
|
||||
|
||||
git_reference* head = nullptr;
|
||||
if (git_repository_head(&head, g_repo.repo) == 0) {
|
||||
const char* shorthand = git_reference_shorthand(head);
|
||||
if (shorthand) g_repo.branch = shorthand;
|
||||
git_reference_free(head);
|
||||
}
|
||||
|
||||
read_branches(GIT_BRANCH_LOCAL, g_repo.local_branches);
|
||||
read_branches(GIT_BRANCH_REMOTE, g_repo.remote_branches);
|
||||
|
||||
git_strarray names{};
|
||||
if (git_remote_list(&names, g_repo.repo) == 0) {
|
||||
for (size_t i = 0; i < names.count; ++i) g_repo.remotes.emplace_back(names.strings[i]);
|
||||
git_strarray_dispose(&names);
|
||||
}
|
||||
if (git_worktree_list(&names, g_repo.repo) == 0) {
|
||||
for (size_t i = 0; i < names.count; ++i) {
|
||||
g_repo.worktrees.emplace_back(names.strings[i]);
|
||||
git_worktree* worktree = nullptr;
|
||||
git_repository* worktree_repo = nullptr;
|
||||
git_reference* worktree_head = nullptr;
|
||||
if (git_worktree_lookup(&worktree, g_repo.repo, names.strings[i]) == 0 &&
|
||||
git_repository_open_from_worktree(&worktree_repo, worktree) == 0 &&
|
||||
git_repository_head(&worktree_head, worktree_repo) == 0) {
|
||||
const char* branch = git_reference_shorthand(worktree_head);
|
||||
if (branch) g_repo.worktree_branches.emplace(branch);
|
||||
}
|
||||
git_reference_free(worktree_head);
|
||||
git_repository_free(worktree_repo);
|
||||
git_worktree_free(worktree);
|
||||
}
|
||||
git_strarray_dispose(&names);
|
||||
}
|
||||
git_submodule_foreach(g_repo.repo, submodule_callback, &g_repo.submodules);
|
||||
|
||||
git_revwalk* walk = nullptr;
|
||||
if (git_revwalk_new(&walk, g_repo.repo) != 0) return;
|
||||
git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
|
||||
if (git_revwalk_push_head(walk) != 0) {
|
||||
git_revwalk_free(walk);
|
||||
return;
|
||||
}
|
||||
git_oid oid{};
|
||||
while (g_repo.commits.size() < 250 && git_revwalk_next(&oid, walk) == 0) {
|
||||
git_commit* commit = nullptr;
|
||||
if (git_commit_lookup(&commit, g_repo.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);
|
||||
const char* summary = git_commit_summary(commit);
|
||||
item.summary = summary ? summary : "(no commit message)";
|
||||
const git_signature* author = git_commit_author(commit);
|
||||
if (author) {
|
||||
item.author = author->name ? author->name : "Unknown";
|
||||
item.email = author->email ? author->email : "";
|
||||
item.date = format_time(author->when.time, author->when.offset);
|
||||
}
|
||||
item.parents = static_cast<int>(git_commit_parentcount(commit));
|
||||
g_repo.commits.push_back(std::move(item));
|
||||
git_commit_free(commit);
|
||||
}
|
||||
git_revwalk_free(walk);
|
||||
load_ref_badges();
|
||||
}
|
||||
|
||||
bool open_repository(const char* path) {
|
||||
git_repository* opened = nullptr;
|
||||
if (git_repository_open_ext(&opened, path, 0, nullptr) != 0) {
|
||||
g_notice = last_error("Unable to open repository");
|
||||
return false;
|
||||
}
|
||||
g_repo.close();
|
||||
g_repo.repo = opened;
|
||||
load_repository_data();
|
||||
g_notice = "Opened " + g_repo.name;
|
||||
return true;
|
||||
}
|
||||
|
||||
void pick_and_open_repository() {
|
||||
izo::dialog_options options;
|
||||
options.title = "Open Git repository";
|
||||
if (!g_repo.path.empty()) options.initial_path = g_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());
|
||||
open_repository(path.c_str());
|
||||
}
|
||||
|
||||
bool init_repository(const char* path) {
|
||||
git_repository* created = nullptr;
|
||||
if (git_repository_init(&created, path, 0) != 0) {
|
||||
g_notice = last_error("Unable to initialize repository");
|
||||
return false;
|
||||
}
|
||||
git_repository_free(created);
|
||||
return open_repository(path);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void section(const char* label, const std::vector<std::string>& items) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
|
||||
bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 18.0f);
|
||||
ImGui::TextDisabled("%d", static_cast<int>(items.size()));
|
||||
ImGui::PopStyleColor();
|
||||
if (open) {
|
||||
for (const auto& item : items) ImGui::Selectable((" " + item).c_str());
|
||||
ImGui::TreePop();
|
||||
}
|
||||
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 std::string& id_path = {}) {
|
||||
for (const auto& [name, node] : parent.children) {
|
||||
const std::string id = id_path + "/" + name;
|
||||
if (!node.children.empty()) {
|
||||
const std::string label = std::string(ICON_FA_FOLDER) + " " + name + "##" + id;
|
||||
const bool open = ImGui::TreeNodeEx(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth);
|
||||
if (open) {
|
||||
if (node.branch) ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##branch" + id).c_str());
|
||||
draw_branch_nodes(node, id);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
} else {
|
||||
ImGui::Selectable((std::string(ICON_FA_CODE_BRANCH) + " " + name + "##" + id).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void branch_section(const char* label, const std::vector<std::string>& branches) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.60f, 0.67f, 0.76f, 1.0f));
|
||||
const bool open = ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ui(18.0f));
|
||||
ImGui::TextDisabled("%d", static_cast<int>(branches.size()));
|
||||
ImGui::PopStyleColor();
|
||||
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);
|
||||
}
|
||||
draw_branch_nodes(root, label);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
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("VIEWING %d", static_cast<int>(g_repo.local_branches.size() + g_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", g_repo.local_branches);
|
||||
branch_section(ICON_FA_CLOUD " REMOTE", g_repo.remote_branches);
|
||||
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", g_repo.worktrees);
|
||||
section(ICON_FA_CUBES " SUBMODULES", g_repo.submodules);
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void draw_ref_badge(const RefBadge& badge, int index) {
|
||||
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 color = badge.kind == RefKind::tag
|
||||
? ImVec4(0.42f, 0.22f, 0.62f, 1.0f)
|
||||
: badge.kind == RefKind::remote
|
||||
? ImVec4(0.10f, 0.31f, 0.35f, 1.0f)
|
||||
: ImVec4(0.08f, 0.40f, 0.50f, 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, int parents) {
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
const float x = p.x + ui(28.0f) + ((parents > 1 || row % 7 == 4) ? ui(18.0f) : 0.0f);
|
||||
const float y = p.y + ImGui::GetTextLineHeight() * 0.5f;
|
||||
const ImU32 cyan = IM_COL32(16, 178, 214, 255);
|
||||
draw->AddLine({x, p.y - ui(4.0f)}, {x, p.y + ImGui::GetTextLineHeight() + ui(5.0f)}, cyan, ui(2.0f));
|
||||
if (parents > 1) draw->AddBezierCubic({x, y}, {x + ui(28), y}, {x + ui(28), y + ui(16)}, {x + ui(28), y + ui(25)}, IM_COL32(153, 55, 220, 255), ui(2.0f));
|
||||
draw->AddCircleFilled({x, y}, ui(5.0f), row == 0 ? IM_COL32(240, 247, 250, 255) : cyan);
|
||||
draw->AddCircle({x, y}, ui(7.0f), cyan, 0, ui(2.0f));
|
||||
ImGui::Dummy({ui(78.0f), ImGui::GetTextLineHeight()});
|
||||
}
|
||||
|
||||
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(92.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>(g_repo.commits.size()); ++i) {
|
||||
const auto& commit = g_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();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
std::string row_id = "##commit" + std::to_string(i);
|
||||
if (ImGui::Selectable(row_id.c_str(), g_repo.selected_commit == i,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) g_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);
|
||||
}
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
draw_graph_cell(i, commit.parents);
|
||||
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 (g_repo.commits.empty()) {
|
||||
ImGui::TextDisabled("Select a repository with commits");
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
const auto& commit = g_repo.commits[std::clamp(g_repo.selected_commit, 0, static_cast<int>(g_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();
|
||||
}
|
||||
|
||||
void toolbar_button(const char* label, const char* notice) {
|
||||
if (ImGui::Button(label, {ui(68), ui(34)})) g_notice = notice;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
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_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();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("repositories", ImGuiTabBarFlags_AutoSelectNewTabs)) {
|
||||
if (ImGui::BeginTabItem(g_repo.name.c_str())) { ImGui::EndTabItem(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+")) pick_and_open_repository();
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
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", g_repo.name.c_str());
|
||||
ImGui::SameLine(ui(145));
|
||||
ImGui::SetNextItemWidth(ui(180));
|
||||
if (ImGui::BeginCombo("##branch", g_repo.branch.c_str())) {
|
||||
for (const auto& branch : g_repo.local_branches) ImGui::Selectable(branch.c_str(), branch == g_repo.branch);
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine(ui(350));
|
||||
toolbar_button(ICON_FA_DOWNLOAD " Pull", "Pull is not wired yet");
|
||||
toolbar_button(ICON_FA_UPLOAD " Push", "Push is not wired yet");
|
||||
toolbar_button(ICON_FA_CODE_BRANCH " Branch", "Branch creation is not wired yet");
|
||||
toolbar_button(ICON_FA_BOX_ARCHIVE " Stash", "Stash is not wired yet");
|
||||
toolbar_button(ICON_FA_BOX_OPEN " Pop", "Stash pop is not wired yet");
|
||||
if (ImGui::Button(ICON_FA_ROTATE " Refresh", {ui(82), ui(34)}) && g_repo.repo) load_repository_data();
|
||||
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();
|
||||
|
||||
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 - 18.0f);
|
||||
ImGui::TextDisabled("%s", version);
|
||||
draw_popups();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
git_libgit2_init();
|
||||
WindowManager window_manager;
|
||||
if (!window_manager.create("Gitree", 1500, 900)) return 1;
|
||||
g_window_manager = &window_manager;
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
load_fonts(window_manager.dpiScale());
|
||||
ImGui_ImplGlfw_InitForOpenGL(window_manager.nativeWindow(), true);
|
||||
ImGui_ImplOpenGL3_Init("#version 330");
|
||||
|
||||
if (argc > 1) open_repository(argv[1]);
|
||||
else open_repository(".");
|
||||
|
||||
while (!window_manager.shouldClose()) {
|
||||
window_manager.pollEvents();
|
||||
if (window_manager.consumeDpiChange()) load_fonts(window_manager.dpiScale());
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
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();
|
||||
}
|
||||
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
g_window_manager = nullptr;
|
||||
g_repo.close();
|
||||
git_libgit2_shutdown();
|
||||
return 0;
|
||||
}
|
||||
93
src/window_manager.cpp
Normal file
93
src/window_manager.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "window_manager.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
#include <GLFW/glfw3native.h>
|
||||
#include <dwmapi.h>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
WindowManager::~WindowManager() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
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"));
|
||||
if (set_dpi_awareness) set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
|
||||
#endif
|
||||
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_) {
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(window_);
|
||||
glfwSwapInterval(1);
|
||||
glfwSetWindowUserPointer(window_, this);
|
||||
glfwSetWindowContentScaleCallback(window_, contentScaleCallback);
|
||||
float x_scale = 1.0f;
|
||||
float y_scale = 1.0f;
|
||||
glfwGetWindowContentScale(window_, &x_scale, &y_scale);
|
||||
dpi_scale_ = std::max(x_scale, y_scale);
|
||||
applyNativeCaption();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowManager::destroy() {
|
||||
if (!window_) return;
|
||||
glfwDestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
glfwTerminate();
|
||||
}
|
||||
|
||||
void WindowManager::pollEvents() { glfwPollEvents(); }
|
||||
void WindowManager::swapBuffers() { glfwSwapBuffers(window_); }
|
||||
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
||||
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }
|
||||
|
||||
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::updateDpi(float scale) {
|
||||
scale = std::clamp(scale, 1.0f, 4.0f);
|
||||
if (scale == dpi_scale_) return;
|
||||
dpi_scale_ = scale;
|
||||
dpi_changed_ = true;
|
||||
applyNativeCaption();
|
||||
}
|
||||
|
||||
void WindowManager::applyNativeCaption() const {
|
||||
#ifdef _WIN32
|
||||
const HWND hwnd = glfwGetWin32Window(window_);
|
||||
const BOOL dark = TRUE;
|
||||
DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark)); // DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
|
||||
// Windows 11 caption customization. Older versions safely ignore these.
|
||||
const COLORREF caption = RGB(31, 36, 47);
|
||||
const COLORREF border = RGB(48, 56, 70);
|
||||
const COLORREF text = RGB(226, 231, 239);
|
||||
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
|
||||
#endif
|
||||
}
|
||||
31
src/window_manager.h
Normal file
31
src/window_manager.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
struct GLFWwindow;
|
||||
|
||||
class WindowManager {
|
||||
public:
|
||||
WindowManager() = default;
|
||||
~WindowManager();
|
||||
|
||||
WindowManager(const WindowManager&) = delete;
|
||||
WindowManager& operator=(const WindowManager&) = delete;
|
||||
|
||||
bool create(const char* title, int width, int height);
|
||||
void destroy();
|
||||
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();
|
||||
|
||||
private:
|
||||
static void contentScaleCallback(GLFWwindow* window, float x_scale, float y_scale);
|
||||
void updateDpi(float scale);
|
||||
void applyNativeCaption() const;
|
||||
|
||||
GLFWwindow* window_ = nullptr;
|
||||
float dpi_scale_ = 1.0f;
|
||||
bool dpi_changed_ = false;
|
||||
};
|
||||
BIN
vendor/fonts/InterVariable.ttf
vendored
Normal file
BIN
vendor/fonts/InterVariable.ttf
vendored
Normal file
Binary file not shown.
92
vendor/fonts/LICENSE-Inter.txt
vendored
Normal file
92
vendor/fonts/LICENSE-Inter.txt
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
vendor/fonts/fa-solid-900.ttf
vendored
Normal file
BIN
vendor/fonts/fa-solid-900.ttf
vendored
Normal file
Binary file not shown.
1
vendor/glfw
vendored
Submodule
1
vendor/glfw
vendored
Submodule
Submodule vendor/glfw added at 7b6aead9fb
1
vendor/iZo
vendored
Submodule
1
vendor/iZo
vendored
Submodule
Submodule vendor/iZo added at 88eb3ced5b
1416
vendor/icons/IconsFontAwesome6.h
vendored
Normal file
1416
vendor/icons/IconsFontAwesome6.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
vendor/imgui
vendored
Submodule
1
vendor/imgui
vendored
Submodule
Submodule vendor/imgui added at b61e56346a
1
vendor/libgit2
vendored
Submodule
1
vendor/libgit2
vendored
Submodule
Submodule vendor/libgit2 added at f7164261c9
Reference in New Issue
Block a user