experimental web port of the Union style engine + adapters
styling theming css solidjs react union kde
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 1 folder 5 files
README.md

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#

  1. 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's stackCenter/stackFill flags are OR'd in (any item with vertical stack-center or stack-fill switches the entire bucket to vertical stacking mode).

  2. Phase 2Layout::layout() runs in sequence:

    • itemContainer is laid out at full parent size
    • After itemContainer's start/end buckets consume their width, the remaining horizontal strip is computed
    • backgroundContainer occupies that remaining strip shrunk by inset (margins)
    • contentContainer occupies that remaining strip shrunk by padding
  3. Phase 3 — Each container's items get their final position and size. Items in the fill bucket get equal-width slices. Vertical alignment (start/center/end/fill) positions within the bucket height. stack-center/stack-fill stack items vertically within the bucket.


Upstream Semantic Model vs. This Port#

What is identical#

  • Alignment and AlignmentContainer enums
  • LayoutItem, LayoutBucket, LayoutContainer, Layout data structures and all their methods
  • spacedSize() 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 propertiesPositioner and PositionedItem are not ported. Alignment data is passed directly as PositionerItemInput objects.
  • No polish loop — layout runs synchronously on demand via computePositionerLayout().
  • No coordinate mappingLayoutItem::setItemPosition() in upstream maps coordinates from layout space to the item's actual parent via mapToItem(). This port assumes all items share the same parent coordinate space.
  • No PositionerSource — alignment is always read from the layout property group (not from icon or text groups).
  • No positionChildren — all items are positioned directly.
  • No minimumWidth/minimumHeight from PositionedItem — 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-fill get stacked y-positions and centered/full-width x-positions
  • Items with start/center/end/fill get 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.