feat: initial commit for pixel-snapping components and helpers
This commit is contained in:
@@ -99,6 +99,7 @@ ProgressBar {
|
||||
}
|
||||
|
||||
Line {
|
||||
id: dot
|
||||
property real implicitSize
|
||||
|
||||
anchors.right: parent.right
|
||||
@@ -124,8 +125,7 @@ ProgressBar {
|
||||
anchors.left: parent.left
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
|
||||
@@ -222,8 +222,7 @@ ProgressBar {
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
CAnim {}
|
||||
}
|
||||
Anim on waveProgress {
|
||||
duration: root.waveDuration
|
||||
|
||||
@@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Templates
|
||||
import ZShell.Components
|
||||
import ZShell
|
||||
import qs.Components
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Slider {
|
||||
@@ -28,7 +28,7 @@ Slider {
|
||||
|
||||
signal interaction(v: real)
|
||||
|
||||
implicitHeight: 12
|
||||
implicitHeight: ZUtils.snap(12, root)
|
||||
implicitWidth: 200
|
||||
|
||||
contentItem: Item {
|
||||
@@ -123,7 +123,7 @@ Slider {
|
||||
bottomRightRadius: Tokens.rounding.extraSmall / 2
|
||||
color: root.fgColor
|
||||
implicitHeight: root.height
|
||||
implicitWidth: root.filledWidth
|
||||
implicitWidth: ZUtils.snap(root.filledWidth, root)
|
||||
radius: root.radius
|
||||
topRightRadius: Tokens.rounding.extraSmall / 2
|
||||
}
|
||||
@@ -137,8 +137,8 @@ Slider {
|
||||
frequency: root.waveFrequency
|
||||
fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin
|
||||
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
|
||||
implicitWidth: root.filledWidth
|
||||
lineWidth: root.height * 0.7
|
||||
implicitWidth: ZUtils.snap(root.filledWidth, root)
|
||||
lineWidth: ZUtils.snap(root.height * 0.7, root)
|
||||
startX: x
|
||||
|
||||
Behavior on color {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import ZShell
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
@@ -15,9 +16,9 @@ ButtonBase {
|
||||
const h = label.implicitHeight + padding * 2;
|
||||
if (h % 2 !== 0)
|
||||
return h + 1;
|
||||
return h;
|
||||
return ZUtils.snap(h, root);
|
||||
}
|
||||
implicitWidth: implicitHeight
|
||||
implicitWidth: ZUtils.snap(implicitHeight, root)
|
||||
inactiveColor: {
|
||||
if (!isToggle && type === IconButton.Filled)
|
||||
return Colors.palette.m3primary;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
@@ -34,10 +35,11 @@ ButtonBase {
|
||||
}
|
||||
verticalPadding: Tokens.padding.extraSmall
|
||||
|
||||
RowLayout {
|
||||
SnapLayout {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
|
||||
+89
-66
@@ -1,6 +1,8 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtCanvas2D
|
||||
import ZShell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -36,54 +38,62 @@ Item {
|
||||
property real fillAlpha: 0.15
|
||||
property real lineWidth: 2
|
||||
property real markerDotRadius: 4
|
||||
property real labelFontSize: 10
|
||||
property real labelFontSize: 8
|
||||
|
||||
Canvas {
|
||||
Canvas2D {
|
||||
id: canvas
|
||||
|
||||
anchors.fill: parent
|
||||
renderStrategy: Canvas.Immediate
|
||||
|
||||
alphaBlending: true
|
||||
fillColor: "transparent"
|
||||
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
function snapV(v) {
|
||||
return ZUtils.snap(v, canvas);
|
||||
}
|
||||
|
||||
function strokeCurve(ctx, pts) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pts[0].x, pts[0].y);
|
||||
|
||||
if (root.curveSmoothing && pts.length > 2) {
|
||||
const k = 1 / 6;
|
||||
const n = pts.length;
|
||||
const dpr = ZUtils.dprFor(canvas);
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const [c1x, c1y, c2x, c2y] = segmentControls(pts, i);
|
||||
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const p0 = pts[Math.max(i - 1, 0)];
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[Math.min(i + 2, n - 1)];
|
||||
|
||||
let c1x = p1.x + (p2.x - p0.x) * k;
|
||||
let c1y = p1.y + (p2.y - p0.y) * k;
|
||||
let c2x = p2.x - (p3.x - p1.x) * k;
|
||||
let c2y = p2.y - (p3.y - p1.y) * k;
|
||||
|
||||
const minY = Math.min(p1.y, p2.y);
|
||||
const maxY = Math.max(p1.y, p2.y);
|
||||
c1y = Math.max(minY, Math.min(maxY, c1y));
|
||||
c2y = Math.max(minY, Math.min(maxY, c2y));
|
||||
|
||||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = pts[i];
|
||||
const p1 = pts[i + 1];
|
||||
const cx = (p0.x + p1.x) * 0.5;
|
||||
ctx.bezierCurveTo(cx, p0.y, cx, p1.y, p1.x, p1.y);
|
||||
const firstCol = Math.ceil(p1.x * dpr) + 1;
|
||||
const lastCol = Math.floor(p2.x * dpr) - 1;
|
||||
for (let col = firstCol; col <= lastCol; ++col) {
|
||||
const x = col / dpr;
|
||||
ctx.lineTo(x, bezierY(p1, c1x, c1y, c2x, c2y, p2, x));
|
||||
}
|
||||
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Control points for segment i (pts[i] -> pts[i+1]), matching strokeCurve
|
||||
function bezierY(p1, c1x, c1y, c2x, c2y, p2, targetX) {
|
||||
let tlo = 0;
|
||||
let thi = 1;
|
||||
for (let it = 0; it < 16; it++) {
|
||||
const tm = (tlo + thi) / 2;
|
||||
const u = 1 - tm;
|
||||
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
|
||||
if (x < targetX)
|
||||
tlo = tm;
|
||||
else
|
||||
thi = tm;
|
||||
}
|
||||
|
||||
const t = (tlo + thi) / 2;
|
||||
const u = 1 - t;
|
||||
return u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
||||
}
|
||||
|
||||
function segmentControls(pts, i) {
|
||||
const p0 = pts[Math.max(i - 1, 0)];
|
||||
const p1 = pts[i];
|
||||
@@ -109,7 +119,6 @@ Item {
|
||||
return [cx, p1.y, cx, p2.y];
|
||||
}
|
||||
|
||||
// Point on the curve at a given x (x is monotonic per segment)
|
||||
function pointAtX(pts, targetX) {
|
||||
let j = pts.length - 2;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
@@ -138,10 +147,16 @@ Item {
|
||||
const t = (tlo + thi) / 2;
|
||||
const u = 1 - t;
|
||||
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
||||
return { x: targetX, y };
|
||||
return {
|
||||
x: targetX,
|
||||
y
|
||||
};
|
||||
}
|
||||
|
||||
function cutPoint(pts, targetX) {
|
||||
return pointAtX(pts, targetX);
|
||||
}
|
||||
|
||||
// Part of the curve left/right of a cut x, including the cut point
|
||||
function cutLeft(pts, targetX) {
|
||||
if (targetX <= pts[0].x)
|
||||
return [];
|
||||
@@ -158,7 +173,7 @@ Item {
|
||||
|
||||
const left = pts.slice(0, j + 1);
|
||||
if (targetX > pts[j].x)
|
||||
left.push(pointAtX(pts, targetX));
|
||||
left.push(cutPoint(pts, targetX));
|
||||
return left;
|
||||
}
|
||||
|
||||
@@ -184,17 +199,20 @@ Item {
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
if (!ctx)
|
||||
return;
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const W = width;
|
||||
const H = height;
|
||||
if (W < 2 || H < 2)
|
||||
return;
|
||||
|
||||
const padLeft = root.labelFontSize * 3.4;
|
||||
const padRight = 2;
|
||||
const padTop = 2;
|
||||
const padBottom = root.labelFontSize + 8;
|
||||
const padLeft = root.labelFontSize * 5;
|
||||
const padRight = 0;
|
||||
const padTop = 0;
|
||||
const padBottom = root.labelFontSize + 14;
|
||||
const plot = {
|
||||
x: padLeft,
|
||||
y: padTop,
|
||||
@@ -204,8 +222,6 @@ Item {
|
||||
if (plot.w < 2 || plot.h < 2)
|
||||
return;
|
||||
|
||||
// Auto-fit: NaN yMin/yMax derive from the data, snapped to the
|
||||
// horizontal step grid, honoring minHorizontalLines
|
||||
let lo = root.yMin;
|
||||
let hi = root.yMax;
|
||||
const loAuto = Number.isNaN(lo);
|
||||
@@ -279,11 +295,17 @@ Item {
|
||||
|
||||
const xAt = i => plot.x + (i / root.xSteps) * plot.w;
|
||||
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
|
||||
const crisp = v => Math.floor(v) + 0.5;
|
||||
|
||||
// 1. Grid lines
|
||||
const dpr = ZUtils.dprFor(canvas);
|
||||
const devW = w => Math.max(1, Math.round(w * dpr));
|
||||
const snapLine = (v, w) => {
|
||||
const dw = devW(w);
|
||||
return (Math.round(v * dpr - dw / 2) + dw / 2) / dpr;
|
||||
};
|
||||
const gridW = devW(1) / dpr;
|
||||
|
||||
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.lineWidth = gridW;
|
||||
|
||||
if (root.verticalLineStep > 0) {
|
||||
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
|
||||
@@ -292,7 +314,7 @@ Item {
|
||||
if (i === root.xSteps && !root.lastXLine)
|
||||
continue;
|
||||
|
||||
const x = crisp(xAt(i));
|
||||
const x = snapLine(xAt(i), 1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, plot.y);
|
||||
ctx.lineTo(x, H);
|
||||
@@ -300,12 +322,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Lines extend down into yPaddingBottom, snapped to the grid
|
||||
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
|
||||
|
||||
if (root.horizontalLineStep > 0) {
|
||||
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
|
||||
const y = crisp(yAt(v));
|
||||
const y = snapLine(yAt(v), 1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(plot.x + plot.w, y);
|
||||
@@ -313,9 +334,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Labels
|
||||
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
|
||||
ctx.font = root.labelFontSize + "px sans-serif";
|
||||
ctx.font = root.labelFontSize + "pt 'sans-serif'";
|
||||
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "top";
|
||||
@@ -328,7 +348,7 @@ Item {
|
||||
|
||||
const text = root.xLabel(i);
|
||||
if (text)
|
||||
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 4);
|
||||
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +361,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Curve, split into segments at null gaps
|
||||
const segments = [];
|
||||
let current = null;
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
@@ -352,7 +371,7 @@ Item {
|
||||
continue;
|
||||
}
|
||||
const pt = {
|
||||
x: xAt(i),
|
||||
x: snapV(xAt(i)),
|
||||
y: yAt(series[i])
|
||||
};
|
||||
if (!current)
|
||||
@@ -369,8 +388,6 @@ Item {
|
||||
const markerPx = hasMarker ? xAt(root.markerX) : 0;
|
||||
let gapPx = 0;
|
||||
if (hasMarker) {
|
||||
// markerGap is measured from the dot edge, perpendicular to the
|
||||
// curve; project onto x via the local curve slope
|
||||
let slope = 0;
|
||||
const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x);
|
||||
if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x)
|
||||
@@ -382,7 +399,6 @@ Item {
|
||||
if (seg.length < 2)
|
||||
continue;
|
||||
|
||||
// Fill stays continuous under the marker
|
||||
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
|
||||
strokeCurve(ctx, seg);
|
||||
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
|
||||
@@ -390,7 +406,6 @@ Item {
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Stroke has a gap around the marker
|
||||
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
|
||||
|
||||
for (const pts of pieces) {
|
||||
@@ -398,24 +413,32 @@ Item {
|
||||
continue;
|
||||
|
||||
ctx.strokeStyle = root.lineColor;
|
||||
ctx.lineWidth = root.lineWidth;
|
||||
ctx.lineWidth = ZUtils.snap(root.lineWidth, root);
|
||||
strokeCurve(ctx, pts);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Marker: dashed vertical + dot
|
||||
if (root.markerX >= 0 && root.markerX <= root.xSteps) {
|
||||
const x = markerPx;
|
||||
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(crisp(x), plot.y);
|
||||
ctx.lineTo(crisp(x), H);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.lineWidth = gridW;
|
||||
const lineX = snapLine(x, 1);
|
||||
let dashY = plot.y;
|
||||
let dashOn = true;
|
||||
|
||||
while (dashY < H) {
|
||||
const dashEnd = Math.min(dashY + 4, H);
|
||||
if (dashOn) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lineX, snapV(dashY));
|
||||
ctx.lineTo(lineX, snapV(dashEnd));
|
||||
ctx.stroke();
|
||||
}
|
||||
dashY = dashEnd;
|
||||
dashOn = !dashOn;
|
||||
}
|
||||
|
||||
const i0 = Math.floor(root.markerX);
|
||||
const v0 = series[i0];
|
||||
@@ -430,7 +453,7 @@ Item {
|
||||
if (y != null) {
|
||||
ctx.fillStyle = root.markerColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, root.markerDotRadius, 0, Math.PI * 2);
|
||||
ctx.circle(snapV(x), y, root.markerDotRadius);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import qs.Drawers
|
||||
import qs.Services
|
||||
|
||||
@@ -111,12 +112,14 @@ MouseArea {
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
radius: parent.radius
|
||||
|
||||
ColumnLayout {
|
||||
SnapLayout {
|
||||
id: column
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
orientation: Qt.Vertical
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
@@ -153,11 +156,12 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SnapLayout {
|
||||
id: menuOptionRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
@@ -53,7 +54,7 @@ Row {
|
||||
onClicked: root.active?.clicked()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SnapLayout {
|
||||
id: textRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ZShell
|
||||
import ZShell.Components
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
@@ -47,7 +48,7 @@ CustomRect {
|
||||
z: -1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SnapLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
@@ -88,9 +89,11 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
SnapLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
orientation: Qt.Vertical
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
Reference in New Issue
Block a user