feat(sidebar): persist resizable section layout

This commit is contained in:
2026-06-18 18:50:25 -05:00
parent dad4d59c7f
commit 5cd1578dc9
3 changed files with 67 additions and 18 deletions

View File

@@ -32,9 +32,16 @@ void UserData::load() {
while (settings >> key) {
if (key == "sidebar_width") settings >> sidebar_width_;
else if (key == "pull_mode") settings >> pull_mode_;
else if (key.rfind("sidebar_section_", 0) == 0) {
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
float height = 0.0f;
settings >> height;
if (index < sidebar_section_heights_.size()) sidebar_section_heights_[index] = height;
}
}
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
pull_mode_ = std::clamp(pull_mode_, 0, 3);
for (float& height : sidebar_section_heights_) height = std::clamp(height, 42.0f, 500.0f);
std::ifstream history(directory_ / "history.txt");
std::string path;
@@ -57,6 +64,8 @@ void UserData::save() const {
std::ofstream settings(directory_ / "settings.ini", std::ios::trunc);
settings << "sidebar_width " << sidebar_width_ << '\n';
settings << "pull_mode " << pull_mode_ << '\n';
for (size_t index = 0; index < sidebar_section_heights_.size(); ++index)
settings << "sidebar_section_" << index << ' ' << sidebar_section_heights_[index] << '\n';
std::ofstream history(directory_ / "history.txt", std::ios::trunc);
for (const auto& path : recently_closed_) history << std::quoted(path) << '\n';

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <filesystem>
#include <string>
#include <vector>
@@ -13,9 +14,11 @@ public:
[[nodiscard]] const std::string& imguiIniPath() const { return imgui_ini_path_; }
[[nodiscard]] const std::vector<std::string>& recentlyClosed() const { return recently_closed_; }
[[nodiscard]] float sidebarWidth() const { return sidebar_width_; }
[[nodiscard]] float sidebarSectionHeight(size_t index) const { return sidebar_section_heights_.at(index); }
[[nodiscard]] int pullMode() const { return pull_mode_; }
void setSidebarWidth(float width) { sidebar_width_ = width; }
void setSidebarSectionHeight(size_t index, float height) { sidebar_section_heights_.at(index) = height; }
void setPullMode(int mode) { pull_mode_ = mode; save(); }
void addRecentlyClosed(const std::string& path);
void save() const;
@@ -27,5 +30,6 @@ private:
std::string imgui_ini_path_;
std::vector<std::string> recently_closed_;
float sidebar_width_ = 230.0f;
std::array<float, 4> sidebar_section_heights_ = {110.0f, 220.0f, 90.0f, 150.0f};
int pull_mode_ = 1;
};

View File

@@ -173,25 +173,54 @@ bool sidebar_collapse_row(const char* id, const std::string& label, bool default
}
bool sidebar_section_header(const char* label, int count, const char* add_tooltip, const char* add_notice) {
const ImVec2 header_min = ImGui::GetCursorScreenPos();
const float header_width = ImGui::GetContentRegionAvail().x;
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();
const ImVec2 mouse = ImGui::GetIO().MousePos;
const bool header_hovered = ImGui::IsWindowHovered() && mouse.x >= header_min.x &&
mouse.x <= header_min.x + header_width && mouse.y >= header_min.y &&
mouse.y <= ImGui::GetItemRectMax().y;
if (header_hovered) {
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 sidebar_section_splitter(const char* id, size_t section_index) {
ImGui::PushID(id);
ImGui::InvisibleButton("##vertical_resize", {-1, ui(5.0f)});
const bool active = ImGui::IsItemActive();
const bool hovered = ImGui::IsItemHovered();
if (active || hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
if (active) {
const float height = std::clamp(g_user_data->sidebarSectionHeight(section_index) +
ImGui::GetIO().MouseDelta.y / g_ui_scale, 42.0f, 500.0f);
g_user_data->setSidebarSectionHeight(section_index, height);
}
if (ImGui::IsItemDeactivated()) g_user_data->save();
const ImVec2 minimum = ImGui::GetItemRectMin();
const ImVec2 maximum = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddLine(
{minimum.x, (minimum.y + maximum.y) * 0.5f},
{maximum.x, (minimum.y + maximum.y) * 0.5f},
active || hovered ? IM_COL32(36, 178, 205, 255) : IM_COL32(62, 72, 88, 255));
ImGui::PopID();
}
enum class SidebarItemKind { worktree, submodule };
void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
@@ -211,16 +240,19 @@ void sidebar_item_context(const std::string& item, SidebarItemKind kind) {
}
void section(const char* label, const std::vector<std::string>& items, const char* item_icon,
const char* add_tooltip, const char* add_notice, SidebarItemKind kind) {
const char* add_tooltip, const char* add_notice, SidebarItemKind kind, size_t section_index) {
const bool open = sidebar_section_header(label, static_cast<int>(items.size()), add_tooltip, add_notice);
if (open) {
ImGui::BeginChild((std::string(label) + "##body").c_str(),
{-1, ui(g_user_data->sidebarSectionHeight(section_index))});
for (const auto& item : items) {
ImGui::Selectable((std::string(" ") + item_icon + " " + item).c_str());
sidebar_item_context(item, kind);
}
ImGui::EndChild();
sidebar_section_splitter(label, section_index);
}
ImGui::Separator();
ImGui::Dummy({0, ui(2.0f)});
}
struct BranchNode {
@@ -277,9 +309,12 @@ void draw_branch_nodes(const BranchNode& parent, const char* branch_icon, const
}
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, bool remote) {
const char* group_icon, const char* add_tooltip, const char* add_notice, bool remote,
size_t section_index) {
const bool open = sidebar_section_header(label, static_cast<int>(branches.size()), add_tooltip, add_notice);
if (open) {
ImGui::BeginChild((std::string(label) + "##body").c_str(),
{-1, ui(g_user_data->sidebarSectionHeight(section_index))});
BranchNode root;
for (const auto& branch : branches) {
if (g_filter[0] && branch.find(g_filter.data()) == std::string::npos) continue;
@@ -288,9 +323,10 @@ void branch_section(const char* label, const std::vector<std::string>& branches,
ImGui::Indent(ui(17.0f));
draw_branch_nodes(root, branch_icon, group_icon, remote, label);
ImGui::Unindent(ui(17.0f));
ImGui::EndChild();
sidebar_section_splitter(label, section_index);
}
ImGui::Separator();
ImGui::Dummy({0, ui(2.0f)});
}
void draw_sidebar(float width) {
@@ -303,13 +339,13 @@ void draw_sidebar(float width) {
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", false);
"Create local branch", "Branch creation is not wired yet", false, 0);
branch_section(ICON_FA_CLOUD " REMOTE", repo().remote_branches, ICON_FA_CODE_BRANCH, ICON_FA_GLOBE,
"Add remote", "Remote creation is not wired yet", true);
"Add remote", "Remote creation is not wired yet", true, 1);
section(ICON_FA_DIAGRAM_PROJECT " WORKTREES", repo().worktrees, ICON_FA_COMPUTER,
"Add worktree", "Worktree creation is not wired yet", SidebarItemKind::worktree);
"Add worktree", "Worktree creation is not wired yet", SidebarItemKind::worktree, 2);
section(ICON_FA_CUBES " SUBMODULES", repo().submodules, ICON_FA_CUBES,
"Add submodule", "Submodule creation is not wired yet", SidebarItemKind::submodule);
"Add submodule", "Submodule creation is not wired yet", SidebarItemKind::submodule, 3);
ImGui::EndChild();
}