Thanks for helping out. SmiðrUI is a retained-mode UI toolkit for OpenFL — widgets are plain
openfl.display.Sprites that repaint only when invalidated, with no dependency on any game framework.
The conventions below exist to keep it that way: fast, typed, framework-free and consistent. Please
read them before opening a pull request — they are enforced in review.
If you are new to the library itself, start with the Getting started guide.
You need Haxe 4.3+ with Lime, OpenFL and hxcpp (plus flixel + hscript if you touch the Flixel bridge or its example):
haxelib install lime
haxelib install openfl
haxelib install hxcpp
haxelib install flixel # only for the smidr.flixel bridgeThe layout:
src/smidr/ the library
widgets/ UIComponent widgets
overlays/ popup services (tooltip, toast, context menu)
types/ enum-abstract value types and typedefs
input/ focus + pointer plumbing
text/ rich-text modules (styler, markdown, style vocabulary)
flixel/ the optional HaxeFlixel bridge
examples/ self-contained example apps (each with its own project.xml)
doc/ docs site sources + the getting-started guide
Run the typecheck suite from the repo root — this is what CI runs, and it must pass:
haxe check.hxml # the whole library, on the hxcpp (native) target
haxe examples/check.hxml # the OpenFL examples
haxe examples/check-flixel.hxml # the Flixel bridge example (needs -lib flixel)Then, if your change has any runtime behaviour (almost anything that is not a docs/typing-only tweak): build and run at least one example natively and confirm the behaviour — do not rely on a green typecheck alone. A widget can compile and still mis-render or mishandle input.
lime test examples/gallery/project.xml windows # or another example / targetFinally, format your changes with the Haxe formatter so diffs stay clean:
haxelib run formatter -s src- Explicit types on the whole public surface: every field, function parameter and return type is
annotated. Type locals too; the only inference we lean on is obvious one-liners like
var root = UIRoot.current;. - Avoid
Dynamic. If you reach for it, there is almost always a typedef, enum abstract or type parameter that expresses the intent. Avoid reflection entirely in hot paths. - Prefer
finalfor fields that never reassign andinlinefor tiny hot helpers. These lower to tighter native code under hxcpp.
- Full-word identifiers. No single-letter names (
c,n,r,v,g, …) — writecount,radius,value,component, and draw with thegraphicsproperty directly (not avar g = graphicsalias). The only allowed single letters are the conventional ones:i/j/kfor loop indices andx/y/w/hfor position and size. - Widget classes are
UI-prefixed (UIButton,UIPanel). Named ids / value types areenum abstracts insmidr.types(e.g.UIGlyph,UITone,UIFill) so callers writeCHEVRON_LEFT/SECONDARYand they compile to plain ints. - Respect the package split:
smidr.widgets(UIComponents),smidr.overlays(popup services),smidr.types(enum abstracts + typedefs),smidr.input,smidr.text,smidr.flixel.
- Public classes and members get a Haxe docstring (
/** … **/). The widget guide on the docs site is generated from each widget's leading class docstring, so a new widget without one will not appear there. Write full sentences; reference other types with markdown backticks. - No inline
//noise. A comment should explain why, not restate the code. - No decorative separators in code — not box-drawing rules and not
// --- section ---dashes. Let the structure and docstrings do the work.
- The package id, haxelib id and every code identifier use plain
smidr. The stylizedSmiðrUIis only for prose (README, docstring descriptions, UI text) — never in tags, ids or type names.
- Tabs for indentation. Run
haxelib run formatterbefore committing.
These keep the core model intact. A change that breaks one of them will be asked to rework, even if it compiles.
- Widgets repaint only when
invalidate()is called — never callrender()directly to force a repaint after a state change. (Callingrender()once at the end of a constructor for the initial paint, as the built-in widgets do, is the one accepted direct call.) - Read
UIThemecolours andUILocalestrings insiderender(), so a theme or locale swap re-skins the live tree for free. - An idle UI must do zero per-frame work. Do not add
ENTER_FRAMEhandlers or standing tickers that run when nothing is changing. UseUIRoot.addTickeronly while something animates (a caret blink, a hold-repeat) andremoveTickerthe moment it stops. - Use
UITheme.px(n)for pixel metrics andUITheme.fs(n)for font sizes so the global density scale is honoured.
- A general-purpose widget must not carry app- or domain-specific logic. Put policy in an
installable module behind an interface, exposed as a property — e.g.
UITextAreais style-agnostic and defers to an installedsmidr.text.UITextStyler(UIRichStyleris the concrete WYSIWYG module). The dependency points widgets → a small interface, never the reverse.
- The core depends on OpenFL only. The Flixel bridge in
smidr.flixelcompiles only when theflixelhaxelib is present — keep it gated and never pull flixel (or any game framework) into the core widgets.
- Support Windows, macOS, Linux, HTML5, HashLink and Android (iOS must compile at the source
level). Gate form-factor differences with
#if desktop/#if mobile, and per-OS behaviour with#if android/#if ios/#if windows/#if mac/#if linux.
Before/after snippets for the things that come up most in review. The left column is what gets a PR sent back; the right column is what we merge.
// bad: untyped field, untyped setter, and Dynamic
public var value;
function set(v) {
value = v;
}
var data:Dynamic = load();// good: explicit types everywhere, a real model type instead of Dynamic
public var value(default, set):Float = 0;
function set_value(next:Float):Float {
value = next;
invalidate();
return next;
}
var data:NoteModel = load();// bad: terse single-letter locals
for (i in 0...rows.length) {
var r = rows[i];
var n = r.parent.children.length;
}// good: full words (the loop index i is fine; r and n are not)
for (i in 0...rows.length) {
var row = rows[i];
var count = row.parent.children.length;
}// bad: mutating and forcing a paint by hand
public var label:String;
public function setLabel(text:String):Void {
label = text;
render(); // never call render() directly
}// good: a set_* that schedules a repaint
public var label(default, set):String;
function set_label(next:String):String {
label = next;
invalidate(); // repaints on the next frame, deduplicated
return next;
}// bad: colour captured in the constructor; a theme swap will not re-skin it
public function new() {
super(true, true);
graphics.beginFill(UIColor.rgb(UITheme.panel2));
graphics.drawRect(0, 0, w, h);
}// good: drawn in render() from the live palette, straight onto the graphics property
override public function render():Void {
graphics.clear();
graphics.beginFill(UIColor.rgb(UITheme.panel2));
graphics.drawRect(0, 0, w, h);
graphics.endFill();
}// bad: a ticker that runs every frame forever, even when nothing changes
public function new() {
super();
UIRoot.addTicker(step);
}
function step(dtMs:Float):Void {
invalidate();
}// good: the ticker exists only while something animates
function startSpin():Void {
spinning = true;
UIRoot.addTicker(step);
}
function stopSpin():Void {
spinning = false;
UIRoot.removeTicker(step);
}// bad: a decorative divider, a missing class docstring (so it never reaches the widget guide),
// and comments that just restate the code
// ----- Rating widget -----
class UIRating extends UIComponent {
// the value
public var value:Int;
}// good: a class docstring feeds the generated guide; members are documented, not narrated
/**
A star rating with a live hover preview. `onChange` fires with the newly picked value.
**/
final class UIRating extends UIComponent {
/** The selected number of stars (0..`max`). **/
public var value(default, set):Int = 0;
}// bad: a general text editor that reaches into the app note store
class UITextArea extends UIComponent {
function onEdit():Void {
NoteStore.current.body = text;
NoteStore.save();
}
}// good: the widget just reports edits; the app wires the policy
// in the widget:
public var onChange:String->Void = null;
// in the app:
editor.onChange = function(text:String):Void {
note.body = text;
store.save();
};- Create
src/smidr/widgets/UIXxx.hxwithclass UIXxx extends UIComponent(final classunless it is meant to be subclassed). - Pick the constructor flavour via
super(interactive, blocking):super(true, true)— interactive leaf (hover/press/click).super(false, true)— passive surface that still swallows pointer hits (panels, backdrops).super(false, false)— pure layout group, pointer-transparent.
- Override
render(); draw from theme colours (UIColor.rgb(UITheme.panel2)), size withUITheme.px(). - Expose state as
public var foo(default, set)whereset_foocallsinvalidate()— never mutate and repaint by hand. - Add the leading class docstring (required for the generated widget guide).
- Keyboard input → implement
smidr.input.IUIFocusable. A popup/menu → live onUIRoot.popupLayerand register a closer withUIRoot.pushOverlayCloser. - Optionally add a demo to the widget gallery (
examples/gallery/) so it shows up in the live examples. - Run the checks above and add a CHANGELOG entry.
- Each example is a self-contained folder under
examples/with its ownproject.xml. Add a new one toexamples/check.hxml(its-cpand main class) so CI typechecks it. - Because the checks compile several example mains together, keep example-only helper classes in a
named package (e.g.
package notepad;), not the empty package, so they cannot collide with another example's classes.
- Add an entry under
## [Unreleased]in CHANGELOG.md (Keep a Changelog format). Prefix API breaks with Breaking:. - Commit messages: imperative mood, a concise subject, ASCII only — no em dashes (use hyphens).
Everything below is gitignored; keep it out of PRs: build output (bin/, export/, .temp/,
dump/), .haxelib/, generated docs (doc/xml/, doc/site/), and the packaging zip.
SmiðrUI is MIT licensed. By contributing, you agree your contributions are licensed under the same terms.