rewrite qtquick size+position guide

This commit is contained in:
outfoxxed 2025-05-13 20:56:31 -07:00
parent bc5e480cd1
commit e151d8c304
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
3 changed files with 195 additions and 82 deletions

View file

@ -32,7 +32,7 @@ const remarkParseAtTypes: RemarkPlugin<[]> = () => {
const node = rawNode as Md.Literal;
node.value = node.value.replace(
/@@((?<module>([A-Z]\w*\.)*)(?<type>([A-Z]\w*))\.?)?((?<member>[a-z]\w*)((?<function>\(\))|(?<signal>\(s\)))?)?(?=[$.,;:\s]|$)/g,
/@@((?<module>([A-Z]\w*\.)*)(?<type>([A-Z]\w*))\.?)?((?<member>[a-z]\w*)((?<function>\(\))|(?<signal>\(s\)))?)?(?=[$.,;:)\s]|$)/g,
(_full, ...args) => {
type Capture = {
module: string | undefined;

View file

@ -145,7 +145,8 @@ more correct code should you chose to use it.
> [!NOTE]
> Nix users should note that qmlls will not be able to pick up qml modules
> that are not in `QML2_IMPORT_PATH`.
> that are not in `QML2_IMPORT_PATH`. The easiest way to ensure this is by setting
> `qt.enable` to `true` and installing the quickshell package globally.
# Next steps

View file

@ -6,107 +6,219 @@ import MD_Title from "@components/MD_Title.tsx"
# {frontmatter.title}
QtQuick has multiple ways to position components. This page has instructions for where and how
to use them.
> [!TIP]
> Read the entire page, understanding this is critical to building a well designed shell.
## <MD_Title titleVar={2}> Anchors </MD_Title>
@@QtQuick.Item has two sets of size properties, actual size (@@QtQuick.Item.width and @@QtQuick.Item.height)
and implicit / desired (@@QtQuick.Item.implicitWidth and @@QtQuick.Item.implicitHeight).
Anchors can be used to position components relative to another neighboring component.
It is faster than [manual positioning](#manual-positioning) and covers a lot of simple
use cases.
Container items, such as layouts and wrappers, use the implicit size of their children to determine
their own implicit size, and their actual size to detetermine the actual size of their children.
If managed by a container, an Item should not set its own size, and should instead allow
the container to determine it based on its implicit size.
The [Qt Documentation: Positioning with Anchors](https://doc.qt.io/qt-6/qtquick-positioning-anchors.html)
page has comprehensive documentation of anchors.
Put simply, implicit size should flow from children to parents, while actual size should flow from
parent to children.
## <MD_Title titleVar={2}> Layouts </MD_Title>
In addition to size, Items also have position properties (@@QtQuick.Item.x and @@QtQuick.Item.y).
Similarly to actual size, (actual) position should not be set directly if your item is managed
by a container, though there is no such thing as implicit position.
Layouts are useful when you have many components that need to be positioned relative to
eachother such as a list.
> [!WARNING]
> Many QtQuick Items have *zero size* by default (both implicit and actual).
>
> An invisible zero sized item (usually a custom container without implicit size set)
> is a common bug and often manifests as an item being laid out as if it took no space.
>
> Quickshell will attempt to detect zero sized items when a window is initially made visible
> and log a warning, but it cannot detect all cases. Please be aware these exist.
The [Qt Documentation: Layouts Overview](https://doc.qt.io/qt-6/qtquicklayouts-overview.html)
page has good documentation of the basic layout types and how to use them.
> [!note/Note:]
> Layouts by default have a nonzero spacing.
## <MD_Title titleVar={2}> Manual Positioning </MD_Title>
If layouts and anchors can't easily fulfill your usecase, you can also manually position and size
components by setting their @@QtQuick.Item.x, @@QtQuick.Item.y, @@QtQuick.Item.width and @@QtQuick.Item.height
properties, which are relative to the parent component.
This example puts a 100x100px blue rectangle at x=20,y=40 in the parent item. Ensure the size
of the parent is large enough for its content or positioning based on them will break.
## Container Items
Below is an example container which adds a margin to a rectangle, and interacts properly
with other container types.
```qml
@@QtQuick.Item {
// make sure the component is large enough to fit its children
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
property real margin: 5
// Set the implicit size of the containing item to the size of
// the contained item, plus the margin on each side.
implicitWidth: child.implicitWidth + margin * 2
implicitHeight: child.implicitHeight + margin * 2
@@QtQuick.Rectangle {
color: "blue"
x: 20
y: 40
width: 100
height: 100
id: child
// Set the size of the child item relative to the actual size
// of the parent item. If the parent item is constrained
// or stretched the child's position and size will be similarly
// constrained.
x: parent.margin
y: parent.margin
width: parent.width - parent.margin * 2
height: parent.height - parent.margin * 2
// The child's implicit / desired size, which will be respected
// by the container item as long as it is not constrained
// or stretched.
implicitWidth: 50
implicitHeight: 50
}
}
```
## <MD_Title titleVar={2}> Notes </MD_Title>
### <MD_Title titleVar={3}> Component Size </MD_Title>
The @@QtQuick.Item.implicitHeight and @@QtQuick.Item.implicitWidth properties control the
_base size_ of a component, before layouts are applied. These properties are _not_ the same as
@@QtQuick.Item.height and @@QtQuick.Item.width which are the final size of the component.
You should nearly always use the implicit size properties when creating a component,
however using the normal width and height properties is fine if you know an
item will never go in a layout.
This example component puts a colored rectangle behind some text, and will act the same
way in a layout as the text by itself.
```qml {filename="TextWithBkgColor.qml"}
@@QtQuick.Rectangle {
implicitWidth: text.implicitWidth
implicitHeight: text.implicitHeight
@@QtQuick.Text {
id: text
text: "hello!"
}
}
```
If you want to size your component based on multiple others or use any other math you can.
```qml {filename="PaddedTexts.qml"}
If we were to write this as a reusable component, we could use @@QtQml.Binding
to control the child item's actual size and position.
```qml
@@QtQuick.Item {
// width of both texts plus 5
implicitWidth: text1.implicitWidth + text2.implicitWidth + 5
// max height of both texts plus 5
implicitHeight: Math.min(text1.implicitHeight, text2.implicitHeight) + 5
id: wrapper
property real margin: 5
required default property Item child
@@QtQuick.Text {
id: text1
text: "text1"
}
// Set the item's visual children list to just the passed item.
children: [child]
@@QtQuick.Text {
id: text2
anchors.left: text1.left
text: "text2"
implicitWidth: child.implicitWidth + margin * 2
implicitHeight: child.implicitHeight + margin * 2
// Bind the child's position and size.
// Note that this syntax is exclusive to the Binding type.
@@QtQuick.Binding { wrapper.child.x: wrapper.margin }
@@QtQuick.Binding { wrapper.child.y: wrapper.margin }
@@QtQuick.Binding { wrapper.child.width: wrapper.width - wrapper.margin * 2 }
@@QtQuick.Binding { wrapper.child.height: wrapper.height - wrapper.margin * 2 }
}
```
Note: @@Quickshell.Widgets.WrapperItem is a builtin component that adds margins similarly to this.
### Reducing boilerplate with Anchors
We can reduce the amount of boilerplate we have to write using
[QtQuick Anchors](https://doc.qt.io/qt-6/qtquick-positioning-anchors.html).
Anchors exist as a shorthand way to achieve many common position and size bindings.
See the linked qt documentation for more details on how to use them.
The following example is equivalent to the one above, but uses anchors instead of setting
position and size directly. A similar change can be made to the `Binding` example.
```qml
@@QtQuick.Item {
property real margin: 5
implicitWidth: child.implicitWidth + margin * 2
implicitHeight: child.implicitHeight + margin * 2
@@QtQuick.Rectangle {
id: child
// "Fill" the space occupied by the parent, setting width
anchors.fill: parent
// Add a margin to all anchored sides.
anchors.margins: parent.margin
implicitWidth: 50
implicitHeight: 50
}
}
```
### <MD_Title titleVar={3}> Coordinate space </MD_Title>
### childrenRect and binding loops
The most common mistake made when creating container items is trying to use @@QtQuick.Item.childrenRect
to determine the size of a child item, such as in the example below:
You should always position or size components relative to the closest possible
parent. Often this is just the @@QtQuick.Item.parent property.
```qml
@@QtQuick.Item {
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
@@QtQuick.Rectangle {
anchors.fill: parent
Refrain from using things like the size of your screen to size a component,
as this will break as soon as anything up the component hierarchy changes, such
as adding padding to a bar.
implicitWidth: 50
implicitHeight: 50
}
}
```
While the snippet above might look like it should work, it is actually hiding a nasty bug.
As stated at the top of the page, an item's implicit size should be used to determine
its parent's implicit size, and the parent's actual size should be used to determine
the child's actual size. **`childrenRect` breaks this pattern.**
`childrenRect` encompasses the geometry of all child items, meaning their *actual* geometry,
not their *implicit* geometry. This results in the container item's size having an indirect
dependency on itself, in what is known as a *binding loop*.
If we were to try to figure out what implicitWidth is by hand, it would look something like this:
*container.implicitWidth = container.childrenRect.width = child.width = container.width (via anchor)
= container.implicitWidth = ... (repeats forever)*
which isn't a valid definition.
### MarginWrapper components
To solve the boilerplate problem that often leads users to `childrenRect`, Quickshell comes with
@@Quickshell.Widgets.MarginWrapperManager and a set of components based on it.
@@Quickshell.Widgets.MarginWrapperManager automatically handles the size and position relationship
between a container item and a single child item, skipping most of the boilerplate in the above
examples. See its linked documentation for more information on how to use it.
Rewriting the examples from the top of the page:
```qml
@@QtQuick.Item {
@@Quickshell.Widgets.MarginWrapperManager {
margin: 5
// By default, MarginWrapperManager centers the child
// instead of resizing it when encountering constraints.
resizeChild: true
}
// Automatically detected by MarginWrapperManager as the
// primary child of the container and sized accordingly.
@@QtQuick.Rectangle {
implicitWidth: 50
implicitHeight: 50
}
}
```
Or as a reusable component:
```qml
@@QtQuick.Item {
// A bidirectional binding to manager.margin,
// where the default value is set.
property alias margin: manager.margin
// MarginWrapperManager tries to automatically detect
// the primary child of the container, but exposing the
// child property allows us to both access the child
// externally and override it if automatic detection fails.
property alias child: manager.margin
// MarginWrapperManager automatically manages the implicit size
// of the container and actual size of the child.
@@Quickshell.Widgets.MarginWrapperManager {
id: manager
resizeChild: true
margin: 5 // the default value of margin
}
}
```
Quickshell bundles three of the most commonly used wrappers, which are implemented similarly
to the example above:
- @@Quickshell.Widgets.WrapperItem
- @@Quickshell.Widgets.WrapperRectangle
- @@Quickshell.Widgets.WrapperMouseArea
## Layouts
QtQuick comes with a set of layout types in the
[QtQuick.Layouts](https://doc.qt.io/qt-6/qtquicklayouts-overview.html) module.
Layouts, such as the Row, Column and Grid layout, are extremely useful for positioning
items adjacent to eachother. See the linked qt documentation for more details.
> [!NOTE]
> Layouts have a default spacing of 5 pixels between items, not zero.