#include "Toolbar.hpp" #include "Viewer.hpp" #include "raygui.h" #include #include // A clickable raygui sidebar overlaying the top-right of the 3D globe (see Toolbar.hpp). Every // widget here is either DIRECT-BIND (a plain bool with no side effect -- GuiCheckBox writes // straight into the Viewer field, exactly what the matching key's bare flip does) or // SHADOW+METHOD (a throw-away local bool seeded from the real state each frame; if raygui flips // it in response to a click, the paired Viewer:: method -- the single source of truth, same one // the keyboard shortcut calls -- runs instead of trusting the shadow's own value). The // before/after compare is used rather than the widget's own return value so this doesn't depend // on remembering the exact "pressed vs. held" return-code convention for every control type. namespace { const float PAD = 8.0f, ROWH = 24.0f, GAP = 4.0f, LABEL_H = 16.0f, SECTION_GAP = 10.0f; void sectionLabel(float x, float& y, const char* text) { DrawText(text, (int)x, (int)y, 14, Color{170, 175, 190, 255}); y += LABEL_H; } Rectangle takeRow(float x, float w, float& y, float h = ROWH) { Rectangle r{ x, y, w, h }; y += h + GAP; return r; } void toggleAction(Rectangle r, const char* label, bool current, void (Viewer::*action)(), Viewer& v) { bool shadow = current; GuiToggle(r, label, &shadow); if (shadow != current) (v.*action)(); } // GuiCheckBox treats `bounds` as the glyph square itself and auto-positions its label OUTSIDE // that rect using the label's own natural width -- it doesn't compose with a fixed-width // column layout (a wide `bounds` just draws an oversized square, and the label runs off past // it). So: pass it a small square glyph rect and draw our own label at a known position // instead. Returns whether *flag changed (a click), for callers using the shadow+method // pattern; direct-bind callers can just ignore the return value. bool guiCheckRow(Rectangle row, const char* label, bool* flag) { Rectangle box{ row.x, row.y + (row.height - 18.0f) * 0.5f, 18.0f, 18.0f }; bool before = *flag; GuiCheckBox(box, "", flag); DrawText(label, (int)(row.x + 26.0f), (int)(row.y + (row.height - 13.0f) * 0.5f), 13, Color{200, 205, 215, 255}); return *flag != before; } // View-mode dropdown: the 13 "plain" colour modes (Ecoregion/Habitability/Territory/Culture/ // Wealth are separate side-effecting actions in the Civilization & Ecology section instead -- // same split the original keys 1-0 vs E/I/P/X/Z already had). const ColorMode kViewModes[] = { ColorMode::Elevation, ColorMode::Plate, ColorMode::Age, ColorMode::Crust, ColorMode::Biome, ColorMode::Temperature, ColorMode::TempSummer, ColorMode::TempWinter, ColorMode::Seasonality, ColorMode::Precip, ColorMode::FloraDensity, ColorMode::FaunaDensity, ColorMode::FungaDensity, }; const int kViewModeCount = 13; const char* kViewModeList = "Elevation;Plates;Crust age;Crust type;Biome;Temperature;Temperature (summer);" "Temperature (winter);Seasonality (summer-winter);Precipitation;Flora density;" "Fauna density;Funga density"; // Log-scaled slider mapping so one slider usefully covers driftRate's 0.5..80 range and // liveRate's 0.25..liveRateMax() range (up to ~175000, a full My/live-clock speed span). float toSliderT(double val, double lo, double hi) { double lv = std::log(std::clamp(val, lo, hi)), llo = std::log(lo), lhi = std::log(hi); return (float)std::clamp((lv - llo) / (lhi - llo), 0.0, 1.0); } double fromSliderT(float t, double lo, double hi) { double llo = std::log(lo), lhi = std::log(hi); return std::exp(llo + (double)std::clamp(t, 0.0f, 1.0f) * (lhi - llo)); } } void setupToolbarStyle() { GuiSetFont(GetFontDefault()); GuiSetStyle(DEFAULT, TEXT_SIZE, 14); GuiSetStyle(DEFAULT, BACKGROUND_COLOR, ColorToInt(Color{12, 14, 22, 235})); GuiSetStyle(DEFAULT, LINE_COLOR, ColorToInt(Color{90, 90, 110, 255})); GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, ColorToInt(Color{90, 90, 110, 255})); GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, ColorToInt(Color{28, 30, 42, 255})); GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, ColorToInt(Color{210, 210, 220, 255})); GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, ColorToInt(Color{235, 225, 140, 255})); GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, ColorToInt(Color{60, 66, 92, 255})); GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, ColorToInt(RAYWHITE)); GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, ColorToInt(Color{255, 200, 120, 255})); GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, ColorToInt(Color{80, 88, 120, 255})); GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, ColorToInt(RAYWHITE)); GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, ColorToInt(Color{50, 52, 60, 255})); GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, ColorToInt(Color{20, 22, 30, 255})); GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, ColorToInt(Color{95, 98, 110, 255})); } void drawToolbar(Viewer& v) { // Always-visible collapse/expand tab, independent of showToolbar. bool wasOpen = v.showToolbar; bool openShadow = wasOpen; GuiToggle(v.toolbarToggleBtn, wasOpen ? "Menu x" : "Menu", &openShadow); if (openShadow != wasOpen) v.showToolbar = openShadow; if (!v.showToolbar) return; bool wasModal = v.phase3Prompt; if (wasModal) GuiDisable(); Rectangle panel = v.toolbarRect; DrawRectangleRec(panel, Color{12, 14, 22, 235}); DrawRectangleLinesEx(panel, 1, Color{90, 90, 110, 255}); float x = panel.x + PAD, w = panel.width - 2 * PAD; float y = panel.y + PAD; Rectangle dropRect = takeRow(x, w, y, 24.0f); // reserved now, drawn LAST so its open list is on top y += SECTION_GAP * 0.5f; Rectangle scrollBounds{ panel.x, y, panel.width, (panel.y + panel.height) - y }; Rectangle content{ 0, 0, panel.width - 14.0f, 900.0f }; Rectangle view{}; bool dropdownOpen = v.toolbarModeEditMode; if (dropdownOpen) GuiLock(); GuiScrollPanel(scrollBounds, nullptr, content, &v.toolbarScroll, &view); BeginScissorMode((int)view.x, (int)view.y, (int)view.width, (int)view.height); { float cx = scrollBounds.x + PAD + v.toolbarScroll.x; float cw = content.width - 2 * PAD; float cy = scrollBounds.y + PAD + v.toolbarScroll.y; // --- Overlays --------------------------------------------------------- sectionLabel(cx, cy, "Overlays"); struct Ov { const char* label; bool* flag; }; Ov overlays[] = { {"Borders (B)", &v.showBorders}, {"Drift (D)", &v.showDrift}, {"Grid (G)", &v.showGrat}, {"Rivers (J)", &v.showRivers}, {"Day/night (N)", &v.dayNightOn}, {"Tides (T)", &v.showTides}, {"Currents (O)", &v.showCurrents}, {"Clouds (K)", &v.showClouds}, {"Volcanoes (V)", &v.showVolcanoes}, }; float halfW = (cw - GAP) * 0.5f; for (int i = 0; i < 9; i += 2) { guiCheckRow(Rectangle{ cx, cy, halfW, ROWH }, overlays[i].label, overlays[i].flag); if (i + 1 < 9) guiCheckRow(Rectangle{ cx + halfW + GAP, cy, halfW, ROWH }, overlays[i + 1].label, overlays[i + 1].flag); cy += ROWH + GAP; } { bool namesShadow = v.showNames; if (guiCheckRow(Rectangle{ cx, cy, halfW, ROWH }, "Names (M)", &namesShadow)) v.toggleNames(); if (GuiButton(Rectangle{ cx + halfW + GAP, cy, halfW, ROWH }, "Reshuffle")) v.reshuffleNames(); cy += ROWH + GAP; } cy += SECTION_GAP; // --- World / Time ------------------------------------------------------- sectionLabel(cx, cy, "World / Time"); { bool pauseShadow = v.paused; GuiToggle(takeRow(cx, cw, cy), v.paused ? "Resume" : "Pause", &pauseShadow); if (pauseShadow != v.paused) v.pauseAction(); } if (GuiButton(takeRow(cx, cw, cy), "Step (S)")) v.stepAction(); { bool settledNow = v.settled; if (settledNow) GuiDisable(); if (GuiButton(takeRow(cx, cw, cy), "Fast-forward (F)")) v.fastForward(); if (settledNow) GuiEnable(); } { bool isLive = v.liveWorld; double lo = isLive ? 0.25 : 0.5; double hi = isLive ? v.liveRateMax() : 80.0; double cur = isLive ? v.liveRate : v.driftRate; DrawText(TextFormat(isLive ? "Speed: %.1f h/s (live clock)" : "Speed: %.1f My/s", cur), (int)cx, (int)cy, 13, Color{170, 175, 190, 255}); cy += LABEL_H; float t = toSliderT(cur, lo, hi); Rectangle r = takeRow(cx, cw, cy); if (GuiSlider(r, "", "", &t, 0.0f, 1.0f)) { double nv = fromSliderT(t, lo, hi); if (isLive) v.liveRate = nv; else v.driftRate = nv; } } { bool hydroShadow = v.phase3; if (guiCheckRow(takeRow(cx, cw, cy), "Hydrology (H)", &hydroShadow)) v.toggleHydrology(); } if (GuiButton(takeRow(cx, cw, cy), "Reseed (R)")) v.reseed(); { Rectangle r = takeRow(cx, cw, cy); if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(v.SAVE_PATH); if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(v.SAVE_PATH); } // Deferred to after drawToolbar() returns -- see Viewer::pendingMapExport/pendingAtlasExport. if (GuiButton(takeRow(cx, cw, cy), "Export Map (F11)")) v.pendingMapExport = true; if (GuiButton(takeRow(cx, cw, cy), "Export Atlas (Shift+F11)")) v.pendingAtlasExport = true; cy += SECTION_GAP; // --- Civilization & Ecology (all need a settled world) ------------------ sectionLabel(cx, cy, "Civilization & Ecology"); { bool settledNow = v.settled; if (!settledNow) GuiDisable(); toggleAction(takeRow(cx, cw, cy), "Ecoregions (E)", v.mode == ColorMode::Ecoregion, &Viewer::toggleEcoregionView, v); toggleAction(takeRow(cx, cw, cy), "Habitability (I)", v.mode == ColorMode::Habitability, &Viewer::toggleHabitabilityView, v); if (GuiButton(takeRow(cx, cw, cy), "Generate biota (L)")) v.generateOrRegenerateBiota(); toggleAction(takeRow(cx, cw, cy), v.planet.settlementsPlaced() ? "Settlements (U)" : "Found settlements (U)", v.showSettlements && v.planet.settlementsPlaced(), &Viewer::placeOrToggleSettlements, v); toggleAction(takeRow(cx, cw, cy), "Territory (P)", v.mode == ColorMode::Territory, &Viewer::toggleTerritoryView, v); toggleAction(takeRow(cx, cw, cy), "Culture (X)", v.mode == ColorMode::Culture, &Viewer::toggleCultureView, v); toggleAction(takeRow(cx, cw, cy), "Trade / wealth (Z)", v.mode == ColorMode::Wealth, &Viewer::toggleTradeView, v); if (!settledNow) GuiEnable(); } cy += SECTION_GAP; // --- Live World ----------------------------------------------------------- sectionLabel(cx, cy, "Live World"); { bool settledNow = v.settled; if (!settledNow) GuiDisable(); toggleAction(takeRow(cx, cw, cy), v.liveWorld ? "Exit Live World" : "Enter Live World (W)", v.liveWorld, &Viewer::enterOrLeaveLiveWorld, v); if (!settledNow) GuiEnable(); bool liveNow = v.liveWorld; if (!liveNow) GuiDisable(); if (GuiButton(takeRow(cx, cw, cy), "Follow storm (Y)")) v.cycleFollowStorm(); { Rectangle r = takeRow(cx, cw, cy); if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Step + (.)")) { v.liveStepForward(); v.setStatus("Step forward"); } if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Step - (,)")) v.liveStepBack(); } if (!liveNow) GuiEnable(); } cy += SECTION_GAP; // --- Edit Mode -------------------------------------------------------- sectionLabel(cx, cy, "Edit Mode"); { bool settledNow = v.settled; if (!settledNow) GuiDisable(); toggleAction(takeRow(cx, cw, cy), v.editMode ? "Exit Edit Mode" : "Edit Mode (F3)", v.editMode, &Viewer::toggleEditMode, v); if (!settledNow) GuiEnable(); } } EndScissorMode(); if (dropdownOpen) GuiUnlock(); // View-mode dropdown, drawn LAST so its open list renders on top of the scroll body below it // (see Toolbar.hpp / the gotcha this avoids: raygui doesn't auto-exclude overlapping controls). if (!v.toolbarModeEditMode) { for (int i = 0; i < kViewModeCount; ++i) if (kViewModes[i] == v.mode) { v.toolbarModeActive = i; break; } } if (GuiDropdownBox(dropRect, kViewModeList, &v.toolbarModeActive, v.toolbarModeEditMode)) v.toolbarModeEditMode = !v.toolbarModeEditMode; if (!v.toolbarModeEditMode) { ColorMode picked = kViewModes[std::clamp(v.toolbarModeActive, 0, kViewModeCount - 1)]; if (picked != v.mode) { v.mode = picked; v.recolor(); } } if (wasModal) GuiEnable(); }