Normalize bar icon size and alignment
This commit is contained in:
17 files changed
+540
-116
No files matched your search
+66
-28
@@ -8,52 +8,90 @@ WidgetButton {
|
||||
property Component iconComponent: null
|
||||
property real slotSize: Style.bar.iconSlot
|
||||
property real opticalSize: Style.bar.iconCanvas
|
||||
property real iconSize: Math.min(Style.bar.iconSize, opticalSize)
|
||||
readonly property real visualIconSize: Style.bar.iconSize
|
||||
readonly property real canvasSize: Math.max(visualIconSize, opticalSize)
|
||||
property string labelText: ""
|
||||
property bool debugOpticalBounds: Quickshell.env("OMARCHY_DEBUG_BAR_ICONS") === "1"
|
||||
// Measured against the slot, which the open-panel underline centers on.
|
||||
readonly property real opticalCenterErrorX: glyph.visible ? opticalCanvas.x + glyph.paintedCenterX - root.width / 2 : 0
|
||||
readonly property real glyphPaintedWidth: glyph.visible ? glyph.tightWidth : 0
|
||||
// A label shifts the icon within the centered group.
|
||||
readonly property real opticalCenterErrorX:
|
||||
content.x + opticalCanvas.x + (glyph.visible ? glyph.paintedCenterX : customIcon.width / 2 + customIcon.centerErrorX) - root.width / 2
|
||||
- (label.visible ? (label.implicitWidth + Style.space(4)) / 2 : 0)
|
||||
readonly property real opticalCenterErrorY:
|
||||
content.y + (glyph.visible ? glyph.paintedCenterY : customIcon.height / 2 + customIcon.centerErrorY) - root.height / 2
|
||||
readonly property real glyphPaintedWidth: glyph.visible ? glyph.paintedWidth : 0
|
||||
readonly property real glyphPaintedHeight: glyph.visible ? glyph.paintedHeight : 0
|
||||
readonly property real glyphBaselineY: glyph.visible ? glyph.baselineY : 0
|
||||
readonly property int glyphFontSize: glyph.visible ? glyph.renderedFontSize : 0
|
||||
readonly property real componentPaintedWidth: customIcon.visible ? customIcon.paintedWidth : 0
|
||||
readonly property real componentPaintedHeight: customIcon.visible ? customIcon.paintedHeight : 0
|
||||
|
||||
labelVisible: false
|
||||
hasVisualContent: text !== "" || iconComponent !== null
|
||||
hasVisualContent: text !== "" || iconComponent !== null || labelText !== ""
|
||||
fontSize: Style.bar.iconFont
|
||||
fixedWidth: vertical ? -1 : slotSize
|
||||
fixedHeight: vertical ? slotSize : -1
|
||||
|
||||
Item {
|
||||
id: opticalCanvas
|
||||
// Placed exactly, not anchored: centerIn snaps an even canvas in an odd slot
|
||||
// to a whole pixel, pulling every glyph off the open-panel underline.
|
||||
id: content
|
||||
// Exact coordinates preserve the center of odd-width slots.
|
||||
x: (root.width - width) / 2
|
||||
y: (root.height - height) / 2
|
||||
width: root.opticalSize
|
||||
height: root.opticalSize
|
||||
width: root.canvasSize + (label.visible ? label.implicitWidth + Style.space(4) : 0)
|
||||
height: root.canvasSize
|
||||
|
||||
OpticalGlyph {
|
||||
id: glyph
|
||||
anchors.fill: parent
|
||||
visible: root.iconComponent === null
|
||||
text: root.text
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: root.fontSize
|
||||
Text {
|
||||
id: label
|
||||
visible: root.labelText !== ""
|
||||
textFormat: Text.PlainText
|
||||
text: root.labelText
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.fontSize
|
||||
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
|
||||
rotation: root.textRotation
|
||||
debugBounds: root.debugOpticalBounds
|
||||
y: (parent.height - height) / 2
|
||||
+ (labelMetrics.descent - labelMetrics.ascent + labelMetrics.tightBoundingRect("0").height) / 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
visible: root.iconComponent !== null
|
||||
sourceComponent: root.iconComponent
|
||||
FontMetrics {
|
||||
id: labelMetrics
|
||||
font: label.font
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.debugOpticalBounds && root.iconComponent !== null
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: "#4488ff"
|
||||
Item {
|
||||
id: opticalCanvas
|
||||
x: label.visible ? label.implicitWidth + Style.space(4) : 0
|
||||
width: root.canvasSize
|
||||
height: root.canvasSize
|
||||
|
||||
OpticalGlyph {
|
||||
id: glyph
|
||||
anchors.fill: parent
|
||||
visible: root.iconComponent === null
|
||||
text: root.text
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: root.fontSize
|
||||
paintedSize: root.visualIconSize
|
||||
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
|
||||
rotation: root.textRotation
|
||||
debugBounds: root.debugOpticalBounds
|
||||
}
|
||||
|
||||
BarIconContent {
|
||||
id: customIcon
|
||||
anchors.fill: parent
|
||||
visible: root.iconComponent !== null
|
||||
sourceComponent: root.iconComponent
|
||||
sourceSize: root.opticalSize
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.debugOpticalBounds && root.iconComponent !== null
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: "#4488ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Component sourceComponent: null
|
||||
property real sourceSize: Style.bar.iconCanvas
|
||||
readonly property rect sourceBounds: bounds.mappedBounds
|
||||
readonly property real contentScale: sourceBounds.width > 0 && sourceBounds.height > 0
|
||||
? Style.bar.iconSize / Math.max(sourceBounds.width, sourceBounds.height) : 1
|
||||
readonly property real paintedWidth: sourceBounds.width * contentScale
|
||||
readonly property real paintedHeight: sourceBounds.height * contentScale
|
||||
readonly property real centerErrorX: content.x + (sourceBounds.x + sourceBounds.width / 2) * contentScale - width / 2
|
||||
readonly property real centerErrorY: content.y + (sourceBounds.y + sourceBounds.height / 2) * contentScale - height / 2
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
sourceComponent: root.sourceComponent
|
||||
onLoaded: {
|
||||
// Keep declared dimensions. Empty wrapper items receive a source canvas.
|
||||
if (!(item instanceof Image)) {
|
||||
if (item.width <= 0) item.width = Qt.binding(function() { return root.sourceSize })
|
||||
if (item.height <= 0) item.height = Qt.binding(function() { return root.sourceSize })
|
||||
}
|
||||
}
|
||||
transformOrigin: Item.TopLeft
|
||||
scale: root.contentScale
|
||||
x: root.width / 2 - (root.sourceBounds.x + root.sourceBounds.width / 2) * scale
|
||||
y: root.height / 2 - (root.sourceBounds.y + root.sourceBounds.height / 2) * scale
|
||||
}
|
||||
|
||||
IconContentBounds {
|
||||
id: bounds
|
||||
sourceItem: content.item
|
||||
boundsParent: content
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ BarIconButton {
|
||||
useActiveColor: false
|
||||
maintainIndicatorReveal: indicatorBlock === "inactive"
|
||||
revealHost: indicatorHost
|
||||
fontSize: Style.font.caption
|
||||
horizontalMargin: 5
|
||||
verticalPadding: 5
|
||||
fixedWidth: vertical ? -1 : Style.bar.statusSlot
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Item sourceItem: null
|
||||
property Item boundsParent: null
|
||||
property rect mappedBounds: Qt.rect(0, 0, 0, 0)
|
||||
visible: false
|
||||
|
||||
readonly property bool textItem: sourceItem instanceof Text
|
||||
readonly property bool imageItem: sourceItem instanceof Image
|
||||
readonly property bool boxItem: sourceItem instanceof Rectangle
|
||||
|| sourceItem instanceof Shape || sourceItem instanceof Canvas || sourceItem instanceof ShaderEffect
|
||||
|
||||
function unionBounds(a, b) {
|
||||
if (b.width <= 0 || b.height <= 0) return a
|
||||
if (a.width <= 0 || a.height <= 0) return b
|
||||
var x = Math.min(a.x, b.x)
|
||||
var y = Math.min(a.y, b.y)
|
||||
return Qt.rect(x, y, Math.max(a.x + a.width, b.x + b.width) - x,
|
||||
Math.max(a.y + a.height, b.y + b.height) - y)
|
||||
}
|
||||
|
||||
readonly property rect paintedBounds: {
|
||||
var item = sourceItem
|
||||
var bounds = Qt.rect(0, 0, 0, 0)
|
||||
if (!item || !item.visible || item.opacity <= 0) return bounds
|
||||
if (textItem) {
|
||||
var ink = textMetrics.tightBoundingRect
|
||||
bounds = Qt.rect(ink.x, item.baselineOffset + ink.y, ink.width, ink.height)
|
||||
} else if (imageItem) {
|
||||
var w = item.paintedWidth
|
||||
var h = item.paintedHeight
|
||||
var x = item.horizontalAlignment === Image.AlignLeft ? 0
|
||||
: (item.horizontalAlignment === Image.AlignRight ? item.width - w : (item.width - w) / 2)
|
||||
var y = item.verticalAlignment === Image.AlignTop ? 0
|
||||
: (item.verticalAlignment === Image.AlignBottom ? item.height - h : (item.height - h) / 2)
|
||||
if (item.fillMode === Image.PreserveAspectFit) {
|
||||
bounds = Qt.rect(x, y, w, h)
|
||||
} else if (item.fillMode === Image.Pad) {
|
||||
var left = Math.max(0, x)
|
||||
var top = Math.max(0, y)
|
||||
bounds = Qt.rect(left, top, Math.max(0, Math.min(item.width, x + w) - left),
|
||||
Math.max(0, Math.min(item.height, y + h) - top))
|
||||
} else {
|
||||
bounds = Qt.rect(0, 0, item.width, item.height)
|
||||
}
|
||||
} else if (boxItem) {
|
||||
bounds = Qt.rect(0, 0, item.width, item.height)
|
||||
}
|
||||
for (var i = 0; i < childrenBounds.count; i++) {
|
||||
var child = childrenBounds.itemAt(i)
|
||||
if (child) bounds = unionBounds(bounds, child.mappedBounds)
|
||||
}
|
||||
if (item.clip && bounds.width > 0 && bounds.height > 0) {
|
||||
var left = Math.max(0, bounds.x)
|
||||
var top = Math.max(0, bounds.y)
|
||||
bounds = Qt.rect(left, top,
|
||||
Math.max(0, Math.min(item.width, bounds.x + bounds.width) - left),
|
||||
Math.max(0, Math.min(item.height, bounds.y + bounds.height) - top))
|
||||
}
|
||||
return bounds
|
||||
}
|
||||
|
||||
function updateMappedBounds() {
|
||||
var b = paintedBounds
|
||||
mappedBounds = sourceItem && boundsParent && b.width > 0 && b.height > 0
|
||||
? sourceItem.mapToItem(boundsParent, b.x, b.y, b.width, b.height)
|
||||
: b
|
||||
}
|
||||
|
||||
function configureSource() {
|
||||
if (sourceItem instanceof Text) sourceItem.renderType = Text.CurveRendering
|
||||
updateMappedBounds()
|
||||
}
|
||||
|
||||
onPaintedBoundsChanged: updateMappedBounds()
|
||||
onSourceItemChanged: configureSource()
|
||||
onBoundsParentChanged: updateMappedBounds()
|
||||
Component.onCompleted: updateMappedBounds()
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font: root.textItem ? root.sourceItem.font : Qt.font({ pixelSize: 12 })
|
||||
text: root.textItem ? root.sourceItem.text : ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.sourceItem
|
||||
function onXChanged() { root.updateMappedBounds() }
|
||||
function onYChanged() { root.updateMappedBounds() }
|
||||
function onWidthChanged() { root.updateMappedBounds() }
|
||||
function onHeightChanged() { root.updateMappedBounds() }
|
||||
function onScaleChanged() { root.updateMappedBounds() }
|
||||
function onRotationChanged() { root.updateMappedBounds() }
|
||||
function onTransformOriginChanged() { root.updateMappedBounds() }
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: childrenBounds
|
||||
model: root.sourceItem ? root.sourceItem.children : []
|
||||
delegate: Loader {
|
||||
id: childLoader
|
||||
required property var modelData
|
||||
readonly property rect mappedBounds: item ? item.mappedBounds : Qt.rect(0, 0, 0, 0)
|
||||
source: Qt.resolvedUrl("IconContentBounds.qml")
|
||||
onLoaded: {
|
||||
item.sourceItem = Qt.binding(function() { return childLoader.modelData })
|
||||
item.boundsParent = Qt.binding(function() { return root.sourceItem })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,22 @@ Item {
|
||||
property string text: ""
|
||||
property string fontFamily: Style.font.family
|
||||
property real fontSize: Style.font.body
|
||||
property real paintedSize: 0
|
||||
property color color: Commons.Color.foreground
|
||||
property bool debugBounds: false
|
||||
|
||||
readonly property int renderedFontSize: Math.max(1, Math.round(fontSize))
|
||||
readonly property real tightWidth: Math.max(1, glyphMetrics.tightBoundingRect.width)
|
||||
readonly property real tightHeight: Math.max(1, glyphMetrics.tightBoundingRect.height)
|
||||
readonly property real paintedScale: paintedSize > 0
|
||||
? Math.min(paintedSize, width, height) / Math.max(tightWidth, tightHeight)
|
||||
: 1
|
||||
readonly property real paintedWidth: tightWidth * paintedScale
|
||||
readonly property real paintedHeight: tightHeight * paintedScale
|
||||
readonly property real horizontalCorrection: glyph.implicitWidth / 2 - (glyphMetrics.tightBoundingRect.x + tightWidth / 2)
|
||||
readonly property real paintedCenterX: glyph.x + glyphMetrics.tightBoundingRect.x + tightWidth / 2
|
||||
readonly property real baselineY: glyph.y + glyph.baselineOffset
|
||||
readonly property real paintedCenterX: glyph.x + (glyphMetrics.tightBoundingRect.x + tightWidth / 2) * paintedScale
|
||||
readonly property real paintedCenterY: glyph.y + (glyph.baselineOffset + glyphMetrics.tightBoundingRect.y + tightHeight / 2) * paintedScale
|
||||
readonly property real baselineY: glyph.y + glyph.baselineOffset * paintedScale
|
||||
|
||||
TextMetrics {
|
||||
id: glyphMetrics
|
||||
@@ -27,17 +35,18 @@ Item {
|
||||
Text {
|
||||
id: glyph
|
||||
textFormat: Text.PlainText
|
||||
// Keep the shared line box and baseline intact. Correcting only the
|
||||
// horizontal painted bounds avoids per-glyph vertical drift.
|
||||
// The correction is fractional, so x is set exactly: centerIn would snap it
|
||||
// to a whole pixel and undo it. y still snaps, keeping the baseline crisp.
|
||||
x: (root.width - width) / 2 + root.horizontalCorrection
|
||||
y: Math.round((root.height - height) / 2)
|
||||
// Bar icons fit their painted bounds. Text labels retain their line box.
|
||||
x: root.width / 2 - (glyphMetrics.tightBoundingRect.x + root.tightWidth / 2) * root.paintedScale
|
||||
y: root.paintedSize > 0
|
||||
? root.height / 2 - (baselineOffset + glyphMetrics.tightBoundingRect.y + root.tightHeight / 2) * root.paintedScale
|
||||
: Math.round((root.height - height) / 2)
|
||||
text: root.text
|
||||
color: root.color
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.renderedFontSize
|
||||
renderType: Text.NativeRendering
|
||||
transformOrigin: Item.TopLeft
|
||||
scale: root.paintedScale
|
||||
renderType: root.paintedSize > 0 ? Text.CurveRendering : Text.NativeRendering
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -65,11 +65,31 @@ Item {
|
||||
// Width of the painted label, for bar chrome that wants to line up with the
|
||||
// text rather than with the slot it sits in. Zero on icon-only buttons.
|
||||
readonly property real labelWidth: label.visible ? label.implicitWidth : 0
|
||||
readonly property bool iconLabel: labelVisible && isIconGlyph(text)
|
||||
readonly property real iconPaintedWidth: iconLabel && iconLoader.item ? iconLoader.item.paintedWidth : 0
|
||||
readonly property real iconPaintedHeight: iconLabel && iconLoader.item ? iconLoader.item.paintedHeight : 0
|
||||
readonly property real iconCenterErrorX: iconLabel && iconLoader.item
|
||||
? iconLoader.x + iconLoader.item.paintedCenterX - width / 2 : 0
|
||||
readonly property real iconCenterErrorY: iconLabel && iconLoader.item
|
||||
? iconLoader.y + iconLoader.item.paintedCenterY - height / 2 : 0
|
||||
|
||||
function isIconGlyph(value) {
|
||||
var text = String(value || "")
|
||||
if (!text) return false
|
||||
var code = text.codePointAt(0)
|
||||
if (text.length !== (code > 0xffff ? 2 : 1)) return false
|
||||
return (code >= 0xe000 && code <= 0xf8ff)
|
||||
|| (code >= 0xf0000 && code <= 0xffffd)
|
||||
|| (code >= 0x100000 && code <= 0x10fffd)
|
||||
|| (code >= 0x2190 && code <= 0x2bff)
|
||||
}
|
||||
|
||||
visible: hasVisualContent || keepSpace
|
||||
opacity: !hasVisualContent || concealed ? 0 : (dimmed ? 0.45 : 1)
|
||||
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2))
|
||||
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + scaledVerticalPadding * 2) : barSize)
|
||||
implicitWidth: fixedWidth > 0 ? fixedWidth
|
||||
: (vertical ? barSize : (iconLabel ? Style.bar.iconSlot : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2)))
|
||||
implicitHeight: fixedHeight > 0 ? fixedHeight
|
||||
: (vertical ? (iconLabel ? Style.bar.iconSlot : Math.max(12, label.implicitHeight + scaledVerticalPadding * 2)) : barSize)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Style.duration(140); easing.type: Easing.OutCubic }
|
||||
@@ -78,7 +98,7 @@ Item {
|
||||
Text {
|
||||
id: label
|
||||
textFormat: Text.PlainText
|
||||
visible: root.labelVisible
|
||||
visible: root.labelVisible && !root.iconLabel
|
||||
anchors.centerIn: parent
|
||||
// Centering the line box puts the figures above the bar's middle, since
|
||||
// the box keeps room for descenders. Center the figure height instead, so
|
||||
@@ -106,6 +126,25 @@ Item {
|
||||
font: label.font
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
active: root.iconLabel
|
||||
width: Style.bar.iconCanvas
|
||||
height: Style.bar.iconCanvas
|
||||
x: (root.width - width) / 2
|
||||
y: (root.height - height) / 2
|
||||
sourceComponent: Component {
|
||||
OpticalGlyph {
|
||||
text: root.text
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: root.fontSize
|
||||
paintedSize: Style.bar.iconSize
|
||||
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
|
||||
rotation: root.textRotation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -2,6 +2,7 @@ module qs.Ui
|
||||
|
||||
BarIndicator 1.0 BarIndicator.qml
|
||||
BarIconButton 1.0 BarIconButton.qml
|
||||
BarIconContent 1.0 BarIconContent.qml
|
||||
BarWidget 1.0 BarWidget.qml
|
||||
BackgroundMedia 1.0 BackgroundMedia.qml
|
||||
BorderOverlay 1.0 BorderOverlay.qml
|
||||
@@ -11,6 +12,7 @@ ButtonGroup 1.0 ButtonGroup.qml
|
||||
ConfirmDialog 1.0 ConfirmDialog.qml
|
||||
CursorSurface 1.0 CursorSurface.qml
|
||||
Dropdown 1.0 Dropdown.qml
|
||||
IconContentBounds 1.0 IconContentBounds.qml
|
||||
KeyboardPanel 1.0 KeyboardPanel.qml
|
||||
MultiSelect 1.0 MultiSelect.qml
|
||||
NumberField 1.0 NumberField.qml
|
||||
|
||||
Reference in new issue
Block a user