diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index f5655a3..f440649 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 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)); }