Positioner Sandbox#
A port of KDE Union's PositionerLayout — the sub-control
positioning engine used to lay out indicators, icons, labels, and other
child items within a styled widget (Button, Checkbox, SpinBox, etc.).
This is a sandbox / study tool, not a library. Its purpose is to provide an exact, documentable, inspectable implementation of the Positioner so you can tweak alignment properties and immediately see how the layout changes.
Quick Start#
from root project directory
npm run dev:pos
Upstream Source Map#
Everything in src/engine/ is a direct port of a corresponding upstream
file from KDE Union's QtQuick output plugin. File-level mappings:
| This file | Upstream file |
|---|---|
src/engine/types.ts |
src/PropertiesTypes.h |
src/engine/layout-item.ts |
src/output/qtquick/plugin/positioner/LayoutItem.h |
src/engine/layout-bucket.ts |
src/output/qtquick/plugin/positioner/LayoutBucket.h |
src/engine/layout-container.ts |
src/output/qtquick/plugin/positioner/LayoutContainer.h |
src/engine/layout.ts |
src/output/qtquick/plugin/positioner/Layout.h |
src/engine/layout-helpers.ts |
src/output/qtquick/plugin/positioner/LayoutHelpers.h |
src/engine/positioner-layout.ts |
src/output/qtquick/plugin/positioner/PositionerLayout.h + PositionerLayout.cpp |
The upstream repository is at: https://invent.kde.org/plasma/union
Concrete commit for this port: 9620b16f892baafef390bc387ae9562b2b235863
, the current master branch as of
mid-2026. Every function and data structure in the port carries comment
headers referencing the exact upstream file.h:line range it was ported
from.
Architecture Overview#
Positioner (QML attached property) ← not ported (Qt binding)
└─ PositionerLayout (QQuickItem) ← ported: positioner-layout.ts
└─ Layout ← ported: layout.ts
├─ itemContainer (LayoutContainer)
│ ├─ start bucket (LayoutBucket)
│ ├─ center bucket
│ ├─ end bucket
│ └─ fill bucket
├─ backgroundContainer ← same bucket structure
└─ contentContainer ← same bucket structure
Layout flow#
-
Phase 1 — Each child item is read: its alignment properties (
container,horizontal,vertical,order) and its margins and size hints. The item is appended to the correct container × bucket tuple, and the bucket'sstackCenter/stackFillflags are OR'd in (any item with verticalstack-centerorstack-fillswitches the entire bucket to vertical stacking mode). -
Phase 2 —
Layout::layout()runs in sequence:itemContaineris laid out at full parent size- After itemContainer's start/end buckets consume their width, the remaining horizontal strip is computed
backgroundContaineroccupies that remaining strip shrunk byinset(margins)contentContaineroccupies that remaining strip shrunk bypadding
-
Phase 3 — Each container's items get their final position and size. Items in the
fillbucket get equal-width slices. Vertical alignment (start/center/end/fill) positions within the bucket height.stack-center/stack-fillstack items vertically within the bucket.
Upstream Semantic Model vs. This Port#
What is identical#
AlignmentandAlignmentContainerenumsLayoutItem,LayoutBucket,LayoutContainer,Layoutdata structures and all their methodsspacedSize()helper- The three-container layout algorithm (item → background → content)
- Start/end bucket width consumption reducing center/fill space
- Fill bucket equal-width division
- Vertical stacking mode (stack-center / stack-fill)
- RTL support via
LayoutDirection order-based stable sorting within buckets- Per-item margin application in fill/stack modes
What is simplified#
- No QQuickItem pointers — items are identified by a string
id - No QML attached properties —
PositionerandPositionedItemare not ported. Alignment data is passed directly asPositionerItemInputobjects. - No polish loop — layout runs synchronously on demand via
computePositionerLayout(). - No coordinate mapping —
LayoutItem::setItemPosition()in upstream maps coordinates from layout space to the item's actual parent viamapToItem(). This port assumes all items share the same parent coordinate space. - No
PositionerSource— alignment is always read from thelayoutproperty group (not fromiconortextgroups). - No
positionChildren— all items are positioned directly. - No
minimumWidth/minimumHeightfromPositionedItem— these are optionally provided on the input.
Divergence: background container position tracking#
In upstream Layout.h, the backgroundContainer position is stored on
the struct field but the Layout struct does not have a dedicated
backgroundPosition — it just uses backgroundContainer.position.
This port exposes it separately for the visualization to render the
background container bounds correctly. The Layout struct has an
added backgroundPosition field for this purpose.
Alignment Values — Reference#
layout-alignment-horizontal#
| Value | Bucket | Behavior |
|---|---|---|
start |
start | Item sits in the left bucket |
center |
center | Item sits in the center bucket (between start/end) |
end |
end | Item sits in the right bucket |
fill |
fill | Item gets an equal-width slice of the remaining space |
stack-center |
start† | Not supported for horizontal — falls through to start with a warning |
stack-fill |
start† | Not supported for horizontal — falls through to start with a warning |
† Upstream: PositionerLayout.cpp:287-293
layout-alignment-vertical#
| Value | Behavior |
|---|---|
start |
Top of bucket ± margin |
center |
Vertically centered in bucket |
end |
Bottom of bucket ± margin |
fill |
Stretches to fill bucket height |
stack-center |
Triggers vertical stacking mode in the bucket. Items stack top-to-bottom, each horizontally centered. If content is shorter than bucket, the whole stack is vertically centered. |
stack-fill |
Triggers vertical stacking mode. Items stack top-to-bottom, each gets equal height (bucketHeight / itemCount). |
layout-alignment-container#
| Value | Container | Reference frame |
|---|---|---|
item |
itemContainer |
Full parent box (default) |
background |
backgroundContainer |
Parent minus start/end minus inset |
content |
contentContainer |
Parent minus start/end minus padding |
Key Algorithm Details#
Bucket sorting#
LayoutBucket::sort() (LayoutBucket.h:28-31): Items are sorted by
order ascending with stable sort. This determines left-to-right
order in horizontal flow, or top-to-bottom order in stack mode.
Start/end width consumption#
LayoutContainer::layout() (LayoutContainer.h:74-78):
startSpace = start.size.width > 0 ? start.size.width + spacing : 0
endSpace = end.size.width > 0 ? end.size.width + spacing : 0
remaining = containerSize - (startSpace + endSpace, 0)
This is the competing-widths model. Wider start/end items leave less room for center/fill/background/content.
Fill bucket equal-width division#
LayoutContainer.h:90-106:
fillWidth = bucket.size.width / itemCount
for each item:
item.position.x = fillStart + item.margins.left
item.size.width = fillWidth - margins.left - margins.right
Stack mode switching#
PositionerLayout.cpp:283-284, 294-312: Any item with vertical
stack-center or stack-fill sets a per-bucket boolean flag.
Once set, LayoutBucket::layout() uses the vertical-stacking code
path for ALL items in that bucket (LayoutBucket.h:43-51), and
LayoutContainer::layout() uses stackedY for final positioning
(LayoutContainer.h:88-138).
Known upstream quirk: mixed stack/non-stack items in the same bucket#
When a bucket's stackCenter or stackFill flag is true
(because at least one item uses that vertical alignment),
LayoutBucket::layout() (LayoutBucket.h:43-51) sets
position.setX(0.0) for all items unconditionally. The
horizontal-flow x accumulation is skipped entirely.
In LayoutContainer::layout() (LayoutContainer.h:94-139), the
per-item positioning loop checks each item's individual
verticalAlignment. So:
- Items with
stack-center/stack-fillget stacked y-positions and centered/full-width x-positions - Items with
start/center/end/fillget their vertical placement applied but their x stays at the bucket's left edge (no horizontal centering or end-positioning)
This means mixing start/center/end with stack-center/
stack-fill in the same bucket produces items at the left edge
for non-stack items — an unintuitive result. In practice, themes
assign all items in a bucket the same vertical alignment, so this
edge case does not arise in real usage.
If you observe a non-stack item appearing to "stay in its original
horizontal position" when another item in the same bucket uses
stack-center/stack-fill, that is correct upstream behavior
faithfully reproduced by this port.
Implicit size from buckets#
LayoutBucket.h:73-77: After layout, the bucket's implicit size
is Math.ceil(maxWidth) × Math.ceil(maxHeight). For horizontal
flow, maxWidth = accumulated item widths. For stack mode,
maxWidth = widest item, maxHeight = sum of all item heights.