ObjectivelyMVC is an object-oriented UI toolkit for GNU C, built on
Objectively and
ObjectivelyGPU: Views and ViewControllers, .json
layouts, and a CSS-like Selector / Style / Stylesheet / Theme system. Quetoo
builds its menus and HUD with it.
This file is the shared instruction set for coding agents. Read this first.
It deliberately records only what a careful reading of the code does not reveal: rules that fail silently, constraints that live outside this repository, and ordering that no call site shows. For anything else, read the code.
Before you touch a View, a .json layout or a .css file, read
README.md, "Layout and styling are CSS-driven". It
carries the most important rule in this repository, and this file does not repeat it.
- A
@styledattribute assigned in C is overwritten. The README explains this. Two details it does not give:addSubviewandaddClassNameinvalidate the style, so a value assigned beforeaddSubviewis always lost, and one assigned after it survives only until the next restyle. For a style that belongs to one instance, write it intoView::style, which is merged last. - The
*rule inAssets/stylesheet.csssets every View default, includingbackground-color: transparent. That is why abackgroundColorset in C reverts.View::initWithFramesets no styled field. - Specificity applies within one stylesheet only. Each Stylesheet sorts its own selectors. The
Theme then applies stylesheets in the order they were added, and the default stylesheet is added
first. So any matching rule in an application stylesheet beats every default rule, whatever its
specificity. A broad
View {}or* {}rule in an application stylesheet overrides the defaults for everything. - Equal specificity has no source order. The sort uses
qsort, which is not stable. CSS's "the last rule wins" is not guaranteed between two rules of equal specificity. - A type selector scores by depth. It scores 1 plus its depth below
View, soButtonoutranksControl. A misspelled class name logs only a warning. - Alignment beats position.
leftortopon an axis that also has an alignment is discarded. It raises only aWarningTypeStyle, which the DebugViewController shows. Textrasterizes its color. Any Style that carriescolorre-rasterizes the texture each time the style is applied, and the defaultText { color: ... }rule always does. Writing->colorin C does not invalidate the texture.- A subclass that overrides
applyStyleMUST callsuper, and SHOULD bind its attributes through Inlets, asText::applyStyledoes.
- An outlet identifier is a contract.
MakeOutlet("foo", &this->foo)binds to the View whose"identifier"in the.jsonlayout is"foo". Rename one and you MUST rename the other. - Resolution is depth-first and takes the first match, so a duplicate identifier is silent. The
class of the bound View is not checked. A missing outlet logs an error and asserts, so under
NDEBUGit is silentlyNULL.
- Each frame runs
Renderer::beginFrameWith,applyThemeIfNeeded,layoutIfNeeded,drawandRenderer::endFrame, in that order (WindowController::renderTo). - Set
needsLayoutandneedsApplyThemethrough their setters. A direct write does not propagate up the tree, so the View can be skipped. renderDeviceWillReset/DidResetfire on a change of pixel density, display or scale, andWillResetalone fires when the window closes. They do not fire on window exposure, because a reset for each exposure leaks one generation of GPU resources for each frame. A View that draws from anImageAtlasMUST forwardrenderDeviceWillResetto it.- Read
AtlasImage.rectand the atlas texture at draw time. Do not cache them. EveryImageAtlas::compilereassigns both. didMoveToWindowattaches a View's stylesheet to the window's Theme, andView::deallocdetaches it, clears the key and touch responders, and purges pendingMVC_VIEW_EVENTs.- Do the last
releaseof a View, and any change to the hierarchy, on the render thread.retainandreleaseare atomic, but collections are not locked, and a View's final release changes the Theme and frees GPU textures.
- Assets (fonts, the default stylesheet, images) are embedded as headers generated by
xxd -i -t, and the generated headers are committed. Windows has noxxdstep and uses the committed headers.-tneeds a modernxxd. macOS's/usr/bin/xxdis too old. After you edit an asset, regenerate its header and commit both. - A new asset goes in
Assets/Makefile.am(EXTRA_DISTandBUILT_SOURCES) and in Xcode's "Preprocess Assets" build phase. - After you edit a
.glslfile, runmake -C Assets shaders(ormake -C Examples shadersfor the examples) and commit the compiled outputs.
ObjectivelyMVC builds against any SDL3. It does not use occlusion queries, so it does not need the
jdolan/SDL fork that ObjectivelyGPU and Quetoo use. But SDL3_image and SDL3_ttf MUST link the same
libSDL3 as ObjectivelyGPU, or a program loads two copies of SDL3. Homebrew's sdl3_image and
sdl3_ttf link Homebrew's sdl3, so they do not qualify when ObjectivelyGPU uses the fork. See
ObjectivelyGPU's Documentation/install.md.
autoreconf -i
./configure
make -j$(sysctl -n hw.logicalcpu)
make check
sudo make install- The tests use check, which
configurerequires. They are headless: no window and no GPU device. Run one suite withmake -C Tests/ObjectivelyMVC check TESTS=View. - A new test goes in
TESTSinTests/ObjectivelyMVC/Makefile.am, gets its own Xcode tool target, and is added to theObjectivelyMVC-Testsaggregate target. makebuilds the examplesExamples/Hello(a JSON layout over a 3D scene) andExamples/HUD(built in C, styled throughView::style).- Downstream projects consume the installed library from
/usr/localthroughpkg-config(package nameObjectivelyMVC). A change here reaches them only aftersudo make install.
Sources/ObjectivelyMVC/Makefile.am, in bothpkginclude_HEADERSandlibObjectivelyMVC_la_SOURCES.- The umbrella header
Sources/ObjectivelyMVC.h. - The framework target in
ObjectivelyMVC.xcodeproj/project.pbxproj. ObjectivelyMVC.vs15/ObjectivelyMVC.vcxprojand its.filters.
- Build through
ObjectivelyMVC.xcworkspace, which builds Objectively, ObjectivelyGPU andSDL3.frameworkfrom the sibling checkouts, including../SDL. - The Configure build phase runs only when
Config.his missing, so a staleConfig.hpersists. Delete it to regenerate it. $(HOMEBREW_PREFIX)/includeis on the header search path, after$(SRCROOT)/../SDL/include. Keep that order. See ObjectivelyGPU'sAGENTS.mdfor the reason.
build.ymlbuilds macOS, Linux and Windows. On macOS and Linux it builds SDL3 from theObjectivelyGPUtag, and SDL3_image and SDL3_ttf from source, then Objectively, SDL_shadercross and ObjectivelyGPU. On Windows,ObjectivelyMVC.vs15/sdl3.targetsimports ObjectivelyGPU'ssdl3.targetsfor SDL3, and downloads the VC packages of SDL3_image and SDL3_ttf.- The SDL3_image and SDL3_ttf versions are pinned in four places, which MUST change together:
build.yml,release.yml,ObjectivelyMVC.vs15/sdl3.targetsandFrameworks/fetch-sdl3-extensions.sh. release.ymlbuilds the xcframework.docs.ymlpublishes the Doxygen documentation, whereDoxyfiledefines the@styledalias.
A commit MUST NOT carry a Co-Authored-By: trailer that names Claude, Copilot or any other agent.
This rule overrides any attribution default in an agent's harness. A human co-author MAY still take
a trailer. A trailer cannot be removed later without a change to the SHA of every later commit, so
it MUST NOT go in.