This document describes the layout/rendering contract used by MewUI, and the rules to keep layout stable across DPIs while avoiding 1px clipping artifacts.
- DIP: Device-independent pixels (logical units). In MewUI, most layout coordinates/sizes are expressed in DIP.
- Px: Device pixels (physical pixels).
px = dip * dpiScale. - dpiScale:
Dpi / 96.0. - Constraint: The
Size availableSizepassed intoMeasure(...). - DesiredSize: The element’s preferred size after
Measure. - Bounds: The element’s arranged rectangle after
Arrange.
MewUI follows an Immediate Mode UI approach (draw each frame), but uses a retained layout tree for hit-testing and for reusing layout results between frames.
The pipeline is:
- Measure: top-down, determines
DesiredSize. - Arrange: top-down, assigns
Bounds. - Render: draws using
Boundsand current visual state.
InvalidateMeasure()marks the element (and ancestors) as needing a new measure pass. This also implies arrange is invalid.InvalidateArrange()marks the element (and ancestors) as needing a new arrange pass, but does not necessarily require measure.InvalidateVisual()requests repaint but should not change layout.
Rule of thumb:
- Scrolling should be Arrange + Render only (offset changes typically require re-arranging children, not re-measuring).
- Content/size-affecting property changes require Measure.
Measure calculates how large an element wants to be under a given constraint.
- Input:
availableSizein DIP - Output:
DesiredSizein DIP
- Measure must be pure with respect to layout state:
- It must not mutate layout-affecting properties without checking for changes first (to avoid infinite invalidation loops).
- It should not depend on previously arranged bounds.
- A re-measure can be skipped if:
- the element is not measure-dirty, and
- the constraint is unchanged.
Measure returns DIP sizes. The layout system may apply layout rounding to keep sizes stable across DPIs and avoid fractional edges that cause 1px seams.
The important principle is:
- Measure should not try to “manually pixel-snap” using parent coordinates.
- Prefer applying a consistent rounding policy at the layout system boundary (e.g., when assigning
DesiredSize).
Arrange assigns the final position/size (Bounds) for each element.
- Input:
finalRectin DIP - Output:
Boundsin DIP
- Arrange may be skipped if:
- the element is not arrange-dirty, and
- the arranged bounds are identical.
- Arrange is responsible for placing children (e.g., content presenter, panels).
The main source of visual artifacts (hairlines, 1px clipping) is inconsistent rounding between:
- parent’s computed child rect,
- the child’s own rounding,
- render-time clipping.
To avoid this:
- Use a single dpiScale for the entire pass (Window DPI).
- Snap rectangle edges, not just width/height:
- snapping both left/right edges can shrink the size by 1px when rounding inward,
- so choose inward/outward snapping intentionally depending on semantics.
Recommended semantics:
- Bounds used for painting borders/background: snap edges so strokes land on device pixels.
- Viewport / clip rectangles: snap outward so the clip does not become smaller than the intended viewport due to rounding.
Render draws the element using Bounds and current visual state.
- Render must not perform layout. No Measure/Arrange calls inside Render.
- Render should draw using already-snapped geometry whenever possible.
Text, strokes, and antialiasing can extend half a pixel outside logical bounds. If an ancestor applies a clip exactly at the child bounds, that overhang can be clipped and look like “right/bottom 1px missing”.
Recommended approach:
- Content rendering inside a viewport should:
- compute the viewport rect in DIP,
- snap it to pixels (prefer outward snapping),
- apply clip,
- optionally expand the clip by +1 device pixel on the right/bottom when drawing 1px strokes/glyphs at the edge (if you know content may overhang).
This is the intent behind helpers like “expand clip by device pixels”.
- Window provides the effective DPI (
Dpi,DpiScale). - Children should use the same DPI for rounding decisions in Measure/Arrange/Render.
Caches that depend on DPI (text measure cache, geometry cache, etc.) must be invalidated when:
- the element’s effective DPI changes,
- font-related properties change,
- wrap/constraint changes (for wrapped text).
- Store offsets in DIP conceptually, but you may keep internal values in pixels for stable snapping.
- Changing offsets should not require re-measure unless extent/viewport changes.
- Arrange children using
viewport - offset. - Render the content under a viewport clip.
- Update scrollbar ranges/values to match the current offset and viewport.
- Setting layout-affecting properties during Measure/Arrange without comparing old/new values first.
- Triggering
InvalidateMeasure()from Render. - Recomputing DPI scale by walking the tree in hot paths without caching.
- Measuring on every scroll tick when only offsets changed.