feat(market): flatten pair selection into round buttons (no exchange dropdown)
Replace the exchange combo dropdown + the horizontally-scrolling pair chip bar with a single flat selector: every trading pair across all exchanges is shown as a round button (pair name + dimmed exchange sublabel) that wraps to multiple rows. Clicking one selects that exchange+pair, persists it, and refreshes market data. The CoinGecko attribution + last-updated line moves under the buttons. Removes the now-unused pair-scroll state (scroll/drag/arrow machinery) and the pair-bar-height budget input. Full-node + lite build clean; source hygiene clean. (Second half of the Market "round buttons + combine cards" item — the hero/chart card merge is next.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,11 +36,6 @@ static double s_last_refresh_time = 0.0;
|
||||
// Exchange / pair selection
|
||||
static int s_exchange_idx = 0;
|
||||
static int s_pair_idx = 0;
|
||||
static float s_pair_scroll = 0.0f;
|
||||
static float s_pair_scroll_target = 0.0f;
|
||||
static bool s_pair_dragging = false;
|
||||
static float s_pair_drag_start_x = 0.0f;
|
||||
static float s_pair_drag_start_scroll = 0.0f;
|
||||
static bool s_market_state_loaded = false;
|
||||
|
||||
// The effective exchange registry: the live CoinGecko list when available, else the
|
||||
@@ -152,7 +147,6 @@ void RenderMarketTab(App* app)
|
||||
+ ImGui::GetStyle().ItemSpacing.y * 2.0f;
|
||||
float mkGapOver = gap + ImGui::GetStyle().ItemSpacing.y;
|
||||
float mkOverhead = 3.0f * (mkSHdr + mkGapOver) + 2.0f * mkGapOver;
|
||||
float pairBarH = S.drawElement("tabs.market", "pair-bar-height").height;
|
||||
float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead);
|
||||
|
||||
Layout::SectionBudget mb(mkCardBudget);
|
||||
@@ -522,250 +516,90 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// EXCHANGE SELECTOR — Combo dropdown + attribution
|
||||
// PAIR SELECTOR — every trading pair across all exchanges shown as
|
||||
// round buttons (flat; no exchange dropdown). Wraps to multiple rows.
|
||||
// ================================================================
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
|
||||
{
|
||||
float comboW = S.drawElement("tabs.market", "exchange-combo-width").size;
|
||||
float chipH = S.drawElement("tabs.market", "pair-chip-height").height;
|
||||
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius;
|
||||
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size;
|
||||
float innerGap = Layout::spacingSm();
|
||||
float sidePad = Layout::spacingMd();
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::PushItemWidth(comboW);
|
||||
if (ImGui::BeginCombo("##ExchangeCombo", currentExchange.name.c_str())) {
|
||||
for (int i = 0; i < (int)registry.size(); i++) {
|
||||
bool selected = (i == s_exchange_idx);
|
||||
if (ImGui::Selectable(registry[i].name.c_str(), selected)) {
|
||||
if (i != s_exchange_idx) {
|
||||
s_exchange_idx = i;
|
||||
s_pair_idx = 0;
|
||||
s_pair_scroll = 0.0f;
|
||||
s_pair_scroll_target = 0.0f;
|
||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
float left = origin.x;
|
||||
float right = origin.x + availWidth;
|
||||
float cx = left;
|
||||
float cy = origin.y;
|
||||
int btnCounter = 0;
|
||||
|
||||
for (int ex = 0; ex < (int)registry.size(); ex++) {
|
||||
for (int pr = 0; pr < (int)registry[ex].pairs.size(); pr++) {
|
||||
const std::string& pairName = registry[ex].pairs[pr].displayName;
|
||||
const std::string& exName = registry[ex].name;
|
||||
|
||||
float pairW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pairName.c_str()).x;
|
||||
float exW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, exName.c_str()).x;
|
||||
float cw = sidePad * 2.0f + pairW + innerGap + exW;
|
||||
|
||||
// Wrap to the next row when the pill would overflow (never on an empty row).
|
||||
if (cx + cw > right && cx > left) {
|
||||
cx = left;
|
||||
cy += chipH + chipSpacing;
|
||||
}
|
||||
|
||||
ImVec2 cMin(cx, cy);
|
||||
ImVec2 cMax(cx + cw, cy + chipH);
|
||||
bool selected = (ex == s_exchange_idx && pr == s_pair_idx);
|
||||
bool hov = material::IsRectHovered(cMin, cMax);
|
||||
ImU32 chipBg = selected ? WithAlpha(Primary(), 200)
|
||||
: (hov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 20));
|
||||
ImU32 chipBorder = selected ? Primary() : WithAlpha(OnSurface(), 40);
|
||||
ImU32 pairCol = selected ? IM_COL32(255, 255, 255, 255) : OnSurface();
|
||||
ImU32 exCol = selected ? WithAlpha(IM_COL32(255, 255, 255, 255), 190) : OnSurfaceDisabled();
|
||||
|
||||
dl->AddRectFilled(cMin, cMax, chipBg, chipR);
|
||||
dl->AddRect(cMin, cMax, chipBorder, chipR, 0, 1.0f);
|
||||
|
||||
float tx = cx + sidePad;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx, cy + (chipH - capFont->LegacySize) * 0.5f), pairCol, pairName.c_str());
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(tx + pairW + innerGap, cy + (chipH - ovFont->LegacySize) * 0.5f),
|
||||
exCol, exName.c_str());
|
||||
|
||||
ImGui::SetCursorScreenPos(cMin);
|
||||
snprintf(buf, sizeof(buf), "##PairBtn%d", btnCounter++);
|
||||
if (ImGui::InvisibleButton(buf, ImVec2(cw, chipH))) {
|
||||
if (ex != s_exchange_idx || pr != s_pair_idx) {
|
||||
s_exchange_idx = ex;
|
||||
s_pair_idx = pr;
|
||||
s_history_initialized = false;
|
||||
app->settings()->setSelectedExchange(registry[i].name);
|
||||
if (!registry[i].pairs.empty())
|
||||
app->settings()->setSelectedPair(registry[i].pairs[0].displayName);
|
||||
app->settings()->setSelectedExchange(exName);
|
||||
app->settings()->setSelectedPair(pairName);
|
||||
app->settings()->save();
|
||||
app->refreshMarketData();
|
||||
}
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
|
||||
cx += cw + chipSpacing;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Attribution
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
// Advance the cursor past the wrapped rows.
|
||||
ImGui::SetCursorScreenPos(ImVec2(left, cy + chipH));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
|
||||
// Attribution (moved here from the removed exchange-selector row).
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
|
||||
|
||||
if (!market.last_updated.empty()) {
|
||||
ImGui::SameLine(0, 12);
|
||||
snprintf(buf, sizeof(buf), " \xc2\xb7 Updated %s", market.last_updated.c_str());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PAIR BAR — Horizontally scrolling chip selector (always visible)
|
||||
// ================================================================
|
||||
{
|
||||
float chipH = S.drawElement("tabs.market", "pair-chip-height").height;
|
||||
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius;
|
||||
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size;
|
||||
float fadeW = S.drawElement("tabs.market", "pair-bar-fade-width").size;
|
||||
float arrowSz = S.drawElement("tabs.market", "pair-bar-arrow-size").size;
|
||||
|
||||
ImVec2 barOrigin = ImGui::GetCursorScreenPos();
|
||||
float barW = availWidth;
|
||||
float barH = pairBarH;
|
||||
|
||||
// Compute total content width of all chips
|
||||
float totalChipW = 0.0f;
|
||||
std::vector<float> chipWidths;
|
||||
for (const auto& pair : currentExchange.pairs) {
|
||||
float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pair.displayName.c_str()).x;
|
||||
float cw = tw + Layout::spacingLg() * 2.0f;
|
||||
chipWidths.push_back(cw);
|
||||
totalChipW += cw + chipSpacing;
|
||||
}
|
||||
totalChipW -= chipSpacing; // no trailing spacing
|
||||
|
||||
float scrollableW = barW - arrowSz * 2.0f - Layout::spacingSm() * 2.0f;
|
||||
float maxScroll = std::max(0.0f, totalChipW - scrollableW);
|
||||
|
||||
// Smooth scroll lerp
|
||||
s_pair_scroll += (s_pair_scroll_target - s_pair_scroll) * 0.15f;
|
||||
if (std::abs(s_pair_scroll - s_pair_scroll_target) < 0.5f)
|
||||
s_pair_scroll = s_pair_scroll_target;
|
||||
|
||||
// Clamp
|
||||
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
|
||||
s_pair_scroll = std::clamp(s_pair_scroll, 0.0f, maxScroll);
|
||||
|
||||
// Left arrow button
|
||||
float arrowY = barOrigin.y + (barH - arrowSz) * 0.5f;
|
||||
bool canScrollLeft = s_pair_scroll_target > 0.01f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x, arrowY));
|
||||
ImGui::BeginDisabled(!canScrollLeft);
|
||||
ImGui::PushFont(material::Typography::instance().iconSmall());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
||||
if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##PairLeft", ImVec2(arrowSz, arrowSz))) {
|
||||
// Scroll left by average chip width
|
||||
float avgChipW = totalChipW / currentExchange.pairs.size();
|
||||
s_pair_scroll_target -= avgChipW + chipSpacing;
|
||||
if (s_pair_scroll_target < 0) s_pair_scroll_target = 0;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Chip area with clipping
|
||||
float chipAreaLeft = barOrigin.x + arrowSz + Layout::spacingSm();
|
||||
float chipAreaRight = barOrigin.x + barW - arrowSz - Layout::spacingSm();
|
||||
float chipY = barOrigin.y + (barH - chipH) * 0.5f;
|
||||
|
||||
dl->PushClipRect(ImVec2(chipAreaLeft, barOrigin.y),
|
||||
ImVec2(chipAreaRight, barOrigin.y + barH), true);
|
||||
|
||||
// Render chips
|
||||
float cx = chipAreaLeft - s_pair_scroll;
|
||||
bool anyClicked = false;
|
||||
for (int i = 0; i < (int)currentExchange.pairs.size(); i++) {
|
||||
float cw = chipWidths[i];
|
||||
ImVec2 cMin(cx, chipY);
|
||||
ImVec2 cMax(cx + cw, chipY + chipH);
|
||||
|
||||
bool selected = (i == s_pair_idx);
|
||||
ImU32 chipBg = selected ? WithAlpha(Primary(), 200) : WithAlpha(OnSurface(), 20);
|
||||
ImU32 chipBorder = selected ? Primary() : WithAlpha(OnSurface(), 40);
|
||||
ImU32 chipText = selected ? IM_COL32(255, 255, 255, 255) : OnSurface();
|
||||
|
||||
dl->AddRectFilled(cMin, cMax, chipBg, chipR);
|
||||
dl->AddRect(cMin, cMax, chipBorder, chipR, 0, 1.0f);
|
||||
|
||||
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0,
|
||||
currentExchange.pairs[i].displayName.c_str());
|
||||
float textX = cx + (cw - textSz.x) * 0.5f;
|
||||
float textY = chipY + (chipH - textSz.y) * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, textY), chipText,
|
||||
currentExchange.pairs[i].displayName.c_str());
|
||||
|
||||
// Click detection via invisible button
|
||||
ImGui::SetCursorScreenPos(cMin);
|
||||
snprintf(buf, sizeof(buf), "##PairChip%d", i);
|
||||
if (ImGui::InvisibleButton(buf, ImVec2(cw, chipH))) {
|
||||
if (!s_pair_dragging || std::abs(ImGui::GetIO().MousePos.x - s_pair_drag_start_x) < 4.0f) {
|
||||
s_pair_idx = i;
|
||||
anyClicked = true;
|
||||
app->settings()->setSelectedPair(currentExchange.pairs[i].displayName);
|
||||
app->settings()->save();
|
||||
s_history_initialized = false;
|
||||
app->refreshMarketData();
|
||||
}
|
||||
}
|
||||
|
||||
cx += cw + chipSpacing;
|
||||
}
|
||||
|
||||
dl->PopClipRect();
|
||||
|
||||
// Fade overlays on edges
|
||||
ImU32 bgCol = IM_COL32(0, 0, 0, 0);
|
||||
ImU32 surfaceCol = Surface();
|
||||
if (s_pair_scroll > 1.0f) {
|
||||
// Left fade
|
||||
dl->AddRectFilledMultiColor(
|
||||
ImVec2(chipAreaLeft, barOrigin.y), ImVec2(chipAreaLeft + fadeW, barOrigin.y + barH),
|
||||
surfaceCol, bgCol, bgCol, surfaceCol);
|
||||
}
|
||||
if (s_pair_scroll < maxScroll - 1.0f) {
|
||||
// Right fade
|
||||
dl->AddRectFilledMultiColor(
|
||||
ImVec2(chipAreaRight - fadeW, barOrigin.y), ImVec2(chipAreaRight, barOrigin.y + barH),
|
||||
bgCol, surfaceCol, surfaceCol, bgCol);
|
||||
}
|
||||
|
||||
// Right arrow button
|
||||
bool canScrollRight = s_pair_scroll_target < maxScroll - 0.01f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x + barW - arrowSz, arrowY));
|
||||
ImGui::BeginDisabled(!canScrollRight);
|
||||
ImGui::PushFont(material::Typography::instance().iconSmall());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
||||
if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##PairRight", ImVec2(arrowSz, arrowSz))) {
|
||||
float avgChipW = totalChipW / currentExchange.pairs.size();
|
||||
s_pair_scroll_target += avgChipW + chipSpacing;
|
||||
if (s_pair_scroll_target > maxScroll) s_pair_scroll_target = maxScroll;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Mouse wheel horizontal scroll when hovering pair bar
|
||||
ImVec2 mPos = ImGui::GetIO().MousePos;
|
||||
if (mPos.x >= barOrigin.x && mPos.x <= barOrigin.x + barW &&
|
||||
mPos.y >= barOrigin.y && mPos.y <= barOrigin.y + barH) {
|
||||
float wheel = ImGui::GetIO().MouseWheel;
|
||||
if (wheel != 0.0f) {
|
||||
float avgChipW = totalChipW / currentExchange.pairs.size();
|
||||
s_pair_scroll_target -= wheel * (avgChipW + chipSpacing);
|
||||
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse drag scrolling
|
||||
if (ImGui::IsMouseClicked(0) && mPos.x >= chipAreaLeft && mPos.x <= chipAreaRight &&
|
||||
mPos.y >= barOrigin.y && mPos.y <= barOrigin.y + barH) {
|
||||
s_pair_dragging = true;
|
||||
s_pair_drag_start_x = mPos.x;
|
||||
s_pair_drag_start_scroll = s_pair_scroll_target;
|
||||
}
|
||||
if (s_pair_dragging) {
|
||||
if (ImGui::IsMouseDown(0)) {
|
||||
float dx = mPos.x - s_pair_drag_start_x;
|
||||
s_pair_scroll_target = std::clamp(s_pair_drag_start_scroll - dx, 0.0f, maxScroll);
|
||||
} else {
|
||||
s_pair_dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow key navigation
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && s_pair_idx > 0) {
|
||||
s_pair_idx--;
|
||||
app->settings()->setSelectedPair(currentExchange.pairs[s_pair_idx].displayName);
|
||||
app->settings()->save();
|
||||
s_history_initialized = false;
|
||||
app->refreshMarketData();
|
||||
anyClicked = true;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) && s_pair_idx < (int)currentExchange.pairs.size() - 1) {
|
||||
s_pair_idx++;
|
||||
app->settings()->setSelectedPair(currentExchange.pairs[s_pair_idx].displayName);
|
||||
app->settings()->save();
|
||||
s_history_initialized = false;
|
||||
app->refreshMarketData();
|
||||
anyClicked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to keep selected chip visible
|
||||
if (anyClicked) {
|
||||
float chipLeft = 0;
|
||||
for (int i = 0; i < s_pair_idx; i++)
|
||||
chipLeft += chipWidths[i] + chipSpacing;
|
||||
float chipRight = chipLeft + chipWidths[s_pair_idx];
|
||||
if (chipLeft < s_pair_scroll_target)
|
||||
s_pair_scroll_target = chipLeft - chipSpacing;
|
||||
if (chipRight > s_pair_scroll_target + scrollableW)
|
||||
s_pair_scroll_target = chipRight - scrollableW + chipSpacing;
|
||||
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
|
||||
}
|
||||
|
||||
// Advance cursor past the bar
|
||||
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x, barOrigin.y + barH));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user