From bc8bcf78b7d30a73bbfc57326157685888ffaea6 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 12 Aug 2026 18:38:59 -0700 Subject: [PATCH] fix: web text corruption, blocking-load notice, no-cache dev server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Most UI text drew with raylib's built-in font, not the loaded one.** Thirteen call sites used DrawText/MeasureText directly instead of the project's DrawTextScaled/MeasureTextScaled wrappers, so the cursor and marker readouts, spectrum panel labels and export toast rendered from raylib's default bitmap atlas. On desktop that merely looked slightly off; on web it renders as garbage. The only raw calls left are the two fallbacks inside the wrappers themselves. **The rail tooltip held a dangling pointer.** RailButton stored a TextFormat() result for the deferred draw pass, and raylib documents that string as expiring once TextFormat has been called a few more times — which it has by then. It copies into owned storage now. **Loading a large file in the browser looked like a crash.** The web build computes its whole STFT in one synchronous pass, with no frame presented in between, so nothing drawn on the canvas during that window ever reaches the screen. A notice now goes into the host DOM instead, which the browser paints independently, and yields once so that paint actually happens before the work starts. Because that yield unwinds the stack under ASYNCIFY, the load block is guarded by stftBusy — without it the re-entered main loop calls ComputeSTFTInit again and frees the STFT the suspended call is still building. **Removed EXPORTED_FUNCTIONS from the web link flags.** It replaces emscripten's default export list rather than extending it, so everything unnamed is dead-code-eliminated. The upload callback stays reachable via the EMSCRIPTEN_KEEPALIVE already on its definition. **Adds serve_web.py**, a dev server that sends no-store. Browsers cache .wasm hard enough that a plain reload runs a stale module while the page looks freshly loaded — which makes a rebuild appear to change nothing. That cost most of a debugging session today: three separate fixes were tested against a binary that never changed, each returning an identical fault at an identical address. README now points at it and says why. Documents the remaining web issue in known_bugs.md: large captures still corrupt text after loading. Leading theory is ALLOW_MEMORY_GROWTH reallocating the heap mid-load and invalidating pointers cached across that moment, which fits the symptom being text specifically. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN --- README.md | 8 +++++- build_web.sh | 13 +++++++++- known_bugs.md | 40 +++++++++++++++++++++++++++++ serve_web.py | 27 ++++++++++++++++++++ src/platform.h | 18 +++++++++++++ src/platform_linux.c | 7 ++++++ src/platform_web.c | 60 ++++++++++++++++++++++++++++++++++++++++++++ src/platform_win32.c | 7 ++++++ src/render.c | 22 ++++++++-------- src/spectrogram.c | 35 +++++++++++++++++++++++--- src/ui.c | 16 +++++++++--- 11 files changed, 233 insertions(+), 20 deletions(-) create mode 100755 serve_web.py diff --git a/README.md b/README.md index ee3931b..3a03af4 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,17 @@ prints the install hint for your platform if anything is missing. ```bash source ~/emsdk/emsdk_env.sh # emscripten on PATH ./build_web.sh # emits the bundle to bin/web/ -cd bin/web && python3 -m http.server 8080 +cd bin/web && python3 ../../serve_web.py ``` Then open . +**Use `serve_web.py`, not `python3 -m http.server`.** Browsers cache `.wasm` +and `.js` hard, and a plain reload will happily run a stale module while the +page looks freshly loaded — so a rebuild appears to change nothing and you end +up debugging a binary you already fixed. `serve_web.py` sends `no-store` on +everything, which makes any reload authoritative. + The browser build's filesystem is an in-memory sandbox, so files come in by **drag-and-drop** or the **Open file** button (which drives the host's native file picker and copies the result in). The desktop file browser is bypassed diff --git a/build_web.sh b/build_web.sh index e4ae80b..50df819 100755 --- a/build_web.sh +++ b/build_web.sh @@ -90,11 +90,22 @@ done # the resources dir at runtime, so the resources@resources preload covers it. # INITIAL_MEMORY + ALLOW_MEMORY_GROWTH: the web build now computes the full STFT # up front, so the heap must be able to grow for longer recordings. +# ASYNCIFY_STACK_SIZE: the whole main loop runs under ASYNCIFY, so every yield +# copies the live C stack into this buffer. The 4 KB default is far too small +# for a stack that runs through the render/STFT call chain — overflowing it +# corrupts the heap and surfaces as "memory access out of bounds" at doRewind. # EXPORTED_RUNTIME_METHODS: the file-upload path calls back into C from a # browser event (ccall), and writes the chosen file into MEMFS (FS). Neither is # exported by default in recent emscripten, and omitting them fails only at # runtime, when the user clicks "Open file". -LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s INITIAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS=ccall,cwrap,FS -s EXPORTED_FUNCTIONS=_main,_rspektrum_upload_done" +# +# Do NOT add EXPORTED_FUNCTIONS here. It *replaces* the default export list +# rather than extending it, so everything unnamed gets dead-code-eliminated — +# including the ASYNCIFY rewind machinery this build depends on for its blocking +# main loop. The symptom is a blank canvas after load with "memory access out of +# bounds" at doRewind. The upload callback stays reachable via +# EMSCRIPTEN_KEEPALIVE on its definition instead. +LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s INITIAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS=ccall,cwrap,FS -s ASYNCIFY_STACK_SIZE=1048576" if [ "$BUILD_TYPE" = "debug" ]; then LDFLAGS="$LDFLAGS -g -O0 -s ASSERTIONS=1" diff --git a/known_bugs.md b/known_bugs.md index df95ea0..46269b8 100644 --- a/known_bugs.md +++ b/known_bugs.md @@ -6,6 +6,46 @@ need to decide. --- +## Large WAVs in the browser: text corruption and a long hang + +**Status:** open. Reproduces on the web build only; desktop is unaffected. + +Loading a large capture through the web UI (drag-drop or the Open file button) +produces garbled glyphs in menu titles, tooltips and axis labels, and the page +stops responding for a long stretch. Small files load and render correctly, and +the corruption appears *after* the load rather than at startup — so it is +triggered by the size of the work, not by the build being broken. + +The hang is understood and partly by design: the web build computes its entire +STFT in one synchronous pass (see the `__EMSCRIPTEN__` branch in the main loop), +because the desktop's incremental fill depends on idle main-loop frames the +browser doesn't hand back the same way. A DOM overlay now warns before it +starts, but the page genuinely is frozen until it finishes. A real fix means +chunking that work across frames or moving it to a Web Worker. + +The **corruption** is the unexplained part. The leading theory is heap growth: +the build links with `ALLOW_MEMORY_GROWTH=1`, and a large file forces the wasm +heap to grow mid-load. Growth reallocates the backing `ArrayBuffer`, which +invalidates every cached view and raw pointer held across that moment — so +anything retaining a `char*` or a texture-side pointer from before the growth +would read garbage afterwards. Font glyph data reached through raylib's atlas is +a plausible casualty, which fits the symptom being *text* specifically. + +Worth checking first: +- Whether `INITIAL_MEMORY` large enough to avoid growth entirely makes it go + away. That would confirm the theory cheaply, at the cost of a bigger initial + allocation. +- Whether the font atlas survives a deliberate `sbrk`-forced growth. +- SAFE_HEAP + ASSERTIONS=2 (`build_web.sh` debug path) to catch the first bad + access rather than the downstream symptom. + +**Testing note:** always serve the web build with `serve_web.py`, never +`python3 -m http.server`. Browsers cache `.wasm` hard enough that a plain reload +runs a stale module, which makes rebuilds look like no-ops and has already +burned significant time chasing bugs that were fixed. + +--- + ## Playhead vs. a selection edited mid-playback **Status:** partially addressed; underlying semantics still undefined. diff --git a/serve_web.py b/serve_web.py new file mode 100755 index 0000000..3c76c58 --- /dev/null +++ b/serve_web.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Dev server for bin/web with caching disabled. + +The browser aggressively caches .wasm/.js, and a plain reload (Ctrl+R) will +happily serve a stale module while the page *looks* freshly loaded — which makes +"did my rebuild take effect?" impossible to answer and sends you chasing bugs +that were already fixed. Everything here is served no-store so a rebuild is +always what you get, regardless of how the page is reloaded. +""" +import http.server, socketserver, sys + +PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + +class NoCache(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + super().end_headers() + + def log_message(self, fmt, *args): + pass # quiet; the build script is the interesting output + +socketserver.TCPServer.allow_reuse_address = True +with socketserver.TCPServer(("", PORT), NoCache) as httpd: + print(f"serving bin/web on http://localhost:{PORT}/rspektrum.html (no-store)") + httpd.serve_forever() diff --git a/src/platform.h b/src/platform.h index 76c094f..5658ead 100644 --- a/src/platform.h +++ b/src/platform.h @@ -137,3 +137,21 @@ void Platform_RequestFileUpload(void); * @return true exactly once per uploaded file; false when nothing is pending. */ bool Platform_TakeUploadedFile(char *outPath, int cap); + +/** + * Show a blocking-work notice outside the app's own render surface. + * + * The web build computes its STFT in a single pass with no frame presented in + * between, so anything drawn on the canvas during that window never reaches the + * screen and the page simply appears to hang. This puts a message in the host + * document instead, which the browser paints independently, and yields long + * enough for that paint to happen before the caller starts working. + * + * No-op on desktop, which presents a progress panel normally. + * + * @param message Text to display, or NULL to dismiss. + */ +void Platform_ShowBlockingNotice(const char *message); + +/** Dismiss whatever Platform_ShowBlockingNotice put up. */ +void Platform_HideBlockingNotice(void); diff --git a/src/platform_linux.c b/src/platform_linux.c index d743f61..a1b468a 100644 --- a/src/platform_linux.c +++ b/src/platform_linux.c @@ -116,3 +116,10 @@ bool Platform_TakeUploadedFile(char *outPath, int cap) { (void)outPath; (void)cap; return false; } + +void Platform_ShowBlockingNotice(const char *message) { + /* Desktop draws its own progress panel; nothing to do here. */ + (void)message; +} + +void Platform_HideBlockingNotice(void) { } diff --git a/src/platform_web.c b/src/platform_web.c index 96cc362..cf3f551 100644 --- a/src/platform_web.c +++ b/src/platform_web.c @@ -169,3 +169,63 @@ bool Platform_TakeUploadedFile(char *outPath, int cap) { outPath[cap - 1] = '\0'; return true; } + +/* + * Blocking-work notice. + * + * Drawn into the host document rather than onto the raylib canvas: the STFT + * runs to completion inside one main-loop iteration, so nothing presented on + * the canvas during that window is ever painted, and the page looks frozen. + * The DOM is painted by the browser on its own schedule, so an overlay put up + * here survives the wasm thread being busy. + * + * EM_ASM only *queues* the mutation — the browser cannot paint until control + * returns to its event loop, so we yield briefly afterwards (ASYNCIFY is + * already enabled for the blocking main loop). Without that yield the overlay + * would appear only after the work it was meant to announce had finished. + */ +void Platform_ShowBlockingNotice(const char *message) { + if (!message) { Platform_HideBlockingNotice(); return; } + EM_ASM({ + var msg = UTF8ToString($0); + var el = document.getElementById('rspektrum-blocking'); + if (!el) { + el = document.createElement('div'); + el.id = 'rspektrum-blocking'; + el.style.cssText = + 'position:fixed;inset:0;z-index:9999;display:flex;' + + 'align-items:center;justify-content:center;' + + 'background:rgba(12,12,16,0.88);color:#dfe4ee;' + + 'font:14px system-ui,sans-serif;text-align:center;' + + 'pointer-events:all;'; + var box = document.createElement('div'); + box.id = 'rspektrum-blocking-box'; + box.style.cssText = + 'padding:22px 28px;border:1px solid #6a6a80;border-radius:6px;' + + 'background:#1b1b22;max-width:32em;line-height:1.5;'; + el.appendChild(box); + document.body.appendChild(el); + } + document.getElementById('rspektrum-blocking-box').innerHTML = msg; + el.style.display = 'flex'; + }, message); + + /* + * Yield so the browser can actually paint the overlay before the caller + * blocks. EM_ASM only queues the DOM mutation; nothing is drawn until + * control returns to the event loop. + * + * Under ASYNCIFY this unwinds the C stack and resumes later, so the main + * loop CAN re-enter while this is suspended. The load block guards against + * that with stftBusy — without it, the second pass re-runs ComputeSTFTInit + * and frees the STFT the suspended call is still building. + */ + emscripten_sleep(32); +} + +void Platform_HideBlockingNotice(void) { + EM_ASM({ + var el = document.getElementById('rspektrum-blocking'); + if (el) el.style.display = 'none'; + }); +} diff --git a/src/platform_win32.c b/src/platform_win32.c index a9ea2dd..2487e5b 100644 --- a/src/platform_win32.c +++ b/src/platform_win32.c @@ -143,3 +143,10 @@ bool Platform_TakeUploadedFile(char *outPath, int cap) { (void)outPath; (void)cap; return false; } + +void Platform_ShowBlockingNotice(const char *message) { + /* Desktop draws its own progress panel; nothing to do here. */ + (void)message; +} + +void Platform_HideBlockingNotice(void) { } diff --git a/src/render.c b/src/render.c index 0716490..a32b095 100644 --- a/src/render.c +++ b/src/render.c @@ -624,7 +624,7 @@ static void DrawStatPanel(Rectangle bounds, Rectangle sel) int fontSize = 10; int maxTextW = 0; for (int i = 0; i < lineCount; i++) { - int w = MeasureText(lines[i], fontSize); + int w = MeasureTextScaled(lines[i], fontSize); if (w > maxTextW) maxTextW = w; } int boxW = maxTextW + 20; @@ -657,7 +657,7 @@ static void DrawStatPanel(Rectangle bounds, Rectangle sel) DrawRectangle((int)boxX, (int)boxY, boxW, boxH, (Color){ 0, 0, 0, 200 }); DrawRectangleLines((int)boxX, (int)boxY, boxW, boxH, Fade(YELLOW, 0.6f)); for (int i = 0; i < lineCount; i++) { - DrawText(lines[i], (int)boxX + 10, (int)boxY + 8 + i * 14, fontSize, LIGHTGRAY); + DrawTextScaled(lines[i], (int)boxX + 10, (int)boxY + 8 + i * 14, fontSize, LIGHTGRAY); } } @@ -986,7 +986,7 @@ void DrawCursorReadout(Rectangle bounds) char text[80]; sprintf(text, "%.3fs %.0f Hz %s", timeSec, freqHz, level); int fontSize = 10; - int tw = MeasureText(text, fontSize); + int tw = MeasureTextScaled(text, fontSize); int boxW = tw + 12, boxH = fontSize + 8; // Offset up-right of the cursor; flip to keep it inside the viewport. @@ -996,7 +996,7 @@ void DrawCursorReadout(Rectangle bounds) DrawRectangle((int)bx, (int)by, boxW, boxH, (Color){ 0, 0, 0, 200 }); DrawRectangleLines((int)bx, (int)by, boxW, boxH, Fade(SKYBLUE, 0.6f)); - DrawText(text, (int)bx + 6, (int)by + 4, fontSize, (Color){ 180, 220, 255, 255 }); + DrawTextScaled(text, (int)bx + 6, (int)by + 4, fontSize, (Color){ 180, 220, 255, 255 }); } // ============================================================================ @@ -1022,7 +1022,7 @@ static void DrawMarkerCross(Vector2 p, Color c, const char* tag) DrawLine((int)p.x - 7, (int)p.y, (int)p.x + 7, (int)p.y, c); DrawLine((int)p.x, (int)p.y - 7, (int)p.x, (int)p.y + 7, c); DrawCircleLines((int)p.x, (int)p.y, 4, c); - DrawText(tag, (int)p.x + 7, (int)p.y - 14, 10, c); + DrawTextScaled(tag, (int)p.x + 7, (int)p.y - 14, 10, c); } // Two-point ruler overlay: crosshairs at A and B, a connecting line, and a @@ -1064,7 +1064,7 @@ void DrawMarkers(Rectangle bounds) int fontSize = 10; int maxW = 0; - for (int i = 0; i < n; i++) { int w = MeasureText(lines[i], fontSize); if (w > maxW) maxW = w; } + for (int i = 0; i < n; i++) { int w = MeasureTextScaled(lines[i], fontSize); if (w > maxW) maxW = w; } int boxW = maxW + 16, boxH = n * 14 + 10; // Anchor near B (or A if B is off-view), clamped inside bounds. @@ -1078,7 +1078,7 @@ void DrawMarkers(Rectangle bounds) DrawRectangle((int)bx, (int)by, boxW, boxH, (Color){ 0, 0, 0, 210 }); DrawRectangleLines((int)bx, (int)by, boxW, boxH, Fade(ORANGE, 0.7f)); for (int i = 0; i < n; i++) - DrawText(lines[i], (int)bx + 8, (int)by + 6 + i * 14, fontSize, (Color){ 255, 210, 160, 255 }); + DrawTextScaled(lines[i], (int)bx + 8, (int)by + 6 + i * 14, fontSize, (Color){ 255, 210, 160, 255 }); } // ============================================================================ @@ -1146,7 +1146,7 @@ void DrawSpectrumPanel(Rectangle bounds) Rectangle plot = { panel.x + padL, panel.y + padT, panel.width - padL - padR, panel.height - padT - padB }; - DrawText(hasSel ? "Spectrum (selection)" : "Spectrum (view)", + DrawTextScaled(hasSel ? "Spectrum (selection)" : "Spectrum (view)", (int)(panel.x + 6 * scale), (int)(panel.y + 4 * scale), 10, (Color){ 180, 220, 255, 255 }); float freqSpan = freqHigh - freqLow; @@ -1179,12 +1179,12 @@ void DrawSpectrumPanel(Rectangle bounds) // Axis labels: frequency at the ends, peak dB at top-left of the plot. char lbl[32]; sprintf(lbl, "%.0f Hz", freqLow); - DrawText(lbl, (int)plot.x, (int)(panel.y + panel.height - 12 * scale), 9, GRAY); + DrawTextScaled(lbl, (int)plot.x, (int)(panel.y + panel.height - 12 * scale), 9, GRAY); sprintf(lbl, "%.0f Hz", freqHigh); - DrawText(lbl, (int)(plot.x + plot.width - MeasureText(lbl, 9)), + DrawTextScaled(lbl, (int)(plot.x + plot.width - MeasureTextScaled(lbl, 9)), (int)(panel.y + panel.height - 12 * scale), 9, GRAY); sprintf(lbl, "pk %.0fHz %.0fdB", peakFr, maxDb); - DrawText(lbl, (int)(plot.x + 2), (int)(plot.y + 1), 9, Fade(YELLOW, 0.85f)); + DrawTextScaled(lbl, (int)(plot.x + 2), (int)(plot.y + 1), 9, Fade(YELLOW, 0.85f)); free(power); } diff --git a/src/spectrogram.c b/src/spectrogram.c index 03382c6..4e57752 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -1537,7 +1537,17 @@ int main(int argc, char* argv[]) } // Processing (incremental across frames) - if (app.loaded && !app.stftComputed) { + // + // stftBusy guards re-entry. On web the notice below yields to the + // browser (ASYNCIFY unwinds the stack and resumes later), so the main + // loop can run again while this block is suspended — and the guard + // condition is still true at that point, because stftComputed isn't set + // until the compute finishes. Without this, the second pass calls + // ComputeSTFTInit again and frees the STFT the suspended call is in the + // middle of building. + static bool stftBusy = false; + if (app.loaded && !app.stftComputed && !stftBusy) { + stftBusy = true; #ifdef __EMSCRIPTEN__ // Web build: there are no worker threads, and the desktop path's // overview-then-deferred-high-res fill depends on many main-loop @@ -1546,6 +1556,23 @@ int main(int argc, char* argv[]) // the spectrogram is completely ready as soon as the file loads. ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); app.skipFactor = 1; // full resolution, no overview stride + + // Warn before blocking. The compute below runs to completion inside + // this one loop iteration, so the canvas cannot update and the page + // would otherwise look hung — on a multi-hour capture, for minutes. + // The notice lives in the DOM (see Platform_ShowBlockingNotice), + // which the browser paints even while wasm is busy. + { + char notice[320]; + float mins = app.signal.duration / 60.0f; + snprintf(notice, sizeof(notice), + "Analysing %.1f minutes of audio
" + "%d FFT frames to compute.

" + "The page will not respond until this finishes.", + mins, app.stft.numSegments); + Platform_ShowBlockingNotice(notice); + } + ComputeSTFTIncremental(&app.signal, &app.stft, app.fftSize, 0); // computes every segment AutoScaleAmplitude(&app.stft); GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); @@ -1559,6 +1586,7 @@ int main(int argc, char* argv[]) app.loadingPhase = 0; SaveToCache(); if (app.autocropPending) { ApplyAutoCrop(); app.autocropPending = false; } + Platform_HideBlockingNotice(); #else if (app.loadingPhase == 0) { // Initialize STFT once @@ -1628,6 +1656,7 @@ int main(int argc, char* argv[]) if (app.autocropPending) { ApplyAutoCrop(); app.autocropPending = false; } } #endif // __EMSCRIPTEN__ + stftBusy = false; } #ifndef __EMSCRIPTEN__ @@ -2371,14 +2400,14 @@ int main(int argc, char* argv[]) // Export message notification if (app.exportMessage[0] != '\0') { - int msgW = MeasureText(app.exportMessage, 20); + int msgW = MeasureTextScaled(app.exportMessage, 20); int boxW = msgW + 40; int boxH = 36; int boxX = GetScreenWidth() / 2 - boxW / 2; int boxY = 15; DrawRectangle(boxX, boxY, boxW, boxH, (Color){ 30, 30, 30, 220 }); DrawRectangleLines(boxX, boxY, boxW, boxH, CYAN); - DrawText(app.exportMessage, boxX + (boxW - msgW) / 2, boxY + 10, 20, WHITE); + DrawTextScaled(app.exportMessage, boxX + (boxW - msgW) / 2, boxY + 10, 20, WHITE); } // Hold the export message for a few seconds, then clear it. diff --git a/src/ui.c b/src/ui.c index a532299..8e395fe 100644 --- a/src/ui.c +++ b/src/ui.c @@ -754,7 +754,14 @@ static float g_railPopoutY = 0.0f; // top of the button that opened it // Deferred rail tooltip: set by RailButton during the early sidebar pass and // drawn in the late pass so it floats above the axis labels and spectrogram. -static const char* g_railTipText = NULL; +// +// COPIED, not borrowed. Callers pass TextFormat() results, which point into +// raylib's small ring of static buffers and are explicitly documented to expire +// once TextFormat has been called a few more times. Holding that pointer across +// passes meant the tooltip rendered whatever text had since overwritten the +// slot — visible as garbled glyphs. +static char g_railTipText[192]; +static bool g_railTipSet = false; static float g_railTipX = 0.0f, g_railTipY = 0.0f; typedef void (*IconFn)(Rectangle, Color); @@ -782,7 +789,8 @@ static bool RailButton(Rectangle r, IconFn icon, bool active, bool enabled, // needs the rail width), so anything drawn here is painted over by the // frequency labels and spectrogram. DrawSidebarPopouts emits it later. if (over && tip) { - g_railTipText = tip; + snprintf(g_railTipText, sizeof(g_railTipText), "%s", tip); + g_railTipSet = true; g_railTipY = r.y + r.height * 0.5f; g_railTipX = r.x + r.width; } @@ -845,7 +853,7 @@ bool SidebarCapturesMouse(void) void DrawSidebar(void) { - g_railTipText = NULL; + g_railTipSet = false; HandleSidebarSplitter(); if (app.sidebarCollapsed) { g_railPopout = RAIL_POP_NONE; return; } @@ -1231,7 +1239,7 @@ void DrawSidebarPopouts(void) if (app.sidebarCollapsed) return; // Hovered icon's tooltip, deferred from the early pass (see RailButton). - if (g_railTipText) { + if (g_railTipSet) { float sc = GetUIScale(); float tw = MeasureTextScaled(g_railTipText, 11) + 12 * sc; float th = 18 * sc;