quickshell: caelestia-style morphing dropdown chrome, single-shape silhouette
One shared panel (bg+border as single Shape paths, CurveRenderer) animates position and size between dropdowns; per-dropdown ears/canvases removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d570674224
commit
ab6f5d5dc8
1 changed files with 130 additions and 249 deletions
|
|
@ -411,6 +411,7 @@ in
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Shapes
|
||||||
import Qt5Compat.GraphicalEffects
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
|
|
@ -456,27 +457,26 @@ in
|
||||||
color: Theme.barBg
|
color: Theme.barBg
|
||||||
}
|
}
|
||||||
|
|
||||||
// The "gap source" for the bar border — dropdown takes priority, then toast
|
// The "gap source" for the bar border — the morphing chrome
|
||||||
property bool hasGap: (activeDropdown && activeDropdown.dropdownHeight > 0)
|
// panel takes priority, then the toast. Tracking the animated
|
||||||
|
// chrome means the border gap follows the morph.
|
||||||
|
property bool hasGap: chrome.visible
|
||||||
|| (toastItem.visible && _toastRect.height > 0)
|
|| (toastItem.visible && _toastRect.height > 0)
|
||||||
property real gapLeft: activeDropdown && activeDropdown.dropdownHeight > 0
|
property real gapLeft: chrome.visible
|
||||||
? activeDropdown.x
|
? chrome.x
|
||||||
: toastItem.visible && _toastRect.height > 0
|
: toastItem.visible && _toastRect.height > 0
|
||||||
? toastItem.x : 0
|
? toastItem.x : 0
|
||||||
property real gapRight: activeDropdown && activeDropdown.dropdownHeight > 0
|
property real gapRight: chrome.visible
|
||||||
? activeDropdown.x + activeDropdown.width
|
? chrome.x + chrome.width
|
||||||
: toastItem.visible && _toastRect.height > 0
|
: toastItem.visible && _toastRect.height > 0
|
||||||
? toastItem.x + toastItem.width : 0
|
? toastItem.x + toastItem.width : 0
|
||||||
property bool gapAlignRight: activeDropdown ? activeDropdown.alignRight : false
|
|
||||||
|
|
||||||
// Bar bottom border — left segment (up to gap)
|
// Bar bottom border — left segment (up to gap). Centered on
|
||||||
|
// y=30 so it runs into the panel's edge-centered border stroke.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: barBorderLeft
|
id: barBorderLeft
|
||||||
// Inset (inside the bar) so it lines up with the inset ear
|
x: 0; y: 30 - Theme.borderWidth / 2
|
||||||
// and dropdown borders; overlaps 8px into the gap to meet
|
width: bar.hasGap ? bar.gapLeft : bar.width
|
||||||
// the ear curve's tapered start.
|
|
||||||
x: 0; y: 30 - Theme.borderWidth
|
|
||||||
width: bar.hasGap ? bar.gapLeft + 8 : bar.width
|
|
||||||
height: Theme.borderWidth
|
height: Theme.borderWidth
|
||||||
color: Theme.base03
|
color: Theme.base03
|
||||||
}
|
}
|
||||||
|
|
@ -484,9 +484,9 @@ in
|
||||||
// Bar bottom border — right segment (after gap)
|
// Bar bottom border — right segment (after gap)
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: barBorderRight
|
id: barBorderRight
|
||||||
visible: bar.hasGap && !bar.gapAlignRight
|
visible: bar.hasGap
|
||||||
x: bar.gapRight - 8
|
x: bar.gapRight
|
||||||
y: 30 - Theme.borderWidth
|
y: 30 - Theme.borderWidth / 2
|
||||||
width: bar.width - x
|
width: bar.width - x
|
||||||
height: Theme.borderWidth
|
height: Theme.borderWidth
|
||||||
color: Theme.base03
|
color: Theme.base03
|
||||||
|
|
@ -988,7 +988,9 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable dropdown component
|
// Dropdown container — content, sizing and autoclose only.
|
||||||
|
// The background, border and ears are drawn once by the shared
|
||||||
|
// `chrome` panel below, which morphs between dropdowns.
|
||||||
component BarDropdown: Item {
|
component BarDropdown: Item {
|
||||||
id: dropdown
|
id: dropdown
|
||||||
property bool open: false
|
property bool open: false
|
||||||
|
|
@ -997,8 +999,8 @@ in
|
||||||
property real fullWidth: 200
|
property real fullWidth: 200
|
||||||
property real fullHeight: 200
|
property real fullHeight: 200
|
||||||
property int autoCloseMs: 1500
|
property int autoCloseMs: 1500
|
||||||
property bool alignRight: false
|
property bool alignRight: false // legacy; all dropdowns now center on their widget
|
||||||
property real dropdownHeight: _dropdownRect.height
|
property real dropdownHeight: open ? fullHeight : 0
|
||||||
default property alias content: dropdownContent.data
|
default property alias content: dropdownContent.data
|
||||||
|
|
||||||
function animateClose() {
|
function animateClose() {
|
||||||
|
|
@ -1013,16 +1015,11 @@ in
|
||||||
if (visible && !closing) _autoClose.restart();
|
if (visible && !closing) _autoClose.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whole-pixel x — fractional positions (odd widths centered
|
x: Math.round(Math.min(bar.width - width, Math.max(0, dropdownX - width / 2)))
|
||||||
// on the bar) antialias the ears/borders into fuzzy seams.
|
|
||||||
x: Math.round(alignRight ? bar.width - width : Math.min(
|
|
||||||
bar.width - width,
|
|
||||||
Math.max(0, dropdownX - (fullWidth + 16) / 2)
|
|
||||||
))
|
|
||||||
y: 30
|
y: 30
|
||||||
|
width: fullWidth + 16
|
||||||
|
height: fullHeight + 4
|
||||||
visible: false
|
visible: false
|
||||||
width: fullWidth + (alignRight ? 8 : 16)
|
|
||||||
height: fullHeight + 4 + (alignRight ? 8 : 0)
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
|
@ -1055,155 +1052,119 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left ear
|
// Content clipped to the chrome's animated height so it
|
||||||
|
// reveals/hides with the morph; fades on open/close.
|
||||||
Item {
|
Item {
|
||||||
anchors.right: _dropdownRect.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8
|
|
||||||
height: Math.min(8, _dropdownRect.height)
|
|
||||||
clip: true
|
|
||||||
visible: _dropdownRect.height >= 8
|
|
||||||
Canvas {
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8; height: 8
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, 8, 8);
|
|
||||||
ctx.fillStyle = Theme.barBg;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.lineTo(8, 8);
|
|
||||||
ctx.arc(0, 8, 8, 0, -Math.PI / 2, true);
|
|
||||||
ctx.closePath(); ctx.fill();
|
|
||||||
// Border stroke along the curve
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = Theme.borderWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 8, 8 + Theme.borderWidth / 2, 0, -Math.PI / 2, true);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right ear (for centered dropdowns)
|
|
||||||
Item {
|
|
||||||
anchors.left: _dropdownRect.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8
|
|
||||||
height: Math.min(8, _dropdownRect.height)
|
|
||||||
clip: true
|
|
||||||
visible: _dropdownRect.height >= 8 && !dropdown.alignRight
|
|
||||||
Canvas {
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8; height: 8
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, 8, 8);
|
|
||||||
ctx.fillStyle = Theme.barBg;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0); ctx.lineTo(8, 0);
|
|
||||||
ctx.arc(8, 8, 8, -Math.PI / 2, Math.PI, true);
|
|
||||||
ctx.closePath(); ctx.fill();
|
|
||||||
// Border stroke along the curve
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = Theme.borderWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(8, 8, 8 + Theme.borderWidth / 2, -Math.PI / 2, Math.PI, true);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: _dropdownRect
|
id: _dropdownRect
|
||||||
anchors.right: dropdown.alignRight ? parent.right : undefined
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.horizontalCenter: dropdown.alignRight ? undefined : parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
width: dropdown.fullWidth
|
width: dropdown.fullWidth
|
||||||
height: dropdown.open ? dropdown.fullHeight : 0
|
height: Math.min(dropdown.fullHeight, chrome.height)
|
||||||
color: Theme.barBg
|
|
||||||
radius: 8
|
|
||||||
topLeftRadius: 0
|
|
||||||
topRightRadius: 0
|
|
||||||
bottomRightRadius: dropdown.alignRight ? 0 : 8
|
|
||||||
clip: true
|
clip: true
|
||||||
|
opacity: dropdown.open ? 1 : 0
|
||||||
// Border outline (sides + bottom with rounded corners)
|
Behavior on opacity {
|
||||||
Canvas {
|
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||||
id: _dropdownBorder
|
|
||||||
anchors.fill: parent
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
var w = width, h = height, r = 8;
|
|
||||||
// o centers the stroke so its outer edge lands on the rect edge
|
|
||||||
var b = Theme.borderWidth, o = b / 2;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
if (h < 1) return;
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = b;
|
|
||||||
ctx.beginPath();
|
|
||||||
// Start just under the bar — the ear band tapers
|
|
||||||
// through the first few px and this fills behind it
|
|
||||||
ctx.moveTo(o, b);
|
|
||||||
ctx.lineTo(o, h - r);
|
|
||||||
// Bottom-left curve — arc centered on the corner circle so
|
|
||||||
// the stroke's outer edge matches the bg corner exactly
|
|
||||||
ctx.arc(r, h - r, r - o, Math.PI, Math.PI / 2, true);
|
|
||||||
// Bottom edge
|
|
||||||
if (dropdown.alignRight) {
|
|
||||||
// Stop 8px before right edge — bottom-right ear continues
|
|
||||||
ctx.lineTo(w - r, h - o);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(w - r - o, h - o);
|
|
||||||
// Bottom-right curve
|
|
||||||
ctx.arc(w - r, h - r, r - o, Math.PI / 2, 0, true);
|
|
||||||
// Right side up to just under the bar
|
|
||||||
ctx.lineTo(w - o, b);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
// Repaint when size changes
|
|
||||||
onWidthChanged: requestPaint()
|
|
||||||
onHeightChanged: requestPaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: dropdownContent
|
id: dropdownContent
|
||||||
anchors.fill: parent
|
width: dropdown.fullWidth
|
||||||
|
height: dropdown.fullHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bottom-right concave ear — connects dropdown bottom to right screen edge
|
// Panel silhouette (caelestia-inspired): fill and border are
|
||||||
Item {
|
// each ONE continuous path — concave ear, side, rounded bottom
|
||||||
visible: dropdown.alignRight && _dropdownRect.height >= 8
|
// corners, side, ear — so there are no seams or junctions at
|
||||||
anchors.right: _dropdownRect.right
|
// all. The border path is open at the top where the panel
|
||||||
anchors.top: _dropdownRect.bottom
|
// joins the bar; the bar's own border strips meet it there.
|
||||||
width: 8
|
component PanelShape: Shape {
|
||||||
height: Math.min(8, _dropdownRect.height)
|
id: pshape
|
||||||
clip: true
|
readonly property real ear: 8
|
||||||
Canvas {
|
readonly property real rr: Math.min(8, Math.max(1, height / 2))
|
||||||
width: 8; height: 8
|
preferredRendererType: Shape.CurveRenderer
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
ShapePath {
|
||||||
ctx.clearRect(0, 0, 8, 8);
|
fillColor: Theme.barBg
|
||||||
ctx.fillStyle = Theme.barBg;
|
strokeWidth: -1
|
||||||
ctx.beginPath();
|
startX: 0; startY: 0
|
||||||
ctx.moveTo(0, 0);
|
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
||||||
ctx.lineTo(8, 0);
|
PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
|
||||||
ctx.lineTo(8, 8);
|
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
||||||
ctx.arc(0, 8, 8, 0, -Math.PI / 2, true);
|
PathLine { x: pshape.width - pshape.ear - pshape.rr; y: pshape.height }
|
||||||
ctx.fill();
|
PathArc { x: pshape.width - pshape.ear; y: pshape.height - pshape.rr; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
||||||
// Border stroke along the curve
|
PathLine { x: pshape.width - pshape.ear; y: Math.min(pshape.ear, pshape.height) }
|
||||||
ctx.strokeStyle = Theme.base03;
|
PathArc { x: pshape.width; y: 0; radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
||||||
ctx.lineWidth = Theme.borderWidth;
|
PathLine { x: 0; y: 0 }
|
||||||
ctx.beginPath();
|
}
|
||||||
ctx.arc(0, 8, 8 + Theme.borderWidth / 2, 0, -Math.PI / 2, true);
|
|
||||||
ctx.stroke();
|
ShapePath {
|
||||||
}
|
fillColor: "transparent"
|
||||||
}
|
strokeColor: Theme.base03
|
||||||
|
strokeWidth: Theme.borderWidth
|
||||||
|
capStyle: ShapePath.FlatCap
|
||||||
|
startX: 0; startY: 0
|
||||||
|
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
||||||
|
PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
|
||||||
|
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
||||||
|
PathLine { x: pshape.width - pshape.ear - pshape.rr; y: pshape.height }
|
||||||
|
PathArc { x: pshape.width - pshape.ear; y: pshape.height - pshape.rr; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
||||||
|
PathLine { x: pshape.width - pshape.ear; y: Math.min(pshape.ear, pshape.height) }
|
||||||
|
PathArc { x: pshape.width; y: 0; radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shared morphing panel: follows the active dropdown's
|
||||||
|
// geometry with animation (the caelestia-style morph), snaps
|
||||||
|
// instantly when opening from closed.
|
||||||
|
Item {
|
||||||
|
id: chrome
|
||||||
|
property real tX: 0
|
||||||
|
property real tW: 200
|
||||||
|
property real tH: 0
|
||||||
|
property real openH: bar.activeDropdown ? tH : 0
|
||||||
|
|
||||||
|
x: tX
|
||||||
|
y: 30
|
||||||
|
width: tW
|
||||||
|
height: openH
|
||||||
|
visible: height > 0.5
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: chrome; property: "tX"
|
||||||
|
value: bar.activeDropdown ? bar.activeDropdown.x : 0
|
||||||
|
when: bar.activeDropdown !== null
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
Binding {
|
||||||
|
target: chrome; property: "tW"
|
||||||
|
value: bar.activeDropdown ? bar.activeDropdown.width : 0
|
||||||
|
when: bar.activeDropdown !== null
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
Binding {
|
||||||
|
target: chrome; property: "tH"
|
||||||
|
value: bar.activeDropdown ? bar.activeDropdown.fullHeight + 4 : 0
|
||||||
|
when: bar.activeDropdown !== null
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on tX {
|
||||||
|
enabled: chrome.height > 0.5
|
||||||
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
Behavior on tW {
|
||||||
|
enabled: chrome.height > 0.5
|
||||||
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
Behavior on openH {
|
||||||
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelShape {
|
||||||
|
width: chrome.width
|
||||||
|
height: chrome.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2293,7 +2254,7 @@ in
|
||||||
|
|
||||||
x: Math.round(bar.width / 2 - width / 2)
|
x: Math.round(bar.width / 2 - width / 2)
|
||||||
y: 30
|
y: 30
|
||||||
width: _toastLeftEar.width + _toastRect.width + _toastRightEar.width
|
width: _toastRect.width + 16
|
||||||
height: _toastRect.height + 4
|
height: _toastRect.height + 4
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
|
|
@ -2360,100 +2321,20 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left inverse corner ear
|
// Same single-path silhouette as the dropdown chrome
|
||||||
Item {
|
PanelShape {
|
||||||
id: _toastLeftEar
|
width: toastItem.width
|
||||||
anchors.right: _toastRect.left
|
height: _toastRect.height
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8
|
|
||||||
height: Math.min(8, _toastRect.height)
|
|
||||||
clip: true
|
|
||||||
visible: _toastRect.height >= 8
|
|
||||||
Canvas {
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8; height: 8
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, 8, 8);
|
|
||||||
ctx.fillStyle = Theme.barBg;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.lineTo(8, 8);
|
|
||||||
ctx.arc(0, 8, 8, 0, -Math.PI / 2, true);
|
|
||||||
ctx.closePath(); ctx.fill();
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = Theme.borderWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 8, 8 + Theme.borderWidth / 2, 0, -Math.PI / 2, true);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right inverse corner ear
|
|
||||||
Item {
|
Item {
|
||||||
id: _toastRightEar
|
|
||||||
anchors.left: _toastRect.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8
|
|
||||||
height: Math.min(8, _toastRect.height)
|
|
||||||
clip: true
|
|
||||||
visible: _toastRect.height >= 8
|
|
||||||
Canvas {
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: 8; height: 8
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, 8, 8);
|
|
||||||
ctx.fillStyle = Theme.barBg;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0); ctx.lineTo(8, 0);
|
|
||||||
ctx.arc(8, 8, 8, -Math.PI / 2, Math.PI, true);
|
|
||||||
ctx.closePath(); ctx.fill();
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = Theme.borderWidth;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(8, 8, 8 + Theme.borderWidth / 2, -Math.PI / 2, Math.PI, true);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: _toastRect
|
id: _toastRect
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
width: 320
|
width: 320
|
||||||
height: toastItem.toastOpen ? toastCol.height + 16 : 0
|
height: toastItem.toastOpen ? toastCol.height + 16 : 0
|
||||||
color: Theme.barBg
|
|
||||||
radius: 8
|
|
||||||
topLeftRadius: 0
|
|
||||||
topRightRadius: 0
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
// Border outline (sides + bottom with rounded corners)
|
|
||||||
Canvas {
|
|
||||||
anchors.fill: parent
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d");
|
|
||||||
var w = width, h = height, r = 8;
|
|
||||||
var b = Theme.borderWidth, o = b / 2;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
if (h < 1) return;
|
|
||||||
ctx.strokeStyle = Theme.base03;
|
|
||||||
ctx.lineWidth = b;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(o, b);
|
|
||||||
ctx.lineTo(o, h - r);
|
|
||||||
ctx.arc(r, h - r, r - o, Math.PI, Math.PI / 2, true);
|
|
||||||
ctx.lineTo(w - r - o, h - o);
|
|
||||||
ctx.arc(w - r, h - r, r - o, Math.PI / 2, 0, true);
|
|
||||||
ctx.lineTo(w - o, b);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
onWidthChanged: requestPaint()
|
|
||||||
onHeightChanged: requestPaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue