add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,182 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Services.UI
import qs.Widgets
/**
* AllBackgrounds - Unified Shape container for all bar and panel backgrounds
*
* Unified shadow system. This component contains a single Shape
* with multiple ShapePath children (one for bar, one for each panel type).
*
* Benefits:
* - Single GPU-accelerated rendering pass for all backgrounds
* - Unified shadow system (one MultiEffect for everything)
*/
Item {
id: root
// Reference Bar
required property var bar
// Reference to MainScreen (for panel access)
required property var windowRoot
readonly property color panelBackgroundColor: Color.mSurface
anchors.fill: parent
// Unified background container
Item {
anchors.fill: parent
// When not using separate bar opacity, use unified approach (original behavior)
Item {
anchors.fill: parent
visible: !Settings.data.bar.useSeparateOpacity
// Enable layer caching to prevent continuous re-rendering
layer.enabled: true
opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity)
Shape {
id: unifiedBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
Component.onCompleted: {
Logger.d("AllBackgrounds", "AllBackgrounds initialized");
}
/**
* Bar
*/
BarBackground {
bar: root.bar
shapeContainer: unifiedBackgroundsShape
windowRoot: root.windowRoot
backgroundColor: panelBackgroundColor
}
/**
* Panel Background Slots
* Only 2 slots needed: one for currently open/opening panel, one for closing panel
*/
// Slot 0: Currently open/opening panel
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[0];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: unifiedBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
// Slot 1: Closing panel (during transitions)
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[1];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: unifiedBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
}
// Apply shadow to the unified backgrounds
NDropShadow {
anchors.fill: parent
source: unifiedBackgroundsShape
}
}
// When using separate bar opacity, separate the rendering
Item {
anchors.fill: parent
visible: Settings.data.bar.useSeparateOpacity
// Panel backgrounds with panel opacity
Item {
anchors.fill: parent
layer.enabled: true
opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity)
Shape {
id: panelBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
/**
* Panel Background Slots
* Only 2 slots needed: one for currently open/opening panel, one for closing panel
*/
// Slot 0: Currently open/opening panel
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[0];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: panelBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
// Slot 1: Closing panel (during transitions)
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[1];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: panelBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
}
// Apply shadow to the panel backgrounds
NDropShadow {
anchors.fill: parent
source: panelBackgroundsShape
}
}
// Bar background with separate opacity
Item {
anchors.fill: parent
layer.enabled: true
opacity: Settings.data.bar.backgroundOpacity
Shape {
id: barBackgroundShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
BarBackground {
bar: root.bar
shapeContainer: barBackgroundShape
windowRoot: root.windowRoot
backgroundColor: panelBackgroundColor
}
}
NDropShadow {
anchors.fill: parent
source: barBackgroundShape
}
}
}
}
}
@@ -0,0 +1,283 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Modules.MainScreen.Backgrounds
import qs.Services.UI
/**
* BarBackground - ShapePath component for rendering the bar background
*
* Unified shadow system. This component is a ShapePath that will be
* a child of the unified AllBackgrounds Shape container.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
// Required reference to the bar component
required property var bar
// Required reference to AllBackgrounds shapeContainer
required property var shapeContainer
// Required reference to windowRoot for screen access
required property var windowRoot
required property color backgroundColor
// Check if bar should be visible on this screen
readonly property bool shouldShow: {
// Check global bar visibility (includes overview state)
if (!BarService.effectivelyVisible)
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || [];
var screenName = windowRoot?.screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName);
}
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// Framed bar properties
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property real frameRadius: Settings.data.bar.frameRadius ?? 20
// Bar position - since bar's parent fills the screen and Shape also fills the screen,
// we can use bar.x and bar.y directly (they're already in screen coordinates)
readonly property point barMappedPos: bar ? Qt.point(bar.x, bar.y) : Qt.point(0, 0)
// Effective dimensions - 0 when bar shouldn't show (similar to panel behavior)
readonly property real barWidth: (bar && shouldShow) ? bar.width : 0
readonly property real barHeight: (bar && shouldShow) ? bar.height : 0
// Screen dimensions for frame
readonly property real screenWidth: windowRoot?.screen?.width || 0
readonly property real screenHeight: windowRoot?.screen?.height || 0
// Inner hole dimensions for framed mode - always relative to screen
readonly property string barPosition: Settings.getBarPositionForScreen(windowRoot?.screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property real holeX: (barPosition === "left") ? barWidth : frameThickness
readonly property real holeY: (barPosition === "top") ? barHeight : frameThickness
readonly property real holeWidth: screenWidth - (barPosition === "left" || barPosition === "right" ? (barWidth + frameThickness) : (frameThickness * 2))
readonly property real holeHeight: screenHeight - (barPosition === "top" || barPosition === "bottom" ? (barHeight + frameThickness) : (frameThickness * 2))
// Flatten corners if bar is too small (handle null bar)
readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
readonly property real tlMultX: bar ? ShapeCornerHelper.getMultX(bar.topLeftCornerState) : 1
readonly property real tlMultY: bar ? ShapeCornerHelper.getMultY(bar.topLeftCornerState) : 1
readonly property real tlRadius: bar ? getCornerRadius(bar.topLeftCornerState) : 0
readonly property real trMultX: bar ? ShapeCornerHelper.getMultX(bar.topRightCornerState) : 1
readonly property real trMultY: bar ? ShapeCornerHelper.getMultY(bar.topRightCornerState) : 1
readonly property real trRadius: bar ? getCornerRadius(bar.topRightCornerState) : 0
readonly property real brMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomRightCornerState) : 1
readonly property real brMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomRightCornerState) : 1
readonly property real brRadius: bar ? getCornerRadius(bar.bottomRightCornerState) : 0
readonly property real blMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomLeftCornerState) : 1
readonly property real blMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomLeftCornerState) : 1
readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0
// True when the bar path has valid, non-degenerate geometry to render.
// Mirrors PanelBackground.isRenderable — prevents CurveRenderer crash on zero-area paths.
readonly property bool isRenderable: bar !== null && shouldShow && (isFramed ? (screenWidth > 0 && screenHeight > 0) : (barWidth > 0 && barHeight > 0))
// Edge overshoot: extend bar background beyond screen edges where both adjacent
// corners are flat (state -1) to prevent CurveRenderer antialiasing artifacts.
// Uses corner state checks instead of radius === 0 since flat corners now have _minR.
readonly property real screenEdgeOvershoot: 2
readonly property real topEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.topRightCornerState === -1 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && bar && bar.bottomLeftCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
readonly property real leftEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.bottomLeftCornerState === -1 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
readonly property real rightEdgeOvs: (!isFramed && shouldShow && bar && bar.topRightCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
// Auto-hide opacity factor for background fade
property real opacityFactor: (bar && bar.isHidden) ? 0 : 1
Behavior on opacityFactor {
enabled: bar && bar.autoHide
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
fillColor: isRenderable ? Qt.rgba(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a * opacityFactor) : "transparent"
fillRule: isFramed ? ShapePath.OddEvenFill : ShapePath.WindingFill
// Starting position — falls back to off-screen when not renderable so that
// all subsequent path elements form a valid non-degenerate off-screen square.
// Each edge is split between PathLine and PathArc so no arc has zero displacement,
// preventing CurveRenderer triangulation crashes on degenerate arcs.
// For framed mode the outer path is a full-screen rectangle; _minR offsets at each
// corner prevent zero-displacement zero-radius arcs that crash qTriangulate.
startX: isRenderable ? (isFramed ? _minR : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
startY: isRenderable ? (isFramed ? 0 : (barMappedPos.y + topEdgeOvs)) : -1
// ========== PATH DEFINITION ==========
// 1. Main Bar / Outer Screen Rectangle
// When !isRenderable all elements use fallback coordinates forming a valid 1×1
// off-screen square with non-degenerate arcs so CurveRenderer never receives
// a zero-area, bare-moveto, or zero-displacement arc path.
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
}
// Top-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
}
// Bottom-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
}
// Bottom-left corner
PathArc {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
}
// Top-left corner (back to start)
PathArc {
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
}
// 2. Inner Hole for Framed Mode (Clockwise)
// When !isFramed, draws a tiny 1x1 rectangle inside the bar as a non-degenerate WindingFill
// no-op to prevent a zero-area degenerate subpath crashing qTriangulate.
// Note: an exact duplicate of the outer path cannot be used here because Qt's CurveRenderer
// has issues with exactly coincident subpaths, causing the fill to not render on some systems.
// When !isRenderable, falls back to a valid 1×1 off-screen square at (-3,-3)→(-2,-2).
readonly property real _nhX: barMappedPos.x + barWidth / 2
readonly property real _nhY: barMappedPos.y + barHeight / 2
PathMove {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
}
// Top edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
}
// Top-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : (root._nhY + 0.25)) : -2.75
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Right edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 1)) : -2
}
// Bottom-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 0.75)) : -2.25
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Bottom edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2
}
// Bottom-left corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 0.75)) : -2.25
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Left edge
PathLine {
x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : root._nhY) : -3
}
// Top-left corner (back to start)
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
}
@@ -0,0 +1,167 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Modules.MainScreen.Backgrounds
/**
* PanelBackground - Dynamic ShapePath for rendering panel backgrounds
*
* Dynamically switches between panels based on which panel is currently
* assigned by PanelService. Only 2 instances are needed: one for the
* currently open panel and one for a closing panel during transitions.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
// Dynamically assigned panel (null if slot is unused)
property var assignedPanel: null
// Required reference to AllBackgrounds shapeContainer
required property var shapeContainer
// Default background color (used if panel doesn't specify one)
property color defaultBackgroundColor: Color.mSurface
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// Get panel's panelRegion (geometry placeholder)
readonly property var panelRegion: assignedPanel?.panelRegion ?? null
// Get the actual panelBackground Item from panelRegion
// Only access panelItem if panelRegion exists and is visible
readonly property var panelBg: (panelRegion && panelRegion.visible) ? panelRegion.panelItem : null
// Effective background color: use panel's if defined, else default
readonly property color effectiveBackgroundColor: {
if (!assignedPanel)
return "transparent";
if (assignedPanel.panelBackgroundColor !== undefined) {
return assignedPanel.panelBackgroundColor;
}
return defaultBackgroundColor;
}
// Panel position - panelBg is in screen coordinates already
readonly property real panelX: panelBg ? panelBg.x : 0
readonly property real panelY: panelBg ? panelBg.y : 0
readonly property real panelWidth: panelBg ? panelBg.width : 0
readonly property real panelHeight: panelBg ? panelBg.height : 0
readonly property bool isRenderable: assignedPanel && panelBg && panelWidth > 0 && panelHeight > 0
// Flatten corners if panel is too small
readonly property bool shouldFlatten: panelBg ? ShapeCornerHelper.shouldFlatten(panelWidth, panelHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? ShapeCornerHelper.getFlattenedRadius(Math.min(panelWidth, panelHeight), radius) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on panelBg's corner states
readonly property real tlMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topLeftCornerState) : 1
readonly property real tlMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topLeftCornerState) : 1
readonly property real tlRadius: panelBg ? getCornerRadius(panelBg.topLeftCornerState) : 0
readonly property real trMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topRightCornerState) : 1
readonly property real trMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topRightCornerState) : 1
readonly property real trRadius: panelBg ? getCornerRadius(panelBg.topRightCornerState) : 0
readonly property real brMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomRightCornerState) : 1
readonly property real brMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomRightCornerState) : 1
readonly property real brRadius: panelBg ? getCornerRadius(panelBg.bottomRightCornerState) : 0
readonly property real blMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomLeftCornerState) : 1
readonly property real blMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomLeftCornerState) : 1
readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
// Start point - use tiny off-screen non-degenerate fallback when not renderable.
// Fallback forms a 1×1 off-screen square where each edge is split between a PathLine
// and a PathArc, ensuring no arc has zero displacement (which can crash qTriangulate).
startX: isRenderable ? (panelX + tlRadius * tlMultX) : -0.75
startY: isRenderable ? panelY : -1
fillColor: isRenderable ? effectiveBackgroundColor : "transparent"
// ========== PATH DEFINITION ==========
// Draws a rectangle with potentially inverted corners
// All coordinates are relative to startX/startY
// Top edge (moving right)
PathLine {
relativeX: root.isRenderable ? (root.panelWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX) : 0.75
relativeY: 0
}
// Top-right corner arc
PathArc {
relativeX: root.isRenderable ? (root.trRadius * root.trMultX) : 0
relativeY: root.isRenderable ? (root.trRadius * root.trMultY) : 0.25
radiusX: root.isRenderable ? root.trRadius : 0
radiusY: root.isRenderable ? root.trRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: root.isRenderable ? (root.panelHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY) : 0.75
}
// Bottom-right corner arc
PathArc {
relativeX: root.isRenderable ? (-root.brRadius * root.brMultX) : -0.25
relativeY: root.isRenderable ? (root.brRadius * root.brMultY) : 0
radiusX: root.isRenderable ? root.brRadius : 0
radiusY: root.isRenderable ? root.brRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
}
// Bottom edge (moving left)
PathLine {
relativeX: root.isRenderable ? (-(root.panelWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX)) : -0.75
relativeY: 0
}
// Bottom-left corner arc
PathArc {
relativeX: root.isRenderable ? (-root.blRadius * root.blMultX) : 0
relativeY: root.isRenderable ? (-root.blRadius * root.blMultY) : -0.25
radiusX: root.isRenderable ? root.blRadius : 0
radiusY: root.isRenderable ? root.blRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
}
// Left edge (moving up) - closes the path back to start
PathLine {
relativeX: 0
relativeY: root.isRenderable ? (-(root.panelHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY)) : -0.75
}
// Top-left corner arc (back to start)
PathArc {
relativeX: root.isRenderable ? (root.tlRadius * root.tlMultX) : 0.25
relativeY: root.isRenderable ? (-root.tlRadius * root.tlMultY) : 0
radiusX: root.isRenderable ? root.tlRadius : 0
radiusY: root.isRenderable ? root.tlRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
}
}
@@ -0,0 +1,74 @@
pragma Singleton
import QtQuick
import QtQuick.Shapes
import Quickshell
/**
* ShapeCornerHelper - Utility singleton for shape corner calculations
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*
* The key technique: Using PathArc direction control (Clockwise vs Counterclockwise)
* combined with multipliers to create both inner and outer corner curves.
*/
Singleton {
id: root
/**
* Get X-axis multiplier for a corner state
* State 1 (horizontal invert) returns -1, others return 1
*/
function getMultX(cornerState) {
return cornerState === 1 ? -1 : 1;
}
/**
* Get Y-axis multiplier for a corner state
* State 2 (vertical invert) returns -1, others return 1
*/
function getMultY(cornerState) {
return cornerState === 2 ? -1 : 1;
}
/**
* Get PathArc direction for a corner based on its multipliers
* Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise
* This creates the outer curve effect for inverted corners
*/
function getArcDirection(multX, multY) {
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise;
}
/**
* Convenience function to get arc direction directly from corner state
*/
function getArcDirectionFromState(cornerState) {
const multX = getMultX(cornerState);
const multY = getMultY(cornerState);
return getArcDirection(multX, multY);
}
/**
* Get the "flattening" radius when shape dimensions are too small
* Prevents visual artifacts when radius exceeds dimensions
*/
function getFlattenedRadius(dimension, requestedRadius) {
if (dimension < requestedRadius * 2) {
return dimension / 2;
}
return requestedRadius;
}
/**
* Check if a shape should use flattened corners
* Returns true if width or height is too small for the requested radius
*/
function shouldFlatten(width, height, radius) {
return width < radius * 2 || height < radius * 2;
}
}