Add a clickable toolbar menu (raygui) alongside the existing keybindings

The app was 100% keyboard-driven (~40 single-key shortcuts dumped as a
dense text list in the HUD) -- this adds a discoverable, mouse-friendly
sidebar menu overlaying the top-right of the 3D globe, built with
raygui (raylib's official header-only widget library, fetched via
CMake FetchContent). Every existing keyboard shortcut keeps working
unchanged: 16 key-handler bodies were extracted from ViewerInput.cpp
into named Viewer methods so both the key and the matching toolbar
widget call the exact same code, and can never diverge.

Sections: a View Mode dropdown (replacing keys 1-0, expanding the old
6-key cycle into 4 explicit entries), Overlays (checkboxes), World/Time
(play/pause, step, fast-forward, a log-scaled speed slider, hydrology,
reseed, save/load), Civilization & Ecology, Live World, and Edit Mode.
Key F1 collapses/expands the sidebar (default expanded); a new
inToolbar input gate keeps interacting with it from also orbiting the
camera or picking a globe tile underneath, and it's disabled while the
Phase-3 modal prompt is up.

Found and fixed a real bug while building this: GuiCheckBox treats its
bounds as the checkbox glyph itself (raygui auto-positions the label
outside it), not a whole clickable row -- a naive full-row checkbox
call drew an oversized glyph with the label clipped off-panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
Jonas Reith 2026-08-30 17:17:40 +02:00
parent 8fec3d82ad
commit 56d74aba7b
10 changed files with 559 additions and 144 deletions

View File

@ -73,6 +73,8 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
F5 / F9 save / load full state (planet.save)
F12 screenshot to screenshot.png
F2 reload planet.cfg (validated) and regenerate
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
checkbox/dropdown for every key above, built with raygui)
CLI flags (applied before the first load/generate):

View File

@ -310,6 +310,31 @@ on the Live World clock). **Steps 17 of the roadmap are done (plus a derived
per-cell `editLock` bitmask + the sparse locked-value maps (climate/biota-density/habitability); older
saves load with no locks. Headless `test_edit.cpp`: nudge-vs-lock behaviour for every field, organism
add/remove, save v24 round-trip, corrupt-stream rejection, pre-v24 compatibility.
- **Toolbar (clickable menu)** *(done — see `src/render/Toolbar.{hpp,cpp}`)* — the app was previously
100% keyboard-driven (~40 single-key shortcuts dumped as a dense text list in the HUD); this adds a
discoverable, mouse-friendly **sidebar menu** overlaying the top-right of the 3D globe, built with
**raygui** (raylib's official header-only widget library, fetched via CMake `FetchContent`
`DOWNLOAD_ONLY` like raylib itself — `src/render/RayGuiImpl.cpp` holds the one
`RAYGUI_IMPLEMENTATION` translation unit, re-skinned dark via `setupToolbarStyle()` to match the
app's palette). Every existing keyboard shortcut still works **unchanged** — the toolbar is purely
additive, and its widgets call the exact same `Viewer::` methods the keys do (16 key-handler bodies
`P`/`X`/`Z`/`U`/`I`/`E`/`L`/`H`/`W`/`Y`/`M`/`Shift+M`/`R`/`S`/`F`/`F3` — were extracted from
`ViewerInput.cpp` into named methods in `Viewer.cpp` for exactly this sharing, guard included, so
keyboard and toolbar can never diverge in behaviour). Sections: a **View Mode** dropdown (the 13
plain colour modes, replacing keys `1`-`0` and expanding the old `6`-key cycle into 4 explicit
entries), **Overlays** (checkboxes for `B`/`D`/`G`/`J`/`N`/`T`/`O`/`K`/`V`/`M`), **World/Time**
(play/pause, step, fast-forward, a log-scaled speed slider over `driftRate`/`liveRate`, hydrology,
reseed, save/load), **Civilization & Ecology** (`E`/`I`/`L`/`U`/`P`/`X`/`Z`, grey until `settled`),
**Live World** (`W`/`Y`/`.`/`,`) and **Edit Mode** (`F3`). Key **`F1`** collapses/expands the sidebar
(default expanded, for first-run discoverability); a `bool inToolbar` gate in `handleInput()` (mirrors
the existing `inPanel`/`inLiveInfo` pattern) keeps interacting with it from also orbiting the camera
or picking a globe tile underneath, and the whole panel is `GuiDisable()`d while the Phase-3 modal
prompt is up (same exclusivity the rest of the input already has). One real bug found + fixed while
building this: `GuiCheckBox` treats its `bounds` as the checkbox glyph itself (raygui auto-positions
the label outside it using the label's own width) rather than as a whole clickable row — a naive
full-row-width checkbox call just drew an oversized glyph with the label clipped off-panel; fixed by
a small `guiCheckRow()` helper (`Toolbar.cpp`) that passes a compact glyph rect and draws the label
itself at a known position. Purely `src/render` — no save-format or `src/sim` change.
## Current state
@ -731,6 +756,8 @@ src/
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
Panels.* right-column UI: detail panel, hover info, world stats, edit-mode panel
Toolbar.* clickable raygui sidebar menu (mouse-friendly counterpart to the key list)
RayGuiImpl.cpp the one RAYGUI_IMPLEMENTATION translation unit (raygui.h is declaration-only elsewhere)
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
ViewerRender.cpp renderGlobe3D / renderMap2D / renderPanels / renderHUD / renderPrompt
@ -774,7 +801,9 @@ cmake --build build -j
Target OS is Nobara Linux (KDE/Wayland, Intel Arc A770). Dependency install
line is in BUILD.md (`dnf install cmake gcc-c++ mesa-libGL-devel ...`).
raylib 5.5 is fetched automatically — do not vendor it.
raylib 5.5 is fetched automatically — do not vendor it. raygui 5.0 (the toolbar's widget
library) is likewise fetched via `FetchContent` (`DOWNLOAD_ONLY` — it's a single header,
no build step of its own).
### Quick headless logic test (no display needed)
@ -845,6 +874,8 @@ on a settled world; re-press regenerates) · `W` enter/leave **Live World** (set
`R` reseed ·
`F3` **edit mode** (a settled world; click a tile to edit every property — geology/climate/biota/
settlement — with per-field lock against automatic recompute; see below) ·
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
button/checkbox/dropdown there — see below) ·
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
@ -887,6 +918,15 @@ automatic recompute until unlocked again (elevation resists erosion/relaxation,
density/habitability stay pinned, a settlement's population/allegiance/culture stop changing on
their own). Saved (v24).
Toolbar (`F1`, default expanded): a clickable raygui sidebar over the top-right of the 3D globe with
a button/checkbox/dropdown for essentially every key above, grouped into View Mode / Overlays /
World & Time / Civilization & Ecology / Live World / Edit Mode. Purely additive — every keyboard
shortcut keeps working exactly as documented above; the sidebar is just a more discoverable way to
reach the same actions. Grey/disabled rows mean the action needs a settled world (or Live World);
the panel scrolls (mouse wheel) past the World/Time controls to reach Civilization/Live World/Edit
Mode. Locked out (`GuiDisable`d) while the Phase-3 "start hydrology?" prompt is up, same as
everything else.
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
file (both applied before the initial load/generate).

View File

@ -15,6 +15,18 @@ set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(raylib)
# raygui: raylib's official header-only companion widget library (buttons, checkboxes,
# dropdowns, sliders, scroll panels -- used by the Toolbar sidebar, src/render/Toolbar.cpp).
# Download-only: it's a single header (src/raygui.h), no build step of its own needed.
FetchContent_Declare(
raygui
GIT_REPOSITORY https://github.com/raysan5/raygui.git
GIT_TAG 5.0
GIT_SHALLOW TRUE
DOWNLOAD_ONLY TRUE
)
FetchContent_MakeAvailable(raygui)
set(SIM_SOURCES
src/sim/IcoSphere.cpp
src/sim/Planet.cpp
@ -50,6 +62,8 @@ set(RENDER_SOURCES
src/render/Overlays.cpp
src/render/Picking.cpp
src/render/Panels.cpp
src/render/Toolbar.cpp
src/render/RayGuiImpl.cpp
src/render/Viewer.cpp
src/render/ViewerInput.cpp
src/render/ViewerRender.cpp
@ -63,7 +77,7 @@ add_executable(planetsim
${RENDER_SOURCES}
)
# Flat includes ("Planet.hpp", "Viewer.hpp", ...) resolve across both folders.
target_include_directories(planetsim PRIVATE src/sim src/render)
target_include_directories(planetsim PRIVATE src/sim src/render ${raygui_SOURCE_DIR}/src)
target_link_libraries(planetsim PRIVATE planetsim_sim raylib)
# OpenMP parallelizes the per-cell passes in Planet::step(). Optional: without
@ -86,7 +100,7 @@ foreach(test_name logic biota ocean live weather volcano geography ecoregions ci
endforeach()
add_executable(test_events test_events.cpp ${RENDER_SOURCES})
target_include_directories(test_events PRIVATE src/sim src/render)
target_include_directories(test_events PRIVATE src/sim src/render ${raygui_SOURCE_DIR}/src)
target_link_libraries(test_events PRIVATE planetsim_sim raylib)
if(UNIX AND NOT APPLE)
target_link_libraries(test_events PRIVATE m pthread dl)

View File

@ -0,0 +1,7 @@
// raygui's single-header implementation, isolated in its own translation unit so
// RAYGUI_IMPLEMENTATION is defined exactly once (a second definition anywhere else
// in the program would be a duplicate-symbol link error). Every other file that
// needs Gui* calls just #include "raygui.h" without the macro (declarations only).
#include "raylib.h" // must come first: raygui expects Rectangle/Color/Vector2 already declared
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"

251
src/render/Toolbar.cpp Normal file
View File

@ -0,0 +1,251 @@
#include "Toolbar.hpp"
#include "Viewer.hpp"
#include "raygui.h"
#include <algorithm>
#include <cmath>
// 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);
}
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();
}

20
src/render/Toolbar.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
struct Viewer;
// A clickable, discoverable raygui sidebar overlaying the top-right of the 3D globe
// viewport -- the mouse-friendly counterpart to the ~40 keyboard shortcuts in
// ViewerInput.cpp. Every shortcut keeps working unchanged; each toolbar widget calls
// the exact same Viewer method its matching key does (see Viewer.hpp's "Actions shared
// by keyboard shortcuts + the toolbar" section), so the two paths can never diverge.
//
// Unlike Panels.cpp's read-only draw*() functions, this both reads AND WRITES Viewer
// state: a raygui widget call is simultaneously "draw this frame" and "was this clicked
// this frame" -- there is no separate input-handling pass for it (input gating against
// the rest of the app -- not orbiting the camera while dragging inside the sidebar --
// still happens in ViewerInput.cpp's `inToolbar` check).
void drawToolbar(Viewer& v);
// Re-skins raygui's (light, Windows95-grey) default theme to match this app's dark palette.
// Call once, after InitWindow(), before the first drawToolbar().
void setupToolbarStyle();

View File

@ -1,6 +1,7 @@
#include "Viewer.hpp"
#include "Picking.hpp" // angBetween (rebuildSub)
#include "Projection.hpp" // EqualEarth (layout)
#include "Toolbar.hpp" // setupToolbarStyle
#include <algorithm>
#include <cmath>
#include <cstring>
@ -117,6 +118,12 @@ bool Viewer::init(int argc, char** argv) {
p3ContinueBtn = Rectangle{ pbCx - pbW - pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
p3StartBtn = Rectangle{ pbCx + pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
// Toolbar (raygui sidebar): right edge of the 3D viewport, doesn't overlap the top-center
// view-mode label, the phase-3 modal, or the bottom-left pause button.
toolbarRect = Rectangle{ (float)view3DW - 300.0f, 0.0f, 300.0f, (float)view3DH };
toolbarToggleBtn = Rectangle{ (float)view3DW - 92.0f, 4.0f, 84.0f, 26.0f };
setupToolbarStyle();
cfg.subdivisions = 5;
if (!loadConfig(configPath, cfg)) saveConfig(configPath, cfg); // load, or create a default
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
@ -897,6 +904,157 @@ void Viewer::rebuildTerritory() {
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
}
// ---- Actions shared by keyboard shortcuts (ViewerInput.cpp) and the toolbar (Toolbar.cpp) -----
// Each extracted verbatim from its key handler's body (guard moved inside), so a keypress and the
// matching toolbar widget can never diverge in behaviour -- see Viewer.hpp for the full list.
void Viewer::toggleTerritoryView() { // P: territory/realms colour view + political borders
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.nationsBuilt() || (int)planet.cellNation().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Territory) ? ColorMode::Biome : ColorMode::Territory;
showNationBorders = (mode == ColorMode::Territory);
recolor();
setStatus(mode == ColorMode::Territory ? "Territory / realms on" : "Territory off");
}
void Viewer::toggleCultureView() { // X: culture/faiths colour view + cultural borders
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.culturesBuilt() || (int)planet.cellCulture().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Culture) ? ColorMode::Biome : ColorMode::Culture;
showCultureBorders = (mode == ColorMode::Culture);
recolor();
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
}
void Viewer::toggleTradeView() { // Z: wealth/trade colour view + trade routes
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.tradeBuilt() || (int)planet.cellWealth().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Wealth) ? ColorMode::Biome : ColorMode::Wealth;
showTradeRoutes = (mode == ColorMode::Wealth);
recolor();
setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off");
}
void Viewer::placeOrToggleSettlements() { // U: seed on first press ("the dawn"), then toggle markers
if (!settled) return;
if (!planet.settlementsPlaced()) {
planet.placeSettlements();
rebuildTerritory(); // initial realms + borders
showSettlements = true;
appendEvent(3, 1, liveTime, planet.settlements.empty() ? 0 : planet.settlements[0].cell, 0,
"Civilization begins",
std::string(TextFormat("%d villages founded", (int)planet.settlements.size())));
setStatus(TextFormat("Civilization begins (%d settlements)", (int)planet.settlements.size()));
} else {
showSettlements = !showSettlements;
setStatus(showSettlements ? "Settlements on" : "Settlements off");
}
}
void Viewer::toggleHabitabilityView() { // I: habitability heat-map view
if (!settled) return;
planet.computeHabitability();
mode = (mode == ColorMode::Habitability) ? ColorMode::Biome : ColorMode::Habitability;
recolor();
setStatus(mode == ColorMode::Habitability ? "Habitability on" : "Habitability off");
}
void Viewer::toggleEcoregionView() { // E: ecoregion atlas colour view (names ecology on first use)
if (!settled) return;
if (!planet.ecoregionsBuilt()) planet.generateEcoregions();
mode = (mode == ColorMode::Ecoregion) ? ColorMode::Biome : ColorMode::Ecoregion;
recolor();
setStatus(mode == ColorMode::Ecoregion ? "Ecoregions on" : "Ecoregions off");
}
void Viewer::generateOrRegenerateBiota() { // L: generate / regenerate the biota population
if (!settled) return;
planet.generateBiota();
if (planet.ecoregionsBuilt()) planet.generateEcoregions();
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
{ mode = ColorMode::FloraDensity; recolor(); }
setStatus("Biota generated (flora/fauna/funga)");
}
void Viewer::toggleHydrology() { // H: toggle Phase 3 (hydrology)
if (!settled) return;
phase3 = !phase3; phase3Prompt = false;
paused = true; // switching stage auto-pauses (SPACE to run)
if (phase3) { phase3PromptAt = elapsedMy; setStatus("Hydrology ON - paused (SPACE to run)"); }
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF - paused"); }
refreshView();
}
void Viewer::enterOrLeaveLiveWorld() { // W: enter / leave Live World (slow real-time clock)
if (!settled) return;
liveWorld = !liveWorld;
if (liveWorld) {
phase3Prompt = false; paused = true; wxUndo.clear(); // enter Live World paused (SPACE to run)
if (planet.volcanoes.empty()) planet.placeVolcanoes(liveTime); // one-time tectonic-context placement
if (!planet.geographyBuilt()) planet.generateGeography(); // name the world's geography (the atlas)
refreshView(); // fresh base colours; overlay builds in stepSim
setStatus("Live World started - paused (SPACE to run)");
} else {
paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused
setStatus("Live World stopped");
}
}
void Viewer::cycleFollowStorm() { // Y: cycle the 3D camera through active storms
if (!liveWorld) return;
const auto& st = planet.storms();
if (st.empty()) { followId = 0; setStatus("No weather systems to follow"); return; }
std::vector<int> idx(st.size()); for (size_t i = 0; i < st.size(); ++i) idx[i] = (int)i;
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return st[a].strength > st[b].strength; });
int cur = -1; for (size_t k = 0; k < idx.size(); ++k) if (st[idx[k]].id == followId) { cur = (int)k; break; }
int next = (cur < 0) ? 0 : cur + 1;
if (next >= (int)idx.size()) { followId = 0; setStatus("Follow cam off"); return; }
const WeatherSystem& ws = st[idx[next]];
followId = ws.id;
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
setStatus(hur ? "Following tropical cyclone" : "Following weather system");
}
void Viewer::toggleNames() { // M: toggle place-name labels (the atlas)
if (!settled) return;
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use
showNames = !showNames;
setStatus(showNames ? "Place names on" : "Place names off");
}
void Viewer::reshuffleNames() { // Shift+M: re-extract current geography + use different names
if (!settled) return;
planet.reshuffleGeography();
showNames = true;
setStatus("Place names reshuffled");
}
void Viewer::reseed() { // R: reseed
cfg.seed = (uint32_t)(GetTime() * 100000) | 1;
regen();
}
void Viewer::stepAction() { // S: one step (forming/drift tick, or Live World clock step)
if (liveWorld) liveStepForward();
else { stepOnce(); refreshView(); }
}
void Viewer::fastForward() { // F: fast-forward Phase-1 forming to settled
if (settled) return;
while (!settled) stepOnce();
dtMy = planet.cflDtMy(); planet.drifting = true; refreshView();
}
void Viewer::toggleEditMode() { // F3: toggle edit mode (manual cell/settlement edits)
if (!settled) return;
editMode = !editMode;
if (editMode) { paused = true; setStatus("Edit mode on - click a tile to edit (F3 to exit)"); }
else { exitEditMode(); setStatus("Edit mode off"); }
}
// Push the current (pre-advance) weather state onto the bounded step-back ring.
void Viewer::wxPushSnapshot() {
if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin());

View File

@ -143,6 +143,16 @@ struct Viewer {
bool editMode = false;
EditPanelState editPanel;
// Toolbar (raygui sidebar, key F1, Toolbar.cpp): a clickable, discoverable menu for the
// most-used actions, overlaying the top-right of the 3D globe. Additive -- every keyboard
// shortcut above keeps working unchanged; the toolbar's widgets call the same Viewer methods.
bool showToolbar = true; // expanded by default (first-run discoverability)
Rectangle toolbarRect{}; // set once in init()
Rectangle toolbarToggleBtn{}; // always-visible collapse/expand tab
Vector2 toolbarScroll{0, 0}; // GuiScrollPanel scroll offset
int toolbarModeActive = 0; // GuiDropdownBox: selected view-mode index
bool toolbarModeEditMode = false; // GuiDropdownBox: is the list currently open
// Transient on-screen status line.
std::string statusMsg; double statusUntil = 0.0;
@ -198,6 +208,27 @@ struct Viewer {
void detectNationEvents(const std::vector<Nation>& beforeNations);
void focusCell(int idx, const std::string& status = "");
// ---- Actions shared by keyboard shortcuts + the toolbar (Viewer.cpp) ---
// Each mirrors the body of the matching key handler in ViewerInput.cpp (its own
// guard, e.g. "only on a settled world", moved inside) so a keypress and the
// corresponding toolbar widget can never diverge in behaviour.
void toggleTerritoryView(); // P
void toggleCultureView(); // X
void toggleTradeView(); // Z
void placeOrToggleSettlements(); // U
void toggleHabitabilityView(); // I
void toggleEcoregionView(); // E
void generateOrRegenerateBiota(); // L
void toggleHydrology(); // H
void enterOrLeaveLiveWorld(); // W
void cycleFollowStorm(); // Y
void toggleNames(); // M
void reshuffleNames(); // Shift+M
void reseed(); // R
void stepAction(); // S
void fastForward(); // F
void toggleEditMode(); // F3
// ---- Input (ViewerInput.cpp) --------------------------------------------
void handleInput();
void handleEditClick(); // dispatch a click inside the edit panel (tabs/fields/locks/picker/remove)

View File

@ -16,6 +16,11 @@ void Viewer::handleInput() {
bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect);
bool inLiveInfo = liveWorld && CheckCollisionPointRec(mp, liveInfoRect);
onPause = CheckCollisionPointRec(mp, pauseBtn);
// The raygui toolbar sidebar (Toolbar.cpp) reads real mouse state itself when drawn later this
// frame; gate it out of camera orbit/pick/zoom here so interacting with it doesn't also spin
// the globe or select a tile underneath (mirrors inPanel/inLiveInfo below).
bool inToolbar = CheckCollisionPointRec(mp, toolbarToggleBtn)
|| (showToolbar && CheckCollisionPointRec(mp, toolbarRect));
// --- Camera input (LMB drag orbits; tracks drag distance for clicks) --
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
@ -66,7 +71,7 @@ void Viewer::handleInput() {
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !phase3Prompt) {
Vector2 d = GetMouseDelta();
dragDist += fabsf(d.x) + fabsf(d.y);
if (in3D && !onPause && !following) { // orbit (disabled while following a storm)
if (in3D && !onPause && !following && !inToolbar) { // orbit (disabled while following a storm)
camYaw += d.x * 0.005f;
camPitch += d.y * 0.005f;
camPitch = std::clamp(camPitch, -1.5f, 1.5f);
@ -84,7 +89,9 @@ void Viewer::handleInput() {
// Wheel: zoom the 2D map toward the cursor when hovering it, else zoom the camera.
float wheel = GetMouseWheelMove();
if (inMap && wheel != 0.0f) {
if (inToolbar) {
// leave the wheel value alone; the toolbar's GuiScrollPanel reads it later this frame
} else if (inMap && wheel != 0.0f) {
Rectangle vr = mapViewRect();
double u = (mp.x - vr.x) / vr.width, v = (mp.y - vr.y) / vr.height; // projection coord under cursor
double nz = std::clamp(mapZoom * (wheel > 0 ? 1.2 : 1.0 / 1.2), 1.0, 8.0);
@ -125,7 +132,7 @@ void Viewer::handleInput() {
hoveredSubIdx = j * R + i;
hoverSub = sg->sub[hoveredSubIdx]; hasHoverSub = true; // marks it on the globe
}
} else if (in3D) {
} else if (in3D && !inToolbar) {
Vec3 d = rayDirFromMouse(cam.position, cam.target, cam.fovy,
mp.x, mp.y, (float)view3DW, (float)view3DH);
Vec3 o{cam.position.x, cam.position.y, cam.position.z};
@ -153,7 +160,7 @@ void Viewer::handleInput() {
}
// --- Click = select a tile (ignored over panel/button / while dragging) -
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !inLiveInfo && !onPause && !phase3Prompt && hovered >= 0)
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !inLiveInfo && !onPause && !phase3Prompt && !inToolbar && hovered >= 0)
selectCell(hovered);
// --- Keys -------------------------------------------------------------
@ -190,148 +197,39 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_D)) showDrift = !showDrift;
if (IsKeyPressed(KEY_G)) showGrat = !showGrat;
if (IsKeyPressed(KEY_J)) showRivers = !showRivers;
if (IsKeyPressed(KEY_H) && settled) { // toggle Phase 3 (hydrology)
phase3 = !phase3; phase3Prompt = false;
paused = true; // switching stage auto-pauses (SPACE to run)
if (phase3) { phase3PromptAt = elapsedMy; setStatus("Hydrology ON - paused (SPACE to run)"); }
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF - paused"); }
refreshView();
}
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population
planet.generateBiota();
if (planet.ecoregionsBuilt()) planet.generateEcoregions();
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
{ mode = ColorMode::FloraDensity; recolor(); }
setStatus("Biota generated (flora/fauna/funga)");
}
if (IsKeyPressed(KEY_E) && settled) { // generate / toggle ecoregion atlas colour view
if (!planet.ecoregionsBuilt()) planet.generateEcoregions();
mode = (mode == ColorMode::Ecoregion) ? ColorMode::Biome : ColorMode::Ecoregion;
recolor();
setStatus(mode == ColorMode::Ecoregion ? "Ecoregions on" : "Ecoregions off");
}
if (IsKeyPressed(KEY_I) && settled) { // toggle the habitability heat-map view
planet.computeHabitability();
mode = (mode == ColorMode::Habitability) ? ColorMode::Biome : ColorMode::Habitability;
recolor();
setStatus(mode == ColorMode::Habitability ? "Habitability on" : "Habitability off");
}
if (IsKeyPressed(KEY_U) && settled) { // civilization: seed on first press ("the dawn"), then toggle markers
if (!planet.settlementsPlaced()) {
planet.placeSettlements();
rebuildTerritory(); // initial realms + borders
showSettlements = true;
appendEvent(3, 1, liveTime, planet.settlements.empty() ? 0 : planet.settlements[0].cell, 0,
"Civilization begins",
std::string(TextFormat("%d villages founded", (int)planet.settlements.size())));
setStatus(TextFormat("Civilization begins (%d settlements)", (int)planet.settlements.size()));
} else {
showSettlements = !showSettlements;
setStatus(showSettlements ? "Settlements on" : "Settlements off");
}
}
if (IsKeyPressed(KEY_P) && settled) { // toggle the territory / realms colour view + borders
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.nationsBuilt() || (int)planet.cellNation().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Territory) ? ColorMode::Biome : ColorMode::Territory;
showNationBorders = (mode == ColorMode::Territory);
recolor();
setStatus(mode == ColorMode::Territory ? "Territory / realms on" : "Territory off");
}
}
if (IsKeyPressed(KEY_X) && settled) { // toggle the culture / faiths colour view + cultural borders
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.culturesBuilt() || (int)planet.cellCulture().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Culture) ? ColorMode::Biome : ColorMode::Culture;
showCultureBorders = (mode == ColorMode::Culture);
recolor();
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
}
}
if (IsKeyPressed(KEY_Z) && settled) { // toggle the wealth / trade colour view + trade routes
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.tradeBuilt() || (int)planet.cellWealth().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Wealth) ? ColorMode::Biome : ColorMode::Wealth;
showTradeRoutes = (mode == ColorMode::Wealth);
recolor();
setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off");
}
}
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
liveWorld = !liveWorld;
if (liveWorld) {
phase3Prompt = false; paused = true; wxUndo.clear(); // enter Live World paused (SPACE to run)
if (planet.volcanoes.empty()) planet.placeVolcanoes(liveTime); // one-time tectonic-context placement
if (!planet.geographyBuilt()) planet.generateGeography(); // name the world's geography (the atlas)
refreshView(); // fresh base colours; overlay builds in stepSim
setStatus("Live World started - paused (SPACE to run)");
} else {
paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused
setStatus("Live World stopped");
}
}
if (IsKeyPressed(KEY_Y) && liveWorld) { // cycle the 3D camera through active storms
const auto& st = planet.storms();
if (st.empty()) { followId = 0; setStatus("No weather systems to follow"); }
else {
std::vector<int> idx(st.size()); for (size_t i = 0; i < st.size(); ++i) idx[i] = (int)i;
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return st[a].strength > st[b].strength; });
int cur = -1; for (size_t k = 0; k < idx.size(); ++k) if (st[idx[k]].id == followId) { cur = (int)k; break; }
int next = (cur < 0) ? 0 : cur + 1;
if (next >= (int)idx.size()) { followId = 0; setStatus("Follow cam off"); }
else {
const WeatherSystem& ws = st[idx[next]];
followId = ws.id;
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
setStatus(hur ? "Following tropical cyclone" : "Following weather system");
}
}
}
if (IsKeyPressed(KEY_H)) toggleHydrology();
if (IsKeyPressed(KEY_L)) generateOrRegenerateBiota();
if (IsKeyPressed(KEY_E)) toggleEcoregionView();
if (IsKeyPressed(KEY_I)) toggleHabitabilityView();
if (IsKeyPressed(KEY_U)) placeOrToggleSettlements();
if (IsKeyPressed(KEY_P)) toggleTerritoryView();
if (IsKeyPressed(KEY_X)) toggleCultureView();
if (IsKeyPressed(KEY_Z)) toggleTradeView();
if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld();
if (IsKeyPressed(KEY_Y)) cycleFollowStorm();
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover
if (IsKeyPressed(KEY_V)) showVolcanoes = !showVolcanoes; // toggle volcano markers (Live World)
if (IsKeyPressed(KEY_M) && settled) { // toggle / reshuffle place-name labels (the atlas)
bool reshuffle = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
if (reshuffle) {
planet.reshuffleGeography(); // re-extract current geography + use different names
showNames = true;
setStatus("Place names reshuffled");
} else {
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use
showNames = !showNames;
setStatus(showNames ? "Place names on" : "Place names off");
}
}
if (IsKeyPressed(KEY_F3) && settled) { // toggle edit mode (manual cell/settlement edits)
editMode = !editMode;
if (editMode) { paused = true; setStatus("Edit mode on - click a tile to edit (F3 to exit)"); }
else { exitEditMode(); setStatus("Edit mode off"); }
if (IsKeyPressed(KEY_M)) {
if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) reshuffleNames();
else toggleNames();
}
if (IsKeyPressed(KEY_F3)) toggleEditMode();
if (IsKeyPressed(KEY_C)) {
selectedCell = -1; subgrids.clear();
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); editPanel.pickerField = EditField::None;
}
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
if (IsKeyPressed(KEY_S)) { // one step
if (liveWorld) liveStepForward(); // Live World: step the clock forward
else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick
}
if (IsKeyPressed(KEY_R)) reseed();
if (IsKeyPressed(KEY_S)) stepAction();
// Live World clock stepper: step by one rate-unit (liveRate hours). Forward integrates weather;
// backward restores the snapshot from the last forward step -> everything (incl. weather +
// storms) steps back, within the current paused stepping session.
if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); }
if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); } // liveStepBack sets its own status
if (IsKeyPressed(KEY_F)) { // fast-forward to settled
if (!settled) {
while (!settled) stepOnce();
dtMy = planet.cflDtMy(); planet.drifting = true; refreshView();
}
}
if (IsKeyPressed(KEY_F)) fastForward();
if (IsKeyPressed(KEY_F1)) showToolbar = !showToolbar; // additive: toggle the toolbar sidebar
if (IsKeyPressed(KEY_EQUAL) && cfg.subdivisions < 7) { cfg.subdivisions++; regen(); }
if (IsKeyPressed(KEY_MINUS) && cfg.subdivisions > 1) { cfg.subdivisions--; regen(); }
if (IsKeyPressed(KEY_F2)) {

View File

@ -2,6 +2,7 @@
#include "Overlays.hpp"
#include "Map2D.hpp"
#include "Panels.hpp"
#include "Toolbar.hpp" // drawToolbar
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
#include "rlgl.h"
#include "Projection.hpp" // dirToLonLat (plate labels)
@ -908,16 +909,8 @@ void Viewer::renderHUD() {
}
y += 8;
line("hover: cell info | click tile: open detail panel | C close");
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)");
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s] | Z trade [%s]",
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
!planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off",
showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off", showTradeRoutes ? "on" : "off"));
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
line(showToolbar ? "Menu: top-right of the globe (F1 to hide) -- or use the keyboard shortcuts below"
: "F1 or the Menu tab (top-right of the globe): a clickable menu for every action below");
if (!statusMsg.empty() && GetTime() < statusUntil) {
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
}
@ -1078,6 +1071,7 @@ void Viewer::renderFrame() {
renderLiveInfo();
renderPanels();
renderHUD();
drawToolbar(*this); // drawn after the HUD, before the modal so it dims/locks together with it
renderPrompt();
EndDrawing();