// Page-level tabs, for a page that is genuinely several subjects. // // SettingsTabs { // tabs: [{ value: "theme", label: "Theme" }, …] // current: root.tab // onSelected: value => root.tab = value // } // // Only for pages long enough that scrolling hides the control someone came for. // Appearance was six cards deep, so light and dark -- the thing reached most -- // sat below a wallpaper grid and an entire lock screen. Tabs are not a way to // make a short page look organized; they are for when the page is long enough // that the order stops being a suggestion and starts being a burial. // // The strip scrolls horizontally when it overflows: the Settings window tiles // down to 900px, and the System category carries nine tabs. The active tab is // always brought into view, and edge fades say there is more to the side. import QtQuick import qs.config Item { id: root // [{ value, label }] property var tabs: [] property string current: "" signal selected(string value) width: parent ? parent.width : 620 implicitHeight: 40 Flickable { id: flick anchors.fill: parent contentWidth: strip.implicitWidth contentHeight: height flickableDirection: Flickable.HorizontalFlick boundsBehavior: Flickable.StopAtBounds interactive: contentWidth > width clip: true function reveal(item: Item): void { if (contentWidth <= width) return; const target = item.x - (flick.width - item.width) / 2; contentX = Math.max(0, Math.min(contentWidth - width, target)); } Row { id: strip anchors.bottom: parent.bottom spacing: 2 Repeater { model: root.tabs delegate: Item { id: tab required property var modelData readonly property bool active: String(tab.modelData.value) === root.current implicitWidth: caption.implicitWidth + 30 implicitHeight: 38 onActiveChanged: if (active) flick.reveal(tab) Component.onCompleted: if (active) flick.reveal(tab) Text { id: caption anchors.centerIn: parent text: String(tab.modelData.label ?? "") color: tab.active ? Theme.fg : (hover.hovered ? Theme.fgDim : Theme.fgMuted) font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: tab.active ? Font.DemiBold : Font.Medium } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 2 radius: 1 visible: tab.active color: Theme.accent } HoverHandler { id: hover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: root.selected(String(tab.modelData.value)) } } } } WheelHandler { target: null onWheel: event => { const delta = event.angleDelta.x !== 0 ? event.angleDelta.x : event.angleDelta.y; flick.contentX = Math.max(0, Math.min(flick.contentWidth - flick.width, flick.contentX - delta)); } } } Rectangle { anchors.left: parent.left anchors.bottom: parent.bottom width: 18 height: parent.height visible: flick.contentX > 1 gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0; color: Theme.bg } GradientStop { position: 1; color: Theme.alpha(Theme.bg, 0) } } } Rectangle { anchors.right: parent.right anchors.bottom: parent.bottom width: 18 height: parent.height visible: flick.contentWidth > flick.width && flick.contentX < flick.contentWidth - flick.width - 1 gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0; color: Theme.alpha(Theme.bg, 0) } GradientStop { position: 1; color: Theme.bg } } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 1 color: Theme.alpha(Theme.fg, 0.08) z: -1 } }