./nob test # build + run all tests
./nob test <filter> # run tests matching substring
./nob examples # build all examples (default)
./nob bench # build synthetic benchmark (output/bench_gui, real window)
./nob release # build release artifacts (.a, .so, combined header)
./nob clean # clean output directories
./nob non_unity # compile each .cpp separately (for IDE)
Po buildzie testy można uruchomić bezpośrednio:
./output/test_slider
./output/test_slider "Value Initialization"
./output/test_slider "[slider]"
SDL GUI to lekka biblioteka GUI oparta na SDL3. Cel: ułatwić tworzenie narzędzi i prototypów desktopowych w C++.
| Warstwa | Elementy |
|---|---|
| Core | GUIManager (kontekst, renderowanie), GUIElement (hierarchia + cache tekstur), TextEditable (selekcja, clipboard) |
| Widgety (22) | Panel, Button, Label, Checkbox, RadioButton, RadioGroup, Slider, RangeSlider, StringGrid, ListView, TextInput, TextArea, ComboBox, TabControl, AnimatedImage, Canvas, ContextMenu, Cursor, ArcContainer, ProgressBar, ScrollArea, ShaderPanel |
| Composite | DialogBox, MessageBox, FileDialog (src/composite/) |
| Editor | EditorWindow, EditorState, PreviewWindow, LayoutImporter, LayoutExporter (src/editor/) |
| Ekrany/okna | ScreenManager (gry), WindowManager (wiele okien systemowych) |
| Zasoby | TextureManager, FontManager, TimerManager, AnimationManager |
| Parsery | JsonParser, SGMLParser, LayoutParser — definicja GUI z JSON/XML |
| Style | Style + Theme — unordered_map<string, array<optional<Style>, 4>> per typ per stan (Normal/Hovered/Pressed/Disabled) |
| Layout | Viewport (NonZero w ctorze GUIManagera) + Anchor (HAnchor/VAnchor enum + marginesy px) + LayoutPass Measure/Arrange (ILayoutManager: AnchorLayout domyślny, StackLayout; layoutChildren() per widget) |
src/ — implementacja (C++23, moduły)
src/composite/ — gotowe dialogi
src/editor/ — edytor wizualny GUI
examples/ — 65 przykładów (00–64); examples/c/ — 10 przykładów C
tests/ — 43 binarki testowe (Catch2)
docs/ — release/ (kanon end-user → dist/docs/), refactor_plan.md, archive/ (nieaktualne)
skills/sdl-gui/ — skill agenta (SKILL.md + references/); `./nob release` kopiuje do dist/skills/
lib/ — Catch2 amalgamated, tinyxml2
SDLApp (src/sdl_app.hpp) inicjalizuje SDL3 i tworzy okno + renderer. Dwa konstruktory:
// CPU renderer (standardowy)
SDLApp app("Tytuł", 800, 600);
// GPU renderer (dla ShaderPanel / Vulkan)
SDLApp app("Tytuł", 800, 600, false, GPU_VULKAN);Dostęp: app.getRenderer(), app.getWindow(), app.getGPUDevice() (tylko GPU).
Destruktor automatycznie sprząta — nie trzeba ręcznie niszczyć.
#include "sdl_app.hpp"
#include "gui_manager.hpp"
#include "theme.hpp"
#include "std.hpp" // zamiast <iostream>, <memory> itd.
int main(int, char**) {
try {
// 1. Inicjalizacja
SDLApp app("Tytuł", 800, 600);
SDL_Renderer* renderer = app.getRenderer();
GUIManager guiManager(renderer, Viewport{800, 600}); // viewport NonZero w ctorze
guiManager.setTheme(Theme::createDefaultTheme()); // KONIECZNE
// 2. Tworzenie widgetów
auto widget = std::make_unique<Panel>(guiManager, x, y, w, h);
widget->setBackgroundColor(ElementState::Normal, {45, 48, 58, 255});
// ... konfiguracja ...
guiManager.addElement(std::move(widget));
// 3. Pętla główna — KOLEJNOŚĆ MA ZNACZENIE
bool quit = false;
SDL_Event e;
while (!quit) {
Uint64 frameStart = SDL_GetTicks(); // do limitowania FPS (patrz app.endFrame)
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) quit = true;
guiManager.processEvent(e);
}
guiManager.update(); // timery, animacje, tooltipy
guiManager.cleanup(); // usuwa elementy z markForDeletion()
SDL_SetRenderDrawColor(renderer, 40, 42, 54, 255);
SDL_RenderClear(renderer);
guiManager.render();
SDL_RenderPresent(renderer);
app.endFrame(frameStart); // cap ~60 FPS — BEZ TEGO pętla kręci się tysiące FPS i zjada 1 rdzeń CPU
}
} catch (const std::runtime_error& e) {
std::cerr << e.what() << std::endl;
return 1;
}
return 0;
}Krytyczne: processEvent → update → cleanup → render — ta kolejność jest obowiązkowa.
Pominięcie update() psuje tooltipy. Pominięcie cleanup() powoduje wyciek elementów z markForDeletion().
Widgety tworzy się jako std::unique_ptr, konfiguruje, potem przekazuje ownership:
// Wzorzec A: stwórz → skonfiguruj → dodaj
auto btn = std::make_unique<Button>(guiManager, 10, 10, 120, 40, "Kliknij");
btn->setBorder(ElementState::Normal, {100, 100, 255, 255}, 2);
btn->setBorderRadius(ElementState::Normal, 8);
btn->setOnClickCallback([](GUIElement*) { /* ... */ });
guiManager.addElement(std::move(btn));
// Wzorzec B: dzieci przed rodzicem
auto panel = std::make_unique<Panel>(guiManager, 0, 0, 200, 100);
panel->addChild(std::move(label));
panel->addChild(std::move(button));
guiManager.addElement(std::move(panel));Uwaga: widgety top-level (dodane do GUIManager) mają współrzędne względem okna.
Dzieci (addChild()) mają współrzędne względem rodzica.
Style ma same optional<T> — brak wartości = dziedziczenie z themu:
Style s;
s.backgroundColor = {45, 48, 58, 255};
s.borderColor = {98, 114, 164, 255};
s.borderWidth = 2;
s.borderRadius = 10;
widget->setStyle(ElementState::Normal, s);
// Skróty z GUIElement:
widget->setBackgroundColor(ElementState::Hover, {60, 63, 73, 255});
widget->setBorder(ElementState::Pressed, {200, 100, 100, 255}, 3);
widget->setTextColor(ElementState::Disabled, {128, 128, 128, 255});Kaskada: m_localStyles[state] → Theme[type][state] → Theme[type][Normal] → m_defaultStyle.
ElementState: Normal, Hover, Pressed, Disabled.
Każdy widget ma własne callbacki. Do komunikacji między widgetami używa się ElementRef<T>:
auto label = std::make_unique<Label>(guiManager, 10, 10, 100, 30, "0");
auto ref = guiManager.makeRef(label.get()); // PRZED std::move!
auto slider = std::make_unique<Slider>(guiManager, 10, 50, 200, 30, 0, 100, 50);
slider->setOnChangeCallback([ref](GUIElement* e) {
auto* s = static_cast<Slider*>(e);
if (s && ref) ref->setText(std::to_string(s->getValue()));
});
panel->addChild(std::move(label));
panel->addChild(std::move(slider));ElementRef sprawdza isElementAlive() przy każdym dostępie — bezpieczny dangling pointer.
Anchor::center() // centruj w rodzicu
Anchor::fill(0) // wypełnij cały rodzic
Anchor::topBar(50, 10, 10) // pełna szerokość, 50px od góry, 10px marginesy boczne
Anchor::bottomBar(50, 10, 10)
Anchor::leftSidebar(60, 70) // szerokość z konstruktora elementu
Anchor::horizontalStretch(5, 5)
Anchor::bottomRightAt(12, 34) // osobne marginesy prawa/dół
Anchor::topCenter(40) // środek poziomy, 40px od góry
Anchor::pinned(HAnchor::Right, VAnchor::Bottom, 0, 0, 12, 34) // escape hatchEnum per oś (HAnchor::{None,Left,Center,Right,Stretch}, VAnchor::{None,Top,Center,Bottom,Stretch}) + marginesy int w px. Brak magicznych floatów: 1px osiągalne, center to wariant (nie 0.5). Silnik: jeden pass Measure/Arrange (ILayoutManager: domyślny AnchorLayout, StackLayout do pasów/kolumn); widgety z własną geometrią nadpisują layoutChildren() (Button: label, Slider: track, ScrollArea: viewport/slidery, TabControl: zakładki, DialogBox/FileDialog: pas przycisków). Parser czyta anchorH/anchorV (none|left|center|right|stretch / none|top|center|bottom|stretch) + marginLeft/Top/Right/Bottom (px) i tworzy od razu docelowy rect (bez dummy (0,0)).
Dla resize: okno z SDLApp("Tytuł", 800, 600, true) (resizable), w pętli obsłuż SDL_EVENT_WINDOW_RESIZED → guiManager.handleResize(w, h) (propaguje do WSZYSTKICH top-level, też bez anchorów; wymiary <= 0 ignorowane — niezmiennik NonZero).
| Problem | Przyczyna |
|---|---|
| Widget się nie rysuje | Brak guiManager.setTheme(Theme::createDefaultTheme()) |
| Tooltip nie znika | Brak guiManager.update() w pętli |
| Elementy nie są usuwane | Brak guiManager.cleanup() w pętli |
| Crash przy callbacku | ElementRef nie utworzony przed std::move() |
| Dziwne pozycje dzieci | Dzieci mają współrzędne względem rodzica, nie okna |
| Zmiana stylu nie działa | Trzeba wywołać markDirty() (settery robią to automatycznie, ale bezpośrednia zmiana Style nie) |
GUIManager::render()→GUIElement::render()wantsDirectRender()?→drawDirect():renderToCache()→m_cachedTexture- Dzieci renderowane z
parent_clip_rect - GPU: elementy używają SDL_gpu przez
m_gpuState
- Hierarchia elementów:
unique_ptr - SDL resources:
SharedTexture/SharedFont(shared_ptrz custom deleterami wsdl_deleters.hpp) - Render cache:
m_cachedTextureinvalidowany przezm_isDirty - Element lifecycle tracking:
m_liveElements(unordered_set) dla bezpieczeństwaElementRef
System osadzania assetów (PNG, TTF) bezpośrednio w binarkach:
- Manifest
assets.embed— lista plików do osadzenia nob.c:build_embedded_assets()—ld -r -b binary→.oz symbolami_binary_<nazwa>_start/_endoutput/embedded_assets.hpp— auto-generowany header zg_embeddedAssets[]TextureManager::loadTextureFromMemory/FontManager::loadFontFromMemory—SDL_IOFromConstMem→IMG_Load_IO/TTF_OpenFontIO- Po załadowaniu działa transparentnie:
loadTexture("assets/button1.png")zwraca cache'owany embedded asset
Optymalizacje wprowadzone 2026-06-21:
getAbsolutePosition()cache:m_cachedAbsPos+m_absPosValid, invalidowane rekurencyjnieprocessHoverTooltip()+processButtonEvent()— wyekstraktowana logika, jednocontains()zamiast podwójnego- Panel i widgety: usunięte nadmiarowe
GUIElement::handleEvent()powodujące podwójny DFS SDL_GetMouseState()→ dane z eventu (SDL3 mamouse_x/yw eventach)- Efekt: przy tysiącach elementów: z kilkuset ms → 16 ms/klatkę
| Kategoria | Szczegóły |
|---|---|
| Język | C++23 z modułami |
| Kompilator | clang++-22 z libc++ (LLVM-23) |
| Zależności | SDL3, SDL3_image, SDL3_ttf, tinyxml2 (wbudowany), Catch2 (amalgamated) |
| Build | nob.c + nob.h v3.8.0, unity build, Nob_Procs do równoległego linkowania |
| Moduły | Prekompilowane std.pcm i std.compat.pcm w modules_cache/ |
| Optymalizacje | Release: -O3 -march=native -flto. Debug: -g -O0 -fsanitize=address,undefined |
| Formatowanie | .clang-format w korzeniu projektu |
SDLRectToFRect()— zamiast manualnychstatic_cast<float>naSDL_RectRenderRect()— opakowanieSDL_RenderRectSetDrawColor(renderer, c)— zamiast rozwlekłegoSDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a)TextureWidth()/TextureHeight()— zamiastSDL_GetTextureSize+static_cast<int>drawRoundedTexturedRect()— renderowanie tekstury z zaokrąglonymi rogami (UV clipping przezSDL_RenderGeometry)
Zasoby muszą być dostępne przez pkg-config sdl3 sdl3-image sdl3-ttf. PKG_CONFIG_PATH=/usr/local/lib/pkgconfig.
- Framework: Catch2 (amalgamated:
lib/catch_amalgamated.hpp) - Helper:
tests/test_helper.hpp/cpp— headless SDL init (okno tworzone jakoSDL_WINDOW_HIDDEN),createMouseEvent(),createKeyboardEvent() - Uruchamianie:
./nob testuruchamia binarki testowe równolegle (dynamiczna kolejka, domyślniemin(16, nprocs)zadań;NOB_TEST_JOBS=<n>zmienia limit). Output każdego testu trafia dooutput/test_logs/<nazwa>.log— przy porażce wypisywany jest ogon logu. Okna SDL w testach są ukrywane przez zmiennąSDL_GUI_HIDDEN=1(ustawianą przez runnera; respektują jąSDLApp,WindowiWindowManager). - Widgety testowane (22): Button, Checkbox, ComboBox, Canvas, ContextMenu, Label, ListView, Panel, RadioButton, RadioGroup, Slider, RangeSlider, StringGrid, TabControl, TextArea, TextInput, AnimatedImage, Cursor, ArcContainer, ProgressBar, ScrollArea, ShaderPanel (CPU)
- Menedżery (4): FontManager, TextureManager, TimerManager, AnimationManager
- Systemy (11): GUIElement, GUIManager, Theme, Easing, UTF8, Anchor (enum H/V + marginesy px, presety, Viewport NonZero, LayoutPass, StackLayout), Style, TextEditable (bazowa klasa przez podklasę testową), RenderCache, RenderPixel (pikselowa walidacja renderowania), Performance
- Screen/Window (2): ScreenManager, WindowManager
- Parsery (3): JsonParser, SGMLParser, LayoutParser (fixture'y w
tests/data/—layout.json,layout.xml,widgets.json,win95_bevel.json/xml,bad.*) - C API (1): test_sdl_gui_c_api (Phase 0+1+2+3; + pixel test renderowania kursora — pozycja myszy ze syntetycznego motion eventu, bez warpowania wskaźnika)
Testy integracyjne: 65 przykładów (examples/, 00–64) + 10 przykładów C (examples/c/) do manualnej weryfikacji wizualnej.
- Stwórz
src/nazwa_widgetu.hpp+src/nazwa_widgetu.cpp, klasa dziedziczy poGUIElement - Zaimplementuj
draw()(rysowanie do cache) lubwantsDirectRender()+drawDirect() - Nob.c automatycznie wykrywa nowe pliki w
src/,src/composite/,src/editor/ - Stwórz
examples/example_nazwa_widgetu.cpp, uruchom./nobi przetestuj - (Opcjonalnie) Stwórz
tests/test_nazwa_widgetu.cppzTestHelper, uruchom./nob test
#define CATCH_CONFIG_MAIN
#include "lib/catch_amalgamated.hpp"
#include "tests/test_helper.hpp"
#include "nazwa_widgetu.hpp"
TEST_CASE("NazwaWidget - opis", "[tag]") {
TestHelper helper;
GUIManager& manager = helper.getManager();
SECTION("nazwa sekcji") {
REQUIRE(warunek);
}
}Uruchom: ./nob test
- Problemy z zasobami: sprawdź ścieżki i logi
SDL_LogErrorz menedżerów - Widget się nie rysuje: sprawdź flagę
m_isDirty - Skomplikowany build:
./nob non_unity— kompiluje każdy.cpposobno, łatwiej znaleźć błędy
- Co: (1)
Button::setText/getText— podmiana (lub leniwe utworzenie, gdy caption był pusty) wycentrowanego Label-dziecka +layoutChildren/markDirty; surowyLabel*ZOSTAJE (ownershipunique_ptr+ lifetime związany z rodzicem, jakButton*w Sliderze;weak_ptrwymagałbyshared_ptrw całym libie — libowy odpowiednik słabej referencji toElementRef/handle'e). Przy okazji naprawiło to przykład wdocs/release/managers.md:237, który używał nieistniejącegosetText. (4) Nowysrc/ui_helpers.{hpp,cpp}— baza wolnych funkcji:linkLabel(prefix/suffix i formatter) +linkRangeLabel(jw.), odświeżenie natychmiastowe, label trzymany przezElementRef; uwaga "jedno wiązanie na widget" (nadpisuje jaksetOnChangeCallback— złapane testem).ui_helpers.hppdopisane dohpp_orderwnob.c. (5)GUIManager::after(ms,fn)/every(ms,fn)/cancelTimer(id)— cienkie wrappery naTimerManager(targetnullptr, wzorzec już używany w testach); NIEpowiązane z lifetimem widgetów (udokumentowane) + sekcje wmanagers.md/patterns.md/skillu. Dogfood: 62_memory przepisane z ręcznego deadlinu naafter(700)z guardemdealIdna restart. - Generatory w ui_helpers (dogrywka tego samego dnia):
gridPanels(siatka Paneli, dzieci lub top-level),faceGrid(Button + centrowany Label-face — obchodzi brakButton::setTextw grach),makeStrip<T>(szablon z fabryką(manager,x,y,w,h,index)na paski widgetów),shuffledPairs(n)(talia do Memory). Wpadki: overloadcreate<T>(parent,…)wymaga ctora(manager,parent,…)— żaden widget tak nie ma, więc impl przezmake_unique+addChild/addElement; szablon zGUIManager&wymaga pełnego typu →ui_helpers.hppincludujegui_manager.hpp(stripowany w amalgamacie), a wpis whpp_orderwylądował ZAgui_manager.hpp. Dogfood: 59→gridPanels, 60→faceGrid(cyferki centrowane zamiast (12,6)), 62→shuffledPairs. - Efekt: 46/46 testów (nowe:
test_ui_helpers.cpp— 3 sekcje bindingów + 4 sekcje generatorów; case'y setText wtest_button.cpp;after/everywtest_timer_manager.cpp), 65/65 przykładów, smoke 59/60/62 headless czysty. - Zmienione pliki: src/button.{hpp,cpp}, src/ui_helpers.{hpp,cpp} (nowe), src/gui_manager.{hpp,cpp}, tests/test_button.cpp, tests/test_ui_helpers.cpp (nowy), tests/test_timer_manager.cpp, examples/{59_snake,60_minesweeper,62_memory}.cpp, nob.c, docs/release/{widgets/Button,managers,patterns}.md, skills/sdl-gui/references/gui-app.md
- Co: audyt wszystkich 65 przykładów (3 subagenty po ~20 plików, weryfikacja grepem) z progiem uniwersalności 3+ użycia w różnych domenach. Nowe w
ui_helpers: Pakiet A (fundament) —addTracked<T>(fuzjamakeRef-przed-move+add, zwracapair<T*, ElementRef<T>>; zabija udokumentowany crash-pitfall, 40+ call sites w 20+ plikach),addButton/addLabel(one-linery z tekstem/callbackiem/kolorem),styleCard(kanoniczny ciemny blob z defaultami) +addDarkPanel(fabryka). Pakiet B (formularze) —addLabeledCheckbox(box + caption, opcjonalny kolor tekstu — potrzebny na ciemnych panelach) +makeTopBar/makeBottomBar(structStatusBar{bar,label}, label-dziecko z insetem 8px). Pakiet C (siatki+suwaki) —makeGrid<T>(fabryka(manager,col,row,x,y,w,h), spójna zmakeStrip; uogólniagridPanels/faceGrid),onAnyChange(wspólnyrefresh()+ natychmiastowe odpalenie, overloady Slider/RangeSlider),bindSliderValue(int + float ze skalą — rodzeństwolinkLabel). Konwencja jak dotąd: impl z(manager, parent*)+ cienkie publiczne overloady; fabryki szablonów w hpp. - Niche odrzucone: status
"Selected: …"(16,17), kolumna Add/Remove/Clear (16,17 — kryjemakeStrip), theme-switcher RadioGroup (27,28), d-pad focus-nav (43,44), bochenki ShaderPanel (40,41), captiony nad/pod sliderem (niespójna geometria),setTooltip-jednolinijkowce. 48 (Win95, status bar na anchorach) i 29 (Anchor::bottomBar) świadomie nietknięte — helpery barów nie wspierają anchorów. - Efekt: 46/46 testów (
test_ui_helpers.cpp: +7 TEST_CASE — tracked/card/builders/grid/sliders/status bars, asercje przezgetComposedStylei syntetyczne click-eventy), 65/65 przykładów, smoke 10 migrowanych headless czysty (wycieki 42/44 zSDL_X11_SetWindowTitle— szum SDLa, nie nasz kod), release zielone. - Zmienione pliki: src/ui_helpers.{hpp,cpp}, tests/test_ui_helpers.cpp, examples/{04_text_input,05_slider,06_checkbox,10_range_slider,36_file_dialog,42_mobile_touch,44_tv_remote,49_arc_sliders,51_bounce_lab,55_gravity_box}.cpp, docs/release/patterns.md, skills/sdl-gui/references/gui-app.md
- Co: 4 gry bez nowych zależności — 59_snake (siatka 20x14 Paneli, strzałki/WASD, slider kroków/s, repaint tylko na kroku logiki), 60_minesweeper (9x9 Buttonów + Label-faces jak Button nie ma setText; LPM z flood-fill, PPM flaga przez
setOnRightClickCallback), 61_breakout (mysz steruje paletką, fizyka kulkidt, cegły chowane przezsetVisible(false)żeby surowe wskaźniki nie wisiały, slider prędkości), 62_memory (16 kart, mismatch wraca po 700 ms przez deadline w pętli, restart tasujestd::shuffle). 2 przykłady 3rd-party: 63_mixer_beeps (SDL3_mixer: syntezowane WAV-y sinus w pamięci →MIX_LoadAudio_IO+MIX_PlayAudio, slider →MIX_SetMixerGain, klawisze 1/2/3), 64_highscores_sqlite (systemowy sqlite3:TextInput+ slider wyniku → INSERT, TOP 5 w multiline-Label,highscores.dbw cwd). - Lekcje buildowe: (1) zero dodatkowych include'ów STL (
<deque>/<vector>/<algorithm>/<random>też sypiąrequires clause differs— wszystko jest wstd.hpp); (2) callbacki klików MUSZĄ braćGUIElement*(lambdy[](){}się nie konwertują); (3)MIX_DestroyMixerPRZED końcemtry— dtorSDLAppwołaSDL_Quiti niszczenie miksera po nim to SEGV wMIX_StopAllTracks(złapane ASan-em). - nob.c: opt-in zależności per przykład (reszta binarek szczupła) —
*mixer*dostaje flagi zpkg-config sdl3-mixer,*sqlite*dostaje-lsqlite3(sqlite3-dev już w systemie, nic nie instalowano);cc -o nob nob.cpo edycji. - Efekt: testów nie ruszano (lib bez zmian), 65/65 przykładów się buduje, smoke headless 59–64 (timeout = pętla chodziła; 64 utworzył
highscores.db, 63 po fixie czysty ASan na dummy-audio). - Zmienione pliki: examples/59_snake.cpp … examples/64_highscores_sqlite.cpp (nowe), nob.c, AGENTS.md, docs/index.md, skills/sdl-gui/SKILL.md (liczniki 59→65)
- Co: 10 przykładów 100–170 linii, każdy pokazuje nietypową, prostą interakcję ze sliderami: 49_arc_sliders (3 poziome slidery obrócone na łuku przez
ArcContainer::addChildAtAngle(rotate=true)mieszają RGB podglądu; środkowy stoi pionowo), 50_servo_panel (slidery X/Y/rotacja jeżdżą panelem jak serwami wprost przezsetPosition/setRotation), 51_bounce_lab (ręczna integracjadtzSDL_GetTicks: slider prędkości w px/s + slider rozmiaru kulki + pauza), 52_range_remap (RangeSliderdefiniuje okno [lo,hi], zwykły slider mapuje 0–100 w to okno,ProgressBarpokazuje wynik), 53_smooth_follow (wzór "smooth follow" bez AnimationManagera:cur += (target-cur)*min(1,dt*speed)), 54_brush_mixer (slidery R/G/B przezbrajająCanvas::setPenColorna żywo + swatch + Clear), 55_gravity_box (pionowy slider grawitacji + slider tłumienia odbicia, fizyka w pętli), 56_dial_morph (slider kąta kręci wskazówką przezsetRotationCenter, slider promienia wołaArcContainer::setRadiusna żywo), 57_text_scaler (slidery rozmiaru/rotacji/jasności labelki przez jeden wspólnyrefresh()budujący pełnyStyle), 58_equalizer (5 par pionowy slider + pionowyProgressBarjako VU-metry z wobblesin,ElementRefw wektorach). - Lekcja buildowa: nie doklejać
#include <vector>/<cmath>—std.hppjuż je zawiera, a mieszanie z modułami (import std) sypierequires clause differsw libc++. - Efekt: testów nie ruszano (kod lib bez zmian), 59/59 przykładów się buduje (
./nob exampleszielone), smoke headless 49/55/58 (4 s każdy,SDL_GUI_HIDDEN=1, exit przez timeout = pętla chodziła, zero crashy). - Zmienione pliki: examples/49_arc_sliders.cpp … examples/58_equalizer.cpp (nowe), AGENTS.md, docs/index.md, skills/sdl-gui/SKILL.md (liczniki 49→59)
- Co: jeden kanoniczny preset Win95 —
createWin9xTheme()to teraz cienki alias wołającycreateWindows95Theme()(bez[[deprecated]], żeby nie zaśmiecać buildu warningami z C-API; sama adnotacja słowna),Theme::createDefaultTheme()deleguje do kanonu. Paleta kanonu podpięta podconstants::kWin95*(nowekWin95Navy), kolory-widma{64}/{160}/{212,208,200}wycięte. Checkbox dostał wygląd Win95: białe pudełko + Sunken, check wtextColor(Normal czarny via default, Disabled szary z zachowanym sunken — Hover/Pressed fallbackują do Normal jak w oryginale). Domknięte pokrycie: jawne flat-FaceDialogBox/FileDialog+ komentarz o świadomie pominiętych typach. Koniec nadużyciaborderColor: Slider/RangeSlider →thumbColor, ProgressBar →fillColor(te same piksele przez fallback, poprawna semantyka). C-API: nowesdlgui_theme_windows95(),sdlgui_theme_win9x()to alias. Przykłady 27/28 dostały trzeci radio "Win95". - Efekt: 45/45 testów (przepisana sekcja Default-Button na semantykę "Pressed = ten sam bg, inny bevel", nowy
TEST_CASE "Win9x alias parity"pinujący identyczność styli alias/kanon/default, ProgressBar nafillColor, nowe case'y checkbox/DialogBox, pikselowy test checkboxa wtest_render_pixel.cpp: biały środek + rogi Sunken TL-Shadow/BR-Highlight/inner-TL-black), 48/48 przykładów, release (standalone + C smoke) zielone. - Zmienione pliki: src/theme_presets.hpp, src/theme.cpp, src/constants.hpp, src/sdl_gui.{h,c_api.cpp}, tests/test_theme.cpp, examples/27_themes.cpp, examples/28_theme_playground.cpp, docs/release/{core,resources,patterns,c_api}.md, docs/refactor_plan.md, docs/win9x_theme_unification.md, skills/sdl-gui/references/gui-app.md, readme.md
- Co:
renderFocusOverlay()— obrys fokusu wyekstraktowany zrender(); usunięta wpiekana gałąź fokusu zrenderToCache()(zmiana fokusu nie unieważnia cache'a treści). Usunięte mylącem_isDirty = falsew gałęziwantsDirectRender. Bugfix przy okazji: direct-dziecko rotowanego rodzica było NIEWIDZIALNE (pętla dzieci zgate'owana rotacją + bake kompozytował stubdraw()); teraz bake pomija direct-dzieci, a one renderują się osobno bez rotacji. - Regresja AABB i fix (ten sam dzień): pierwsza wersja rysowała obrys osiowo (AABB) dla rotowanych — w demie ArcContainera wyglądało to jak błąd (ramka nie podążała za obrotem buttona). Fix: obrys rotowanego trzymany w OSOBNEJ teksturze
m_focusTexture(sam obrys, lokalne coords; odbudowa wrenderToCache()— te same inputy co cache treści; niezmiennik: istnieje ⟺ element rotowany), blitowanej z IDENTYCZNYMI parametrami co treść (SDL_RenderTextureRotated, ten sam dst/center) — wizualnie 1:1 ze starą wpiekanką, ale zmiana fokusu nie tyka cache'a treści. Bake rotowanego rodzica kompozytuje też focus-textury dzieci z fokusem (stary kod to miał via tekstura główna). Nierotowane bez zmian (direct draw, zero tekstur). - Efekt: 45/45 testów (2 pikselowe w
test_render_pixel.cpp: outline na rotowanej krawędzi niesymetrycznego panelu 120x60@90° — punkt leży W ŚRODKU AABB, więc fail i na braku outline'u, i na wersji AABB; czerwone direct-dziecko na niebieskim rotowanym rodzicu), 48/48 przykładów (demo ArcContainera startuje, brak crasha), release zielone. Bench: runda niemiarodajna (sama sekcjaframe_loop+70/+120% przy reszcie w szumie, scena bez rotacji/fokusu — delta kodu to null-reset(); winny present na żywym desktopie, nie kod) — neutralność z konstrukcji: gorąca ścieżka sceny bez zmian. - PUNKT 6 ZAMKNIĘTY (6/6 plastrów: OverlayStack, TextShaper, koniec mutacji w draw(), LRU bajtowe, precyzyjny dirty, rotacja/blit).
- Zmienione pliki: src/gui.{hpp,cpp}, tests/test_render_pixel.cpp, docs/release/core.md
- Co: reguła — cached texture zależy tylko od własnych inputów
draw()(klucz cache'a nie miesza dzieci ani pozycji — zaudytowane;draw()w lokalnych coords). Stąd:markDirty(cascade)brudzi self + wyłącznie rotowanych przodków (nowy helpermarkBakedAncestorsDirty()— tylko oni wpiekają dzieci we własną teksturę); koniec kaskady do root przy każdymmarkDirty()(~40 call sites naprawionych jednym ruchem).setPosition: early-out + zero self-dirty (cache niezależny od pozycji; drag Panela nie przerysowuje już łańcucha przodków na każdy MOTION) + rotowani przodkowie.setSize: early-out przy tych samych wymiarach (wcześniej zawsze dirty — m.in.AnchorLayout, slidery, tooltip).addChild/clearChildren/cleanup:markDirty(false)(dodanie/usunięcie dziecka nie zmienia pikseli rodzica). Nieruszone:markDirtyRecursively(theme — semantycznie O(n)),wantsDirectRender(redraw co klatkę z natury),setVisible(już no-op), focus-outline (poza cache; rotowany-focus przezonFocus*self-dirty jak dotąd). - Przy okazji: usunięty pojedynczy U+00AD (soft hyphen) zawleczony we własnym komentarzu w
gui.hpp(weryfikacja bajtowa: 0 w dotkniętych plikach). - Efekt: 45/45 testów (poprawiony
setPosition() marks element dirty→ nowa semantyka + 6 nowych sekcji: no-op same-position/same-size, brak propagacji do zwykłych przodków, rotowani przodkowie przymarkDirtyisetPosition, brak kaskadyaddChild), 48/48 przykładów, release zielone. Bench vspre6.json:frame_loop+0.8/+0.1/+5.3% (neutralnie-do-plus; eventy w zwykłym szumie). - Zmienione pliki: src/gui.{hpp,cpp}, tests/test_gui_element.cpp, docs/release/core.md
- Co: wpisy wszystkich trzech cache'y (
texture/render/text) toCacheEntry{texture, bytes, lastUse}— bajty =w*h*4(render-cache z parametrów, reszta przezSDL_GetTextureSize). Każde wstawienie księguje bajty i egzekwuje budżet (enforceByteBudget(): wyrzuca martweuse_count==1od najstarszego użycia, we wszystkich mapach naraz); żywe wpisy nigdy nie wypadają (budżet może chwilowo overshootować). Hit odświeża recency we WSZYSTKICH ścieżkach (loadTexture,loadTextureFromMemory, obacreateTextureFromText,renderCache,addTexture, constgetTexture—lastUse/zegar sąmutable, sygnatury API bez zmian). Nowy limitkDefaultByteBudget = 128 MiB+setByteBudget()(egzekwuje natychmiast) /getByteBudget()/getBytesUsed();pruneUnused()/clearCache()odejmują/zerują bajty.GUIManager::update()tripuje prune'agetBytesUsed() > getByteBudget()zamiast samego licznika (próg 256 wpisów zostaje jako hygiene-backstop). - Efekt: 45/45 testów (nowy
byte-budget LRU: accounting, eksmisja najstarszego martwego, zakaz eksmisji żywych, hit-odświeża-recency przez porównanie surowych wskaźników, shrink-budżetu,pruneUnusedzeruje bajty), 48/48 przykładów, release zielone. Bench vspre6.json:frame_loop−0.4/−2.8/−1.0% (szum; koszt to bump licznika na hicie + query rozmiaru na insert — na ścieżce klatki dominują hity). - Zmienione pliki: src/texture_manager.{hpp,cpp}, src/gui_manager.cpp, tests/test_texture_manager.cpp, docs/release/managers.md
- Co:
ComboBox::createDropdownButtons()buduje się eager wtoggleDropdown()(expand) iaddItem()(gdy rozwinięte) — zdraw()wycięty blokaddChild/setSize/markDirtyw środku passa renderu; usunięta flagam_needs_update.ContextMenu::createMenuButtons()buduje się eager waddItem/addSeparator/clearItemsgdy menu widoczne (ścieżkashowAtbez zmian — już była w event path);draw()to czysty no-op.TextAreaświadomie nietknięta: jejrecalculateLines/refreshTextureswdraw()dotyka tylko własnych wektorów (bez hierarchii, bezmarkDirty— idempotentny self-cache, potrzebny też w handlerach klików do hit-testu). - Efekt: 45/45 testów (nowe case'y
No draw-path mutation: buttony ComboBoxa i pozycje menu widoczne przezfindElementAt/dzieci panelu BEZrender()— na starym kodzie fail), 48/48 przykładów, release zielone. Bench vspre6.json:frame_loop−1.7/−0.7/−0.9% (neutralnie — scena nie używa combo/menu; zysk to brak hitchy pierwszej klatki menu, niemierzalny obecną sceną). - Zmienione pliki: src/combobox.{hpp,cpp}, src/context_menu.cpp, tests/test_combobox.cpp, tests/test_context_menu.cpp
- Co:
TextureManager::m_textCache(uint64 → SharedTexture) — jeden współdzielony magazyntext/font/color → texturezamiast string-kluczy (text|ptr|rgbabudowanych per wywołanie, czyli per klatkę dla ComboBox/ProgressBar/StringGrid). Klucz: FNV-1a po bajtach tekstu + tożsamość fontu + kolor, zero alokacji na hicie. Oba publiczne overloadycreateTextureFromText(API bez zmian) chodzą po tym samym store; lifecycle 1:1 jak dotąd (pruneUnused/clearCacheobejmują nowy magazyn, trigger progu bez zmian) + nowegetTextCacheSize().StringGrid: skasowany nielimitowanym_localTextureCache(klucz bez fontu — latentna kolizja) icreateLocalTextTexture/clearLocalTextureCache; helpery rysujące biorąconst SharedFont&i wołają managera (font domieszany do klucza). - Efekt: 45/45 testów (nowy case
TextureManager TextShaper: sharing, rozróżnienie koloru/fontu/tekstu, 100× ten sam tekst → 1 wpis), 48/48 przykładów, release zielone. Bench vspre6.json(visible, load ~5):frame_loop−0.3/+0.1/+1.3% (neutralnie — scena bench była już cache-hit; zysk to tempo alokacji + bound pamięci, nie fps). Uwaga: jeden przelotny fail suity (44/45) nie do odtworzenia w 3 kolejnych runach — flake. - Zmienione pliki: src/texture_manager.{hpp,cpp}, src/string_grid.{hpp,cpp}, tests/test_texture_manager.cpp, docs/release/managers.md
- Co: nowe
src/overlay_stack.{hpp,cpp}— cache nieposiadających widoków (GUIElement*) na top-level elementy zisOverlay()==true. Ownership i kolejność Z zostają w jednymm_elements(brak przenoszeniaunique_ptr); overlay-passrender(),getActiveOverlay()i focus-modal chodzą po cacheO(K)zamiast skanuO(N). Invalidacja:addElement/detachElement/cleanup+ publicznenotifyOverlayChanged()(dynamiczny flipStringGrid::isOverlay()przystart/stopEditing). Pierwszy pass renderu celowo nadal filtruje live (poprawność przy dynamicznym statusie). Zmiana semantyki: przy wielu overlayachgetActiveOverlay()zwraca górny (ostatni), nie dolny — poprawny modal (test: dwa dialogi, focus w górnym, po close wraca dolny). - Przy okazji:
overlay_stack.hppdopisane dohpp_order/includes_to_removewnob.c(jakcomponent_type.hpp— inaczejdist/sdl_gui.hppwołał niezdefiniowany typ);nobtrzeba przebudować (cc -o nob nob.c) i wymusić regenerację dist touche'm — sama zmiananob.cnie invalidujedist/sdl_gui.hpp. - Efekt: 45/45 testów (nowy case
GUIManager OverlayStack: modal-Tab, topmost-wins, grid editing-flip), 48/48 przykładów, release (standalone + C smoke) zielone. Bench vsbench/pre6.json(3 runy visible, sober):frame_loop±1% na 100/200/1000w (neutralnie, zgodnie z analizą); sekcje eventowe pływają ±40% run-to-run na tym samym binarniku (szum, nie regresja). Uwaga: run z--hiddenucinaframe_loopdo ~57 fps (vsync na ukrytym oknie) — porównywać tylko runy visible. - Zmienione pliki: src/overlay_stack.{hpp,cpp} (nowe), src/gui_manager.{hpp,cpp}, src/string_grid.cpp, tests/test_gui_manager.cpp, nob.c
- 1b Focus: nowe
GUIManager::requestFocus()— jedyna droga widgetów do brania fokusu (polityka: focusable + enabled + visible + alive;nullptrzawsze czyści). Migracja: Button/Checkbox/TextInput/TextArea.setKeyboardFocus()zostaje mechanicznym setterem (testy/Tab go używają). Click-outside w managerze sprawdza teraz focusowalność głębokim hit-testem (findElementAt+ spacer w górę pocanGetKeyboardFocus— klik w Button-z-Labelką trzyma fokus), więc klik w Panel/Label czyści fokus; zdublowany handler w TextArea usunięty (został lokalny reset hover/drag bez wczesnegoreturn, żeby event doszedł do managera). - 1b Capture:
PointerCapture{autoReleaseOnUp}— manager zwalnia capture po LEFT-UP nawet gdy właściciel zapomniałreleaseMouse()(podwójny release jest cichy). Drag poza bounds działał już przez forward capture — dopięty testem (Button + ruch poza + UP poza → brak klika, capture null). - 1b FocusScope:
ContextMenu::{showAt,hide}trzymam_returnFocus(handle generacyjny): show snapshotuje fokus spoza menu, hide przywraca go gdy fokus jest w menu (zamiast gołego clear — koniec ducha focus-overlay i utraty fokusu pola tekstowego po kliknięciu itemu). - Unifikacje (testy poprawione): ukryty TextInput ignoruje klik/typing jak TextArea i baza (koniec testów przypinających buga „hidden still receives focus"); disabled-odstępstwa z 1a domknięte polityką requestFocus.
- 1c Cursor:
Cursor::handleEventskasowany →updatePosition(event)(MOTION + BUTTON — jedno źródło prawdy) +getPosition()+render(); manager karmi pozycję centralnie na górzeprocessEvent()(skasowane oba forwardy: gałąź capture i ogon), render przezrender(). Dziedziczenie poGUIElement, slot lifetime,ElementRef, C-API (sdlgui_cursor_*,get_type=="Cursor") i przykład 33 bez zmian — świadomie, dla stabilnego ABI. Jedyny fallbackSDL_GetMouseStateto udokumentowany cold-start (przed pierwszym eventem). - Efekt: 45/45 testów (nowe:
requestFocuspolityka 4×, click-outside panel/button, FocusScope-restore, cursor-pod-capture + autoRelease), 48/48 przykładów. Bench vs baseline: wszystko na + (maszyna cichsza niż przy baseline) — jak w 1a, pojedyncze runy niekonkluzywne; brak sygnatury regresji. - Zmienione pliki: src/gui_manager.{hpp,cpp}, src/context_menu.{hpp,cpp}, src/cursor.{hpp,cpp}, src/button.cpp, src/checkbox.cpp, src/text_input.cpp, src/text_area.cpp, tests/test_cursor.cpp, tests/test_gui_manager.cpp, tests/test_context_menu.cpp, tests/test_text_input.cpp
- Szkielet:
GUIElement::handleEvent()to stały potokpropagateToChildren()→handleSelf()→processRightClick(); widgety nadpisująhandleSelf()(nowe, domyślnie hover+button-state), niehandleEvent().processButtonEvent()liczyinsidez koordynatów eventu (MOTION/DOWN/UP) zamiast zgate'owanegom_isHovered; reszta eventów po staremu.updateHoverState()(protected) — hover/tooltip z MOTION-a w 1×contains(). - Migracja: Button/Checkbox/Panel/RadioButton przeszły na
handleSelf()(DFS i RMB w bazie; Button stracił własnyprocessRightClick, Panel ręczny hover-set). KwalifikowanePanel::handleEvent(StringGrid/ScrollArea/dialogi/...) działają bez zmian — trafiają w bazę + wirtualnyPanel::handleSelf. TextInput/TextArea i reszta celowo nietknięte (plaster 1b/1c). - Unifikacja: disabled Button/Checkbox wchodzi w
Disabledprzy evencie jak reszta widgetów (koniec odstępstwareturn falseprzed DFS); test_button poprawiony. - Efekt: 45/45 testów, 48/48 przykładów. Bench A/B pod obciążeniem (IDE ~60% CPU) niekonkluzywny — szum ±20% w obie strony, sekcje bez eventów (label_text/typing) pływają identycznie, więc brak sygnatury regresji; pliki
/tmp/base_loaded.json(pre-1a, pod obciążeniem) do wyrzucenia po powtórce na idle. Powtórzyć A/B na spokojnej maszynie przed 1b. - Zmienione pliki: src/gui.{hpp,cpp}, src/button.{hpp,cpp}, src/checkbox.{hpp,cpp}, src/panel.{hpp,cpp}, src/radio_button.{hpp,cpp}, tests/test_button.cpp
- Nowość:
bench/bench_gui.cpp+ target./nob bench→output/bench_gui(prawdziwe okno przezSDLApp, flaga--backend vulkanna GPU renderer,--hiddenpod CI). Deterministyczna scena (seed 42): siatka Button/Label/Slider/Checkbox/TextInput/TextArea/Panel-z-dzieckiem, co 5. widget zakotwiczony — 8 sekcji:hover(MOTION/DFS, tylko processEvent),click,slider_drag,label_text(setText),typing(TEXT_INPUT),create_destroy,resize(prawdziweSDL_SetWindowSize+handleResize),frame_loop(mieszany ruch + update/cleanup/render/present, fps + p95). Parametry:--widgets N,--sweep(100/200/1000),--seconds S,--save/--compare F.json. - Baseline:
bench/baseline.json(sweep 5 s, debug+ASan, backend sdl): hover 5.8k/3.7k/0.68k ev/s dla 100/200/1000 widgetów (171 µs → 1.47 ms/ev — superliniowo), frame_loop 278/261/66 fps. Porównanie--comparepokazuje delty % per_sec. Uwagi: pętla celowo bez capa (surowy throughput), run-to-run szum ~15% na sekcjach CPU — porównywać na tej samej maszynie/obciążeniu; release (./nob --release bench) da stabilniejsze liczby; ASan wypisuje leak-noise SDLa na końcu (nieszkodliwe, testy je tłumią przezdetect_leaks=0). - Zmienione pliki: bench/bench_gui.cpp (nowy), bench/baseline.json (nowy), nob.c (
BENCH_DIR,build_bench(), targetbench)
- Nowość (punkt 5 planu):
src/element_handle.hpp—ElementHandle{index,generation};GUIManagertrzyma sloty (generacja rośnie przyunregister, brak ABA przy reużyciu adresu).ElementRef<T>rozwiązuje się przez slot + weryfikacjęraw*(stary kod zisAlive-guardami działa bez zmian, guardy nie są już potrzebne w nowych lambdach). Focus/capture jako handle'e —cleanup()bez spaceruhasAncestorMarkedForDeletion, powiadomieniaonFocusLost/onMouseCaptureLosttylko na żywym obiekcie. Tooltip bez ping-ponga własności (stały panel +setVisible),ContextMenuprzez handle (getContextMenu()nigdy nie wisi),ContextMenu::hide()przezisFocusInside(). - WidgetFactory (
src/widget_factory.{hpp,cpp}): jeden rejestrstring↔ComponentType+ domyślne rozmiary + konstrukcja zWidgetProps(wszystkie skalarne propsy z parsera).LayoutParser::parseNodebuduje tylkoWidgetProps(strukturalne dzieci — zakładki przez nowyTabControl::getTabContent/getTabCount, treść scrolla, kąty łuku — zostały w parserze),PreviewWindow::createWidgetiEditorState::addElement(rozmiary) korzystają z fabryki — koniec 3 kopiiif type==. Podgląd zyskałRadioGroup/RangeSlider/ProgressBar/ScrollArea/ArcContainer(wcześniej cichy fallback doPanela). - Edytor diff:
PreviewWindow::m_widgetMapkluczowane stabilnymEditorElement.id(nie indeksem —onElementDeletedprzychodzi POdeleteElement, stary kod usuwał zły widget); nowesyncAll()— update w miejscu (geometria/style/skalary, focus/scroll/selekcja przetrwają), recreate tylko przy zmianie strukturalnej (typ/items/tabs), przycinanie usuniętych id.refreshElement/removeElementWidgetto wrappery (API przykładu 45 bez zmian). - C-API:
checked_elem<T>(dynamic_cast— podtypy jak Slider-jako-Panel dalej działają) w fazach Button/Label/Panel/Slider/Checkbox/TextInput/ListView/TextArea/ComboBox — zły typ to no-op/default +sdlgui_last_error()(sukces czyści błąd); nowesdlgui_element_is_alive,sdlgui_create_widget(fabryka), warianty*_get_text_buf/*_get_item_text_bufkopiujące do bufora callera (koniec wiszącychc_str()). Reszta faz (Progress/Grid/Tab/Cursor/...) zostaje na surowych castach — do dokończenia w tym samym wzorcu. Świadomy kompromis: uchwyty C zostają surowymi wskaźnikami (stabilne ABI Padre-10 przykładów C), walidacja jest po stronie wywołań. - Przy okazji:
component_type.hpp(i nowe nagłówki) dopisane dohpp_order/includes_to_removewnob.c—dist/sdl_gui.hppnaprawdę samowystarczalny (-I distsyntax-check zielony; wcześniejcomponent_type.hppwisiał jako include od punktu 3). - Efekt: 45/45 testów (nowy
tests/test_lifetime.cpp: handle/ref/focus/capture/ABA, fabryka 19 typów, diff edytora, C-API błędy/buf/factory), 48/48 przykładów + release (standalone, C smoke) zielone. - Zmienione pliki: src/element_handle.hpp (nowy), src/widget_factory.{hpp,cpp} (nowe), src/gui.{hpp,cpp}, src/gui_manager.{hpp,cpp}, src/context_menu.cpp, src/layout_parser.{hpp,cpp}, src/tab_control.hpp, src/editor/editor_state.cpp, src/editor/preview_window.{hpp,cpp}, src/sdl_gui.{h,c_api.cpp}, tests/test_lifetime.cpp (nowy), nob.c, docs/release/c_api.md, docs/refactor_plan.md
- Przyczyna:
SDL_CreateRendererw SDL3 domyślnie wyłącza vsync (SDL_RENDERER_VSYNC_DISABLED), a pętle przykładów nie miały żadnego limitera — przykład 29 kręcił się z ~270 FPS (zmierzone; czysty renderer do 11000+ FPS), co na wielordzeniowym CPU widać jako ~7% (1 rdzeń na 100%). SamSDL_SetRenderVSync(renderer, 1)nie wystarczył — sterownik na X11 go ignorował (nadal 271 FPS przyvsync=1). - Fix:
SDLApp(CPU) iWindowwołająSDL_SetRenderVSync(renderer, 1)po utworzeniu renderera (pomaga tam, gdzie sterownik respektuje vsync + eliminuje tearing) + nowaSDLApp::endFrame(frameStart)— dosypia resztę budżetu 16 ms; nie podwaja throttlingu, gdy vsync zadziałał.SDLApp::run()i przykład 29 jej używają; przykład 29 dostał też brakująceguiManager.cleanup(). Boilerplate pętli w AGENTS.md zaktualizowany. - Efekt: przykład 29 w idle: 14–20% → 0–3% CPU (widoczne okno, build debug+ASan). Pozostałe ~44 przykłady nadal mają starą pętlę bez capa — migracja to 2 linijki (
frameStart+app.endFrame(frameStart)). - Zmienione pliki: src/sdl_app.hpp, src/window.cpp, examples/29_resize.cpp, AGENTS.md
- AGENTS.md → CHANGELOG.md: 8 starszych wpisów (2026-08-22–08-25: Label multiline, tooltip multiline, Win95Theme, Cursor-flake, Button-parser, anchory-przy-add, Bevel, cleanup null-checków) przeniesionych na górę CHANGELOG.md; w AGENTS.md zostały 4 wpisy z 2026-09-05. Usunięty też wiszący link "do 2026-08-02" w środku listy.
- docs/: martwe pliki przeniesione
git mvdodocs/archive/(api/, en/, pl/ — era SDL2; getting_started.md root — SDL2; pigulka.md — usuniętesetWindowSize(); mouse_cursor.md — stara nazwaMouseCursor; propozycje/plany: responsive_layout, text_input_text_area, texture_font_review, wysiwyg — zrealizowane lub zastąpione). Zostały:release/(kanon),refactor_plan.md(w toku),index.md(linki do archive/, poprawione 48→49 przykładów). - Skill: naprawione rozjazdy z kodem po refaktorach —
gui-app.md: podwójna deklaracjaGUIManager(brak Viewport) → jeden ctor zViewport, stara konwencja float-anchorów (0–1/>1/0.5) → enumHAnchor/VAnchor+ int px,setStyle("Button"…)→ComponentType::Button, wymiary pasków/sidebara z ctora;rts-game.md: niekompilujące sięcreate<Label>(bar,…)→make_unique+addChildzmakeRefprzed move,onExit→markForDeletionzamiast gołegocleanup(); SKILL.md: przykłady00–47→00–48. - Dystrybucja:
nob.c build_release()kopiujeskills/sdl-gui/→dist/skills/sdl-gui/(marker: SKILL.md); SKILL.md dostał sekcję instalacji (cp -r <sdk>/skills/sdl-gui <gra>/.kilo/skills/) — skill samowystarczalny, gra nie kopiuje źródeł biblioteki. Walidacjaquick_validate.pyPASS dlaskills/idist/skills/. - Efekt:
./nob releasezielone (dist zawiera skills/),diff -r skills/sdl-gui dist/skills/sdl-guiidentyczne. Testów nie ruszano (bez zmian kodu lib). - Zmienione pliki: AGENTS.md, CHANGELOG.md, docs/index.md, docs/archive/* (git mv), nob.c, skills/sdl-gui/SKILL.md, skills/sdl-gui/references/gui-app.md, skills/sdl-gui/references/rts-game.md
- Nowość:
Viewport{w,h}wstrzykiwany doGUIManagerw ctorze (niezmiennik NonZero — brak0x0, koniecsetWindowSize()i fallbacku800x600wContextMenu;handleResize(<=0)ignorowane, np. minimalizacja).Anchorjako enum per oś (HAnchor/VAnchor+ marginesy int px) zamiast magicznych floatów (<0/0-1/>1/==0.5):1pxosiągalne, center to wariant; noweat/pinned/topCenter/bottomRightAt. JedenLayoutPassMeasure/Arrange (src/layout.hpp:ILayoutManager,AnchorLayoutdomyślny,StackLayoutzarrangeStripdo pasów przycisków).layoutChildren()zamiastonSizeChanged()(Button: label, Slider: track+przyciski, ScrollArea: viewport/slidery — koniec shadowowaniaupdateLayout, TabControl: zakładki+panele, DialogBox/FileDialog: pas przycisków). Usuniętem_originalW/H/storeOriginalSize(center z bieżącego rozmiaru). Parser tworzy od razu docelowy rect (bez dummy(0,0)+setSize), kotwice zanchorH/anchorV+margin*(px).handleResizepropaguje do WSZYSTKICH top-level (fix: rodzic bez anchora blokował resize zakotwiczonych dzieci). DialogBox/FileDialog centrują z realnego viewportu (koniec hardcode800x600), C-API:sdlgui_anchor_t{h,v,l,t,r,b}+sdlgui_anchor_make(koniecanchor_raw/floatów). - Zero shimów: stare API usunięte (flota
Anchor,applyAnchor,onSizeChanged,onParentResize,setWindowSize,AnchorMode,anchorLeft/...w plikach) — poprawione 48 przykładów, fixture'y JSON/XML, C-API i testy.getAbsolutePosition-cache i współdzielony render-cache bez zmian (perf). - Przy okazji (pre-existing, blokowały
./nob release):text_area.cppnie includowałgui_manager.hpp(sypał per-file compile),nob.c—layout.hppw combined header +theme_presets.hpppogui.hpp(undeclaredapplyBevelToStylewdist/sdl_gui.hpp);WindowManager::createWindowłapiestd::exception(okno 0x0 → deterministycznynullptrprzez NonZero Viewport). - Efekt: 44/44 testów przechodzi (test_anchor: 103 asercje/7 case'ów — nowe regresje: 1px, center bez historii, setSize rodzica bez anchora, propagacja przez rodzica bez anchora, brak (0,0) przed resize, NonZero przy resize 0x0, StackLayout dialogów), 48/48 przykładów + release (47_standalone, C smoke) zielone.
- Zmienione pliki: src/anchor.hpp (rewrite), src/layout.hpp/cpp (nowe), src/gui.hpp/cpp, src/gui_manager.hpp/cpp, src/button.hpp/cpp, src/slider.hpp/cpp, src/tab_control.hpp/cpp, src/scroll_area.hpp/cpp, src/context_menu.cpp, src/composite/dialog_box.hpp/cpp, src/composite/file_dialog.hpp/cpp, src/layout_parser.hpp/cpp, src/window.cpp, src/window_manager.cpp, src/gui_context.hpp, src/text_area.cpp, src/sdl_gui.h, src/sdl_gui_c_api.cpp, nob.c, examples/* (48), examples/layouts/, tests/data/win95_bevel.json, tests/test_anchor.cpp (rewrite), tests/test_gui_manager.cpp, tests/test_window_manager.cpp, tests/test_sdl_gui_c_api.cpp, tests/test_text_area.cpp, tests/test_render_.cpp, tests/test_context_menu.cpp, tests/test_helper.cpp, docs/release/{core,resources,managers,patterns,getting_started,c_api}.md + skeletony w docs/release/widgets/*.md
- Nowość:
src/component_type.hpp— enumComponentType:uint8_tjako jedyny klucz typu w libie (stringi"Button"…tylko na granicy: pliki layoutu przezcomponentTypeFromString, C-API przezcomponentTypeToString). Usunięte: wirtualnygetComponentType()ze wszystkich widgetów, stringowa mapa i overloady wTheme(została tablicaO(1)[typ][stan]+epoch()). Klucz render-cache używa ID.GUIElement::getComposedStyle()cache'uje scalony styl per stan, przelicza tylko przy zmianie epoki themu/lokalnej. - Globalny współdzielony cache zachowany:
TextureManager::m_renderCachebez zmian — identyczne widgety dają identyczny klucz i współdzielą 1 teksturę (test: 100 identycznych paneli → 1 wpis; inny rozmiar → nowy wpis). - BorderRenderer:
drawResolvedBorder()jeden kod bevel-vs-plain (bevel ma priorytet, ostry jak Win95);RadioButtonużywa go zamiast własnej kopii brancha (semantyka texture-jako-indykator zachowana).Style::hasBevel(). - Koniec nadużycia
borderColor: nowethumbColor(Slider/RangeSlider uchwyt) ifillColor(ProgressBar wypełnienie) z fallbackiem doborderColor— stare motywy/przykłady działają bez zmian;setThumbColor/setFillColor, klucz cache je domiesza. - Efekt: 44/44 testów przechodzi (nowe case'y sharingu RTS w test_style.cpp), 48/48 przykładów się buduje bez zmian kodu.
- Zmienione pliki: src/component_type.hpp (nowy), src/style.hpp, src/theme.hpp, src/theme.cpp, src/gui.hpp, src/gui.cpp, src/slider.cpp, src/range_slider.cpp, src/progress_bar.cpp, src/radio_button.cpp, tests/test_style.cpp, docs/release/resources.md, docs/release/widgets/Slider.md, docs/release/widgets/ProgressBar.md
- Refaktor:
TextArea : public TextEditable(wcześniej: GUIElementz równoległym API byte-index). Usunięte duplikaty: tekst/selekcja/schowek/focus/blink/menu/locked — wszystko dziedziczone; zostały tylko linie/wrap/scroll/nawigacja góra/dół.setLocked/isLockedisetContextMenuEnabledprzeniesione do bazy (TextInput::setLockeddeleguje doTextEditable::setLocked). Wszystkie pozycje w znakach UTF-8 (fix:erase/insert/strlen/m_cursorPos++/--, mieszanelength()+1zcharToByteIndex,substrwgetSelection). - Edge-case'y zlikwidowane: pisanie wymaga fokusu (było
m_isHovered || hasFocus— hover wystarczał); drag działa poza bounds (hasKeyboardFocuszamiastm_isHovered, jak w TextInput);recalculateLineszawija wszystkie paragrafy (wcześniej tylko ostatni), zachowuje wielokrotne spacje i łamie zbyt długie słowa po znakach;\r\njak pojedynczy break; strzałki na granicach zwracająfalse(jak TextInput); Ctrl+C/V/X/A przez wspólnehandleClipboard*/selectAll. - Przykłady: bez zmian kodu (API kompatybilne —
setText/setOnTextChanged/setWordWrap/setLockedzachowane), 48/48 się buduje. - Efekt: 44/44 testów przechodzi (nowe case'y UTF-8 char-index w test_text_area.cpp).
- Zmienione pliki: src/text_editable.hpp, src/text_editable.cpp, src/text_input.hpp, src/text_input.cpp, src/text_area.hpp, src/text_area.cpp, tests/test_text_area.cpp, docs/release/widgets/TextArea.md, docs/release/widgets/TextEditable.md
- Nowość:
GUIElement::setOnRightClickCallback(fn(element, x, y))— callback prawego przycisku z pozycją kliknięcia (współrzędne okna); RMB jest konsumowane, więc przodkowie nie odpalają swoich callbacków. Widgety nadpisującehandleEvent(Button, TextInput, TextArea) wołają chroniony helperprocessRightClick(e). - Domyślne menu kontekstowe:
GUIManager::showContextMenu(items, x, y)— jedno leniwie tworzoneContextMenuwspółdzielone (przebudowa itemów przy każdym pokazaniu). TextInput i TextArea: RMB nie startuje już drag-selekcji, tylko otwiera menu Wytnij/Kopiuj/Wklej/Zaznacz wszystko z enabled-state zależnym od selekcji i clipboardu; per-widgetsetContextMenuEnabled(false)wyłącza.TextEditabledostaje publicznecopyToClipboard/cutToClipboard/pasteFromClipboard/selectAll(virtual); TextArea nie dziedziczy po TextEditable (byte-index zamiast char-index) — ma równoległe własne API. Akcje menu guardują żywotność widgetu przezisElementAlive(menu może przeżyć widget). - ContextMenu:
positionMenuużywa realnego rozmiaru okna (GUIManager::getWindowSize, fallback 800×600 gdy nieustawione) zamiast hardcode'u — bez tego (rozmiar 0×0) clamp zerował pozycję do (0,0); wysokość liczona z separatorami. GetterygetItemCount()/isItemEnabled(i)dla testów. - Przykład 12: menu otwiera się prawdziwym RMB na pozycji kursora (wcześniej fake przez
setOnClickCallback); przykład wołasetWindowSize(bez niego clamp menu liczył na rozmiarze 0×0 i zerował pozycję do (0,0)). - Bugfix (duch klikniętego itemu): klik w item kradł focus klawiatury (
ButtonrobisetKeyboardFocusprzy BUTTON_DOWN), acloseMenu()go nie oddawało —GUIManager::rendermalował potem przycisk przez focus-overlay mimo ukrytego menu (z ramką focusu, znikał dopiero po kliknięciu gdzie indziej). Fix:ContextMenu::hide()zwalnia focus, gdy wskazuje w głąb menu (focus spoza menu, np. pisane pole tekstowe, zostaje nietknięty). - Efekt: 43/43 testów przechodzi, 48 przykładów się buduje.
- Zmienione pliki: src/gui.hpp, src/gui.cpp, src/gui_manager.hpp, src/gui_manager.cpp, src/context_menu.hpp, src/context_menu.cpp, src/text_editable.hpp, src/text_editable.cpp, src/text_input.cpp, src/text_area.hpp, src/text_area.cpp, examples/12_context_menu.cpp, tests/test_gui_element.cpp, tests/test_text_input.cpp, tests/test_text_area.cpp, tests/test_context_menu.cpp
Starsza historia zmian: CHANGELOG.md.
Po każdej znaczącej zmianie (nowa funkcjonalność, optymalizacja, istotny bugfix, refactor) zaktualizuj sekcję „Bieżący stan i ostatnie zmiany". Wpis powinien być zwięzły i zawierać: datę, co zmieniono, efekt (testy/examples), listę zmienionych plików.
Nie aktualizuj przy zmianach kosmetycznych (formatowanie, nazwy zmiennych bez zmiany logiki) ani tymczasowych branchach eksperymentalnych.
Jeśli zmieniono architekturę (nowy wzorzec, nowy menedżer, nowa warstwa abstrakcji), zaktualizuj również odpowiednie sekcje powyżej.