Rendering Rules¶
This document defines the rendering discipline for qt-material3-widgets.
The goal is to keep the library visually consistent, performant, and maintainable while staying aligned with the architecture:
Qt owns behavior
theme owns design tokens
specs own resolved metrics and colors
effects own reusable rendering infrastructure
widgets compose geometry and paint the Material shell
These are hard rules unless there is a strong documented reason to break them.
1. Core Principle¶
A widget may custom-paint only what Qt does not already provide correctly for Material 3.
Good custom-paint responsibilities:
container shape
container fill
outline
state layer
ripple
focus ring
indicator visuals
surface/elevation treatment
Bad custom-paint responsibilities:
text editing engine
keyboard activation semantics from scratch
accessibility behavior from scratch
text cursor logic
input method handling
selection model logic already owned by Qt
Use Qt for behavior, paint for appearance.
2. Use Qt Primitives for Behavior¶
Preferred foundations:
QAbstractButtonfor buttons and selection controlsQLineEditfor single-line text inputQWidgetand composition for surfaces and containersQt focus, tab order, and event systems for interaction semantics
If Qt already owns the semantics correctly, keep Qt in charge.
3. Never Reimplement Text Editing¶
Text-field-like components must not implement a custom text engine.
For text fields:
use
QLineEditinternally for single-line inputlater use
QTextEditorQPlainTextEditfor multiline if needed
Custom painting is allowed for:
outline
filled container
floating label
supporting text
error text
prefix/suffix visuals
focus/error state visuals
Do not custom-build:
caret behavior
text selection behavior
clipboard semantics
IME/input method behavior
4. Paint Only from Resolved Specs¶
Widgets must not paint directly from raw theme roles.
Bad:
auto c = theme().colorScheme().color(ColorRole::Primary);
Good:
const auto &spec = resolvedSpec();
painter.setBrush(spec.containerColor);
Rules:
a widget resolves its spec
a widget caches its spec
paint consumes the spec only
5. No Magic Visual Constants in Paint Code¶
Widget paint code must not contain ad hoc visual constants.
Metrics must come from:
component specs
theme tokens
shared private helpers
Metrics belong in specs, not in paint functions.
6. Keep Paint Functions Dumb¶
A paintEvent() should mostly do the following:
ensure spec is resolved
ensure cached geometry is valid
configure painter
paint container
paint state layer
paint ripple
paint focus ring
paint content
A paintEvent() must not:
decide theme policy
generate design tokens
perform heavy state derivation
allocate unnecessary temporary objects repeatedly
own animation logic
Paint renders prepared state. It does not invent it.
7. Cache Geometry That Depends on Size¶
If geometry depends only on size, shape, and spec, cache it.
Examples:
rounded container path
outline path
indicator path
focus ring path
ripple clip path
Recompute only when:
widget size changes
shape-related spec values change
layout-affecting content changes
8. Cache Text Layout Inputs¶
Text layout work must be treated as non-trivial.
Cache or recompute only when these inputs change:
widget text
font
icon
available width
typography-related spec values
9. Effects Must Be Reusable Infrastructure¶
Effects must live in the effects layer, not inside final widget implementations.
Effects layer owns:
ripple animation
focus indicator painting
state layer painting
shadow rendering
shadow caching
transition control
Widgets may:
own an effect controller instance
configure effect geometry
trigger effects
pass colors and paths
Effects are infrastructure, not widget-local hacks.
10. Repaint the Smallest Reasonable Region¶
Widgets should not repaint more than needed. Early versions may tolerate coarse repaint for correctness, but the long-term target is tighter invalidation.
11. Shadow Rendering Must Be Cached¶
Shadows are expensive and must not be rebuilt casually.
Rules:
no heavy shadow generation every frame in steady-state rendering
cache by size, radius, blur, offset, and color
invalidate only on relevant changes
prefer shared cache infrastructure
12. Separate Visual Rect from Touch Target¶
Material 3 often uses a visual container smaller than the interactive target.
sizeHint()must respect the touch targetpaint must respect the visual container
the two must not be confused
13. Disabled State Must Be Spec-Driven¶
Disabled rendering must not be improvised per widget.
Allowed:
disabled colors computed in specs
a centralized disabled alpha policy applied in one place
Not allowed:
every widget choosing its own arbitrary opacity values
14. Focus Rendering Must Be Keyboard-Correct¶
Focus indication must reflect actual focus semantics, not hover.
keyboard focus must always be visually clear
hover is not a replacement for focus
focus ring must respect component shape
focus rendering should be consistent across widget families
15. Animation State Must Be Outside Paint¶
Paint reads animation progress. Paint does not run animation logic.
Use:
ripple controllers
transition controllers
explicit animated state objects
16. Compose Before Rebuilding Complex Behavior¶
When a control can be built as an existing Qt behavior primitive plus a Material shell around it, prefer that over rewriting complex behavior.
17. Public Headers Must Not Leak Rendering Internals¶
Public widget headers must not expose:
path caches
shadow cache keys
animation internals
effect engine private state
private layout helpers
19. Render Tests Are Part of the Rendering Contract¶
Every real widget must eventually have render regression coverage.
At minimum capture:
default state
hover state
focus state
pressed state
disabled state
dark mode state
checked/error state where relevant
20. Optimize After Correctness, But Optimize on Purpose¶
The correct order is:
correct behavior
clean architecture
stable rendering
measurement
optimization
Widget-Specific Rules¶
Selection Controls¶
use the shared selection-control family base
paint indicator visuals manually
keep state-layer region explicit
cache indicator geometry where practical
separate label layout from indicator layout cleanly
Surfaces¶
use surface semantics consistently
render container, shape, and elevation from spec
use cached shadow rendering
use scrim composition where relevant
keep transition logic outside paint
Text Fields¶
host
QLineEditpaint only the Material shell
keep focus/error/label/supporting text layout separate from editor behavior
cache label/supporting/error layout where practical
derive colors, sizes, and outlines from
TextFieldSpec
Forbidden Patterns¶
The following patterns are banned unless explicitly justified and reviewed:
raw theme lookups in every paint branch
repeated hardcoded pixel metrics across widgets
widget-local ripple implementations
widget-local shadow algorithms
rebuilding heavy paths every frame without need
recalculating text elision every paint unnecessarily
custom text entry logic replacing Qt editing widgets
paint functions performing business logic or token mapping
Project Mantra¶
Qt owns behavior.
Specs own metrics.
Effects own reusable rendering infrastructure.
Widgets compose geometry and paint the Material shell.