feat(market): portfolio groups as a draggable/resizable dashboard grid
Turn the portfolio group area into a dashboard grid: - Responsive column count (1-4 by width); each 1x1 cell is one card. - Drag a card to move it to another cell; drag its bottom-right corner to resize (span multiple cells). Both snap to the grid. - A subtle dot grid appears while rearranging (dragging or resizing), with a highlighted drop-target / resize footprint. - Click (no drag past a 4px threshold) still opens the group editor. - Grid placement (col/row) and span (w/h) persist per group in settings.json; positions are resolved each frame — stored placement when it fits, else auto-placed row-major into free cells. Dropping onto an occupied cell swaps. PortfolioEntry gains grid_col/grid_row/grid_w/grid_h. The card render was refactored into a drawCard() lambda reused for the in-grid, dragged, and resizing states. Full-node + Lite build clean; ctest 1/1; hygiene clean. This is an interactive feature I can't verify without the GUI — needs drag/resize testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -332,6 +332,14 @@ bool Settings::load(const std::string& path)
|
||||
entry.show24h = e["show_24h"].get<bool>();
|
||||
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
entry.gridRow = e["grid_row"].get<int>();
|
||||
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
|
||||
entry.gridW = e["grid_w"].get<int>();
|
||||
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
|
||||
entry.gridH = e["grid_h"].get<int>();
|
||||
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
@@ -490,6 +498,10 @@ bool Settings::save(const std::string& path)
|
||||
entry["show_value"] = e.showValue;
|
||||
entry["show_24h"] = e.show24h;
|
||||
entry["show_sparkline"] = e.showSparkline;
|
||||
entry["grid_col"] = e.gridCol;
|
||||
entry["grid_row"] = e.gridRow;
|
||||
entry["grid_w"] = e.gridW;
|
||||
entry["grid_h"] = e.gridH;
|
||||
j["portfolio_entries"].push_back(std::move(entry));
|
||||
}
|
||||
j["font_scale"] = font_scale_;
|
||||
|
||||
@@ -90,6 +90,12 @@ public:
|
||||
bool showValue = true; // show the converted/fiat value on the card
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
// Dashboard grid placement on the Market portfolio. col/row in grid cells (-1 = auto-place),
|
||||
// w/h in cells (span). Set when the user drags/resizes a card.
|
||||
int gridCol = -1;
|
||||
int gridRow = -1;
|
||||
int gridW = 1;
|
||||
int gridH = 1;
|
||||
};
|
||||
|
||||
// Theme
|
||||
|
||||
@@ -131,6 +131,16 @@ static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
dl->AddPolyline(pts.data(), n, col, ImDrawFlags_None, 1.2f);
|
||||
}
|
||||
|
||||
// Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize).
|
||||
struct PfCell { int col, row, w, h; };
|
||||
static int s_pf_drag = -1; // card index being dragged (-1 = none)
|
||||
static int s_pf_resize = -1; // card index being resized
|
||||
static bool s_pf_moved = false; // drag passed the click threshold this gesture
|
||||
static ImVec2 s_pf_press; // mouse pos at grab
|
||||
static ImVec2 s_pf_grab; // mouse offset within the card at grab
|
||||
static PfCell s_pf_old = {0,0,1,1};// dragged card's cell at grab (for swap-on-drop)
|
||||
static PfCell s_pf_target = {0,0,1,1}; // live drop target cell
|
||||
|
||||
// Build the right-hand amount strings for a group card / preview from its price-data config.
|
||||
struct PfDisplay {
|
||||
std::string primary; // name-row, top-right (DRGX amount or the value)
|
||||
@@ -757,15 +767,47 @@ void RenderMarketTab(App* app)
|
||||
int pfN = (int)pfEntriesGeo.size();
|
||||
float pfCardGap = Layout::spacingSm();
|
||||
float pfCardPad = Layout::spacingMd(); // inner padding of each group card
|
||||
float pfCardW = std::min(280.0f * mktDp, availWidth); // max card width (shrinks on a narrow tab)
|
||||
int pfCardsPerRow = std::max(1, (int)((availWidth + pfCardGap) / (pfCardW + pfCardGap)));
|
||||
if (pfN > 0 && pfCardsPerRow > pfN) pfCardsPerRow = pfN;
|
||||
// Card: a name row (sub1) + a fiat row (caption); the DRGX/fiat amounts sit top-right.
|
||||
float pfCardH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize;
|
||||
int pfRows = (pfN > 0) ? (pfN + pfCardsPerRow - 1) / pfCardsPerRow : 0;
|
||||
float pfGroupsH = (pfN > 0)
|
||||
? (pfRows * pfCardH + std::max(0, pfRows - 1) * pfCardGap)
|
||||
: 0.0f;
|
||||
// Dashboard grid: responsive column count; a 1x1 cell is one card. Cards can be dragged to a
|
||||
// new cell and corner-resized to span multiple cells.
|
||||
int pfCols = std::max(1, std::min(4, (int)(availWidth / (240.0f * mktDp) + 0.5f)));
|
||||
float pfCellW = (availWidth - (pfCols - 1) * pfCardGap) / (float)pfCols;
|
||||
// A 1x1 cell: name row (sub1) + a fiat row (caption) + padding; the amounts sit top-right.
|
||||
float pfCellH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize;
|
||||
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
|
||||
// row-major into the first free cells. Deterministic each frame.
|
||||
std::vector<PfCell> pfLayout((size_t)std::max(0, pfN));
|
||||
{
|
||||
std::vector<std::vector<char>> occ;
|
||||
auto ensure = [&](int rEnd) { while ((int)occ.size() < rEnd) occ.emplace_back(pfCols, 0); };
|
||||
auto fits = [&](int c, int r, int w, int h) -> bool {
|
||||
if (c < 0 || r < 0 || c + w > pfCols) return false;
|
||||
ensure(r + h);
|
||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) if (occ[rr][cc]) return false;
|
||||
return true;
|
||||
};
|
||||
auto mark = [&](int c, int r, int w, int h) { ensure(r + h);
|
||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) occ[rr][cc] = 1; };
|
||||
std::vector<char> placed((size_t)pfN, 0);
|
||||
for (int i = 0; i < pfN; i++) {
|
||||
const auto& e = pfEntriesGeo[i];
|
||||
int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
||||
if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) {
|
||||
pfLayout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < pfN; i++) {
|
||||
if (placed[i]) continue;
|
||||
const auto& e = pfEntriesGeo[i];
|
||||
int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
||||
int fc = 0, fr = 0; bool found = false;
|
||||
for (int r = 0; !found && r < 4096; r++)
|
||||
for (int c = 0; c + w <= pfCols; c++)
|
||||
if (fits(c, r, w, h)) { fc = c; fr = r; found = true; break; }
|
||||
pfLayout[i] = {fc, fr, w, h}; mark(fc, fr, w, h);
|
||||
}
|
||||
}
|
||||
int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h);
|
||||
float pfGroupsH = (pfN > 0) ? (pfMaxRow * (pfCellH + pfCardGap) - pfCardGap) : 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
// ================================================================
|
||||
@@ -1307,48 +1349,43 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(cx, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// ---- CUSTOM GROUPS — floating glass cards below the summary (no heading) ----
|
||||
// ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize.
|
||||
// A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ----
|
||||
if (pfN > 0) {
|
||||
float gy = cardMin.y + pfSummaryH;
|
||||
for (int i = 0; i < pfN; i++) {
|
||||
const auto& e = pfEntriesGeo[i];
|
||||
int col = i % pfCardsPerRow;
|
||||
int row = i / pfCardsPerRow;
|
||||
ImVec2 gcMin(cx + col * (pfCardW + pfCardGap), gy + row * (pfCardH + pfCardGap));
|
||||
ImVec2 gcMax(gcMin.x + pfCardW, gcMin.y + pfCardH);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
bool hov = material::IsRectHovered(gcMin, gcMax);
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
bool interacting = (s_pf_drag >= 0 || s_pf_resize >= 0);
|
||||
float rh = 14.0f * mktDp; // corner resize-grip size
|
||||
|
||||
GlassPanelSpec gcGlass;
|
||||
gcGlass.rounding = 10.0f;
|
||||
gcGlass.fillAlpha = hov ? 34 : 22;
|
||||
gcGlass.borderAlpha = 40;
|
||||
auto cellMin = [&](const PfCell& L) {
|
||||
return ImVec2(cx + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap));
|
||||
};
|
||||
auto cellSize = [&](int w, int h) {
|
||||
return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap);
|
||||
};
|
||||
|
||||
// Draw one card's content into [gcMin, gcMax].
|
||||
auto drawCard = [&](ImVec2 gcMin, ImVec2 gcMax, const config::Settings::PortfolioEntry& e, bool hov) {
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
float ch = gcMax.y - gcMin.y;
|
||||
GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f;
|
||||
gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40;
|
||||
DrawGlassPanel(dl, gcMin, gcMax, gcGlass);
|
||||
if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||
|
||||
// Left accent bar in the group's color (if one is set).
|
||||
if (accent) {
|
||||
float bx = gcMin.x + 2.0f;
|
||||
dl->AddRectFilled(ImVec2(bx, gcMin.y + pfCardPad * 0.6f),
|
||||
ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f);
|
||||
}
|
||||
|
||||
// Price-trend sparkline (faint, in the lower band, behind the text) for
|
||||
// live-market groups. The group's value tracks the DRGX price, so plot its history.
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1) &&
|
||||
market.price_history.size() >= 2) {
|
||||
const auto& h = market.price_history;
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + pfCardH * 0.46f);
|
||||
ImVec2 spMax(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f);
|
||||
pfDrawSparkline(dl, spMin, spMax, h, spCol);
|
||||
const auto& hh = market.price_history;
|
||||
ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80);
|
||||
pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f),
|
||||
ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol);
|
||||
}
|
||||
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
float rowY = gcMin.y + pfCardPad;
|
||||
float gcRight = gcMax.x - pfCardPad;
|
||||
|
||||
// Amounts, top-right corner, per the group's price-data config.
|
||||
float rowY = gcMin.y + pfCardPad, gcRight = gcMax.x - pfCardPad;
|
||||
PfDisplay disp = pfBuildDisplay(e, bal, market);
|
||||
float primaryW = disp.primary.empty() ? 0.0f
|
||||
: body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, disp.primary.c_str()).x;
|
||||
@@ -1357,7 +1394,6 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(gcRight - primaryW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f),
|
||||
OnSurface(), disp.primary.c_str());
|
||||
{
|
||||
// Second line: the value (when DRGX is primary) and/or the 24h change.
|
||||
float belowY = rowY + sub1->LegacySize + Layout::spacingXs();
|
||||
float cursorR = gcRight;
|
||||
if (!disp.change.empty()) {
|
||||
@@ -1370,37 +1406,139 @@ void RenderMarketTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), disp.secondary.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Icon (left) then the group name.
|
||||
float nameX = gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f);
|
||||
if (!e.icon.empty()) {
|
||||
ImFont* gIcon = Type().iconSmall();
|
||||
float gIconSz = sub1->LegacySize;
|
||||
material::project_icons::drawByName(
|
||||
dl, e.icon.c_str(), ImVec2(nameX + gIconSz * 0.5f, rowY + sub1->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurface(), gIcon, gIconSz);
|
||||
nameX += gIconSz + Layout::spacingSm();
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(),
|
||||
ImVec2(nameX + sub1->LegacySize * 0.5f, rowY + sub1->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurface(), Type().iconSmall(), sub1->LegacySize);
|
||||
nameX += sub1->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
|
||||
// Group name, larger — truncated so it never overlaps the amount.
|
||||
float nameMaxW = (gcRight - primaryW - Layout::spacingMd()) - nameX;
|
||||
std::string label = e.label;
|
||||
bool truncated = false;
|
||||
while (label.size() > 1 &&
|
||||
sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, label.c_str()).x > nameMaxW) {
|
||||
label.pop_back();
|
||||
truncated = true;
|
||||
std::string label = e.label; bool tr2 = false;
|
||||
while (label.size() > 1 && sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, label.c_str()).x > nameMaxW) {
|
||||
label.pop_back(); tr2 = true;
|
||||
}
|
||||
if (truncated) label += "\xE2\x80\xA6";
|
||||
if (tr2) label += "\xE2\x80\xA6";
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, rowY), OnSurface(), label.c_str());
|
||||
};
|
||||
|
||||
// Whole card is clickable → open the editor for this group.
|
||||
ImGui::PushID(3000 + i);
|
||||
ImGui::SetCursorScreenPos(gcMin);
|
||||
ImGui::InvisibleButton("##pfCard", ImVec2(pfCardW, pfCardH));
|
||||
if (ImGui::IsItemClicked()) { PortfolioBeginEdit(app, i); s_portfolio_editor_open = true; }
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
// Subtle dot grid while rearranging.
|
||||
if (interacting) {
|
||||
ImU32 dotc = WithAlpha(OnSurface(), 55);
|
||||
for (int r = 0; r <= pfMaxRow + 1; r++)
|
||||
for (int c = 0; c <= pfCols; c++)
|
||||
dl->AddCircleFilled(ImVec2(cx + c * (pfCellW + pfCardGap) - pfCardGap * 0.5f,
|
||||
gy + r * (pfCellH + pfCardGap) - pfCardGap * 0.5f),
|
||||
1.5f * mktDp, dotc);
|
||||
}
|
||||
|
||||
// Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below).
|
||||
for (int i = 0; i < pfN; i++) {
|
||||
ImVec2 gcMin = cellMin(pfLayout[i]);
|
||||
ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h);
|
||||
ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y);
|
||||
|
||||
if (i == s_pf_drag && s_pf_moved) {
|
||||
dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost
|
||||
continue;
|
||||
}
|
||||
if (i == s_pf_resize) continue;
|
||||
|
||||
bool hov = !interacting && material::IsRectHovered(gcMin, gcMax);
|
||||
drawCard(gcMin, gcMax, pfEntriesGeo[i], hov);
|
||||
|
||||
// Resize grip (bottom-right).
|
||||
bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax);
|
||||
ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80);
|
||||
for (int k = 1; k <= 2; k++) {
|
||||
float o = k * 4.0f * mktDp;
|
||||
dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp);
|
||||
}
|
||||
|
||||
// Start a gesture only when idle. One body button; the press location (corner vs body)
|
||||
// decides resize vs move — avoids overlapping-item hit-test issues.
|
||||
if (s_pf_drag < 0 && s_pf_resize < 0) {
|
||||
ImGui::PushID(3000 + i);
|
||||
ImGui::SetCursorScreenPos(gcMin);
|
||||
ImGui::InvisibleButton("##pfBody", sz);
|
||||
bool bodyAct = ImGui::IsItemActivated();
|
||||
bool bodyHov = ImGui::IsItemHovered();
|
||||
ImGui::PopID();
|
||||
if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand);
|
||||
if (bodyAct) {
|
||||
if (gripHov) { s_pf_resize = i; s_pf_old = pfLayout[i]; }
|
||||
else {
|
||||
s_pf_drag = i; s_pf_moved = false; s_pf_old = pfLayout[i];
|
||||
s_pf_press = io.MousePos;
|
||||
s_pf_grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pfPersist = [&](const std::vector<config::Settings::PortfolioEntry>& v) {
|
||||
app->settings()->setPortfolioEntries(v); app->settings()->save();
|
||||
};
|
||||
|
||||
// ----- Resize in progress (corner drag) -----
|
||||
if (s_pf_resize >= 0 && s_pf_resize < pfN) {
|
||||
int i = s_pf_resize;
|
||||
ImVec2 mn = cellMin(pfLayout[i]);
|
||||
int nw = std::max(1, std::min(pfCols - pfLayout[i].col,
|
||||
(int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f)));
|
||||
int nh = std::max(1, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f));
|
||||
ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y);
|
||||
drawCard(mn, mx, pfEntriesGeo[i], true);
|
||||
dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
|
||||
if (!io.MouseDown[0]) {
|
||||
auto entries = app->settings()->getPortfolioEntries();
|
||||
if (i < (int)entries.size()) {
|
||||
entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row;
|
||||
entries[i].gridW = nw; entries[i].gridH = nh;
|
||||
pfPersist(entries);
|
||||
}
|
||||
s_pf_resize = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Drag in progress -----
|
||||
if (s_pf_drag >= 0 && s_pf_drag < pfN) {
|
||||
int i = s_pf_drag;
|
||||
if (io.MouseDown[0]) {
|
||||
if (std::abs(io.MousePos.x - s_pf_press.x) > 4.0f || std::abs(io.MousePos.y - s_pf_press.y) > 4.0f)
|
||||
s_pf_moved = true;
|
||||
if (s_pf_moved) {
|
||||
int w = pfLayout[i].w, h = pfLayout[i].h;
|
||||
float tx = io.MousePos.x - s_pf_grab.x, ty = io.MousePos.y - s_pf_grab.y;
|
||||
int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx) / (pfCellW + pfCardGap) + 0.5f)));
|
||||
int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f));
|
||||
s_pf_target = {tc, tr, w, h};
|
||||
ImVec2 tmn(cx + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap));
|
||||
ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y);
|
||||
dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f);
|
||||
dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
||||
ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y);
|
||||
drawCard(fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
|
||||
}
|
||||
} else {
|
||||
if (!s_pf_moved) { PortfolioBeginEdit(app, i); s_portfolio_editor_open = true; }
|
||||
else {
|
||||
auto entries = app->settings()->getPortfolioEntries();
|
||||
if (i < (int)entries.size()) {
|
||||
int occ = -1;
|
||||
for (int j = 0; j < pfN; j++)
|
||||
if (j != i && pfLayout[j].col == s_pf_target.col && pfLayout[j].row == s_pf_target.row) { occ = j; break; }
|
||||
entries[i].gridCol = s_pf_target.col; entries[i].gridRow = s_pf_target.row;
|
||||
if (occ >= 0 && occ < (int)entries.size()) {
|
||||
entries[occ].gridCol = s_pf_old.col; entries[occ].gridRow = s_pf_old.row;
|
||||
}
|
||||
pfPersist(entries);
|
||||
}
|
||||
}
|
||||
s_pf_drag = -1; s_pf_moved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user