feat(image-picker): smooth scroll, inset scrollbar, 2-column folder grid

- Smooth (lerped) wheel scrolling via ApplySmoothScroll on the list, matching
  the app's other modal lists.
- The list is now a bordered/rounded outer frame whose 6px padding insets the
  scrollbar so it clears the card's rounded corners; the inner scroll child is
  transparent (the frame draws the single background — no more box-in-a-box
  from the ChildBg being left on the stack across both BeginChild calls).
- Folders render as a 2-column grid of thin rounded rectangles (folder icon +
  name) instead of full-width rows, so more folders are visible at a glance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:31:48 -05:00
parent bc3c6037fc
commit a53b13b6b6

View File

@@ -153,8 +153,19 @@ public:
float listH = ImGui::GetContentRegionAvail().y - footerBlockH;
listH = std::max(listH, 160.0f * dp);
const float fullW = ImGui::GetContentRegionAvail().x;
// Bordered/rounded outer frame whose 6px padding insets the inner scrollbar so it clears the
// card's rounded corners; the inner child scrolls smoothly (ApplySmoothScroll lerps the wheel).
ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 20));
ImGui::BeginChild("##imgList", ImVec2(fullW, listH), true);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f * dp, 6.0f * dp));
ImGui::BeginChild("##imgListFrame", ImVec2(fullW, listH), true,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2); // ChildRounding + outer WindowPadding (captured by the frame child)
ImGui::PopStyleColor(); // ChildBg — the frame already drew it; the inner child stays transparent
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp);
ImGui::BeginChild("##imgList", ImVec2(0, 0), false, ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const float padX = Layout::spacingSm();
@@ -165,27 +176,35 @@ public:
ImGui::Unindent(padX);
}
// Folder rows (compact, clickable to descend).
{
const float rowH = rowFont->LegacySize + Layout::spacingSm();
const float rowW = ImGui::GetContentRegionAvail().x;
// Folders: a 2-column grid of thin rounded rectangles (folder icon + name), clickable to descend.
if (!s_subdirs.empty()) {
const int cols = 2;
const float cellGap = Layout::spacingSm();
const float availW = ImGui::GetContentRegionAvail().x;
const float cellW = (availW - cellGap * (cols - 1)) / (float)cols;
const float cellH = rowFont->LegacySize + Layout::spacingSm() * 1.5f;
int col = 0;
for (std::size_t i = 0; i < s_subdirs.size(); ++i) {
ImVec2 rMin = ImGui::GetCursorScreenPos();
ImVec2 rMax(rMin.x + rowW, rMin.y + rowH);
if (col != 0) ImGui::SameLine(0, cellGap);
ImVec2 mn = ImGui::GetCursorScreenPos();
ImVec2 mx(mn.x + cellW, mn.y + cellH);
ImGui::PushID((int)(i + 1));
const bool clicked = ImGui::InvisibleButton("##idir", ImVec2(rowW, rowH));
const bool clicked = ImGui::InvisibleButton("##idir", ImVec2(cellW, cellH));
const bool hov = ImGui::IsItemHovered();
ImGui::PopID();
if (hov) { dl->AddRectFilled(rMin, rMax, WithAlpha(OnSurface(), 22), 6.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
const float midY = (rMin.y + rMax.y) * 0.5f;
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), hov ? 34 : 16), 6.0f * dp);
if (hov) { dl->AddRect(mn, mx, WithAlpha(OnSurface(), 70), 6.0f * dp, 0, 1.0f); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
const float midY = (mn.y + mx.y) * 0.5f;
const float icoSz = rowFont->LegacySize * 1.05f;
float x = rMin.x + padX;
float x = mn.x + padX;
dl->AddText(icoFont, icoSz, ImVec2(x, midY - icoSz * 0.5f), hov ? Primary() : OnSurfaceMedium(), ICON_MD_FOLDER);
x += icoSz + Layout::spacingSm();
std::string nm = fit(s_subdirs[i], rowFont, (rMax.x - padX) - x);
std::string nm = fit(s_subdirs[i], rowFont, (mx.x - padX) - x);
dl->AddText(rowFont, rowFont->LegacySize, ImVec2(x, midY - rowFont->LegacySize * 0.5f), OnSurface(), nm.c_str());
if (clicked) pendingNav = (std::filesystem::path(s_dir) / s_subdirs[i]).string();
col = (col + 1) % cols;
}
if (!s_images.empty()) ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // gap before the thumbnails
}
// Thumbnail grid.
@@ -233,8 +252,9 @@ public:
}
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::EndChild(); // inner scrolling list
ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding
ImGui::EndChild(); // outer frame
// ---- Status ----------------------------------------------------------------------------
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));