149 lines
4.7 KiB
QML
149 lines
4.7 KiB
QML
// 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
|
|
}
|
|
}
|