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:

  • QAbstractButton for buttons and selection controls

  • QLineEdit for single-line text input

  • QWidget and composition for surfaces and containers

  • Qt 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 QLineEdit internally for single-line input

  • later use QTextEdit or QPlainTextEdit for 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:

  1. ensure spec is resolved

  2. ensure cached geometry is valid

  3. configure painter

  4. paint container

  5. paint state layer

  6. paint ripple

  7. paint focus ring

  8. 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 target

  • paint 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

18. Share Paint Helpers Across Families

If multiple widgets paint the same concept, they should not each re-solve it independently.

Examples:

  • button family should share content layout helpers

  • selection controls should share indicator geometry helpers

  • surfaces should share container/shadow paint helpers

  • inputs should share label/supporting text layout helpers

Shared visual concepts should be rendered through shared 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:

  1. correct behavior

  2. clean architecture

  3. stable rendering

  4. measurement

  5. optimization

Widget-Specific Rules

Buttons

  • derive behavior from QAbstractButton

  • resolve and cache ButtonSpec

  • cache container path on resize

  • cache content layout until text/font/icon/size changes

  • clip ripple to the container shape

  • render focus ring consistently with the container geometry

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 QLineEdit

  • paint 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.