diff --git a/Modules/Notifications/Sidebar/Chat/ChatContent.qml b/Modules/Notifications/Sidebar/Chat/ChatContent.qml index 66cbf63..3fdf1db 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatContent.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatContent.qml @@ -541,13 +541,16 @@ Item { bg.border.color: Colors.palette.m3outlineVariant bg.color: Colors.tPalette.m3surfaceContainerLowest fastGlow: Chat.busy + glow.opacity: input.activeFocus && (!Chat.busy || ChatState.isWindow) font: Tokens.font.body.small implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding) focus: true placeholderText: qsTr("Send a message") sendIcon.font: Tokens.font.icon.medium - sendIcon.icon: "arrow_upward" + sendIcon.icon: Chat.busy ? "stop" : "arrow_upward" sendIcon.padding: Tokens.padding.extraSmall + sendIcon.opacity: text || Chat.busy ? 1.0 : 0.5 + sendIcon.enabled: text || Chat.busy Component.onCompleted: { root.focusInput(); @@ -568,6 +571,11 @@ Item { } } } - onSendPressed: root.send(text) + onButtonPressed: { + if (Chat.busy) + Chat.stop(); + else + root.send(text); + } } } diff --git a/Modules/Notifications/Sidebar/Chat/ChatInput.qml b/Modules/Notifications/Sidebar/Chat/ChatInput.qml index 34d2275..ceb4e77 100644 --- a/Modules/Notifications/Sidebar/Chat/ChatInput.qml +++ b/Modules/Notifications/Sidebar/Chat/ChatInput.qml @@ -8,10 +8,11 @@ TextAreaBase { readonly property alias bg: bg readonly property alias sendIcon: sendIcon + readonly property alias glow: glow property bool fastGlow: false - signal sendPressed + signal buttonPressed bottomPadding: Tokens.padding.large leftPadding: Tokens.padding.extraLarge @@ -39,6 +40,7 @@ TextAreaBase { GeminiGlow { id: glow glowSize: 32 + z: 5 opacity: root.activeFocus ? 1 : 0 visible: opacity > 0 @@ -131,7 +133,7 @@ TextAreaBase { } } - onClicked: root.sendPressed() + onClicked: root.buttonPressed() } TapHandler { diff --git a/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml index d708ce5..3ee1c15 100644 --- a/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml +++ b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml @@ -34,8 +34,12 @@ Item { }); const attach = () => { - incubator.object.anchors.fill = container; + incubator.object.anchors.top = container.top; + incubator.object.anchors.bottom = container.bottom; + incubator.object.anchors.margins = Tokens.padding.medium; + incubator.object.anchors.horizontalCenter = container.horizontalCenter; incubator.object.requestClose.connect(root.requestClose); + incubator.object.implicitWidth = Qt.binding(() => Math.max(Math.min(container.width - Tokens.padding.medium * 2, 800), Config.sidebar.sizes.width)); currentItem = incubator.object; root.loading = false; if (!noAnim) diff --git a/Modules/Notifications/Sidebar/Chat/SidebarView.qml b/Modules/Notifications/Sidebar/Chat/SidebarView.qml index afc93d9..748a3ae 100644 --- a/Modules/Notifications/Sidebar/Chat/SidebarView.qml +++ b/Modules/Notifications/Sidebar/Chat/SidebarView.qml @@ -126,9 +126,8 @@ Item { z: 0 anchors.top: parent.top anchors.bottom: parent.bottom - anchors.margins: Tokens.padding.medium anchors.right: parent.right - implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins * 2 + implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 states: State { name: "wide" @@ -151,10 +150,7 @@ Item { ChatHost { id: convHost - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width) + anchors.fill: parent clip: true onLoadingChanged: if (!loading) diff --git a/Modules/Notifications/Sidebar/Content.qml b/Modules/Notifications/Sidebar/Content.qml index 1a6cea8..41a6728 100644 --- a/Modules/Notifications/Sidebar/Content.qml +++ b/Modules/Notifications/Sidebar/Content.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ZShell.Config +import ZShell.Llm import QtQuick import QtQuick.Layouts import qs.Components @@ -35,10 +36,10 @@ Item { visible: height > 0 } - CustomClippingRect { + Item { Layout.fillHeight: true Layout.fillWidth: true - radius: Tokens.rounding.small + clip: true Item { id: pages @@ -58,6 +59,7 @@ Item { } CustomRect { + id: bg anchors.fill: parent color: Colors.tPalette.m3surfaceContainerLow radius: Tokens.rounding.small @@ -96,11 +98,63 @@ Item { CustomRect { anchors.fill: parent color: Colors.tPalette.m3surfaceContainerLow - radius: Tokens.rounding.small + radius: Tokens.rounding.large ChatPanel { anchors.fill: parent } + + GeminiGlow { + innerGlow: 50 + z: 5 + opacity: Chat.busy ? 1.0 : 0.0 + glowSize: 0 + visible: opacity > 0 && !ChatState.isWindow + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: fadeMask + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Rectangle { + id: fadeMask + + readonly property real fadeStartPos: { + const h = Math.max(1, height); + const nh = (h - h / 3) / h; + return nh; + } + + anchors.fill: parent + layer.enabled: true + visible: false + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + color: Qt.rgba(0, 0, 0, 0) + position: 0 + } + + GradientStop { + color: Qt.rgba(0, 0, 0, 0) + position: fadeMask.fadeStartPos + } + + GradientStop { + color: Qt.rgba(0, 0, 0, 1.0) + position: 1.0 + } + } + } } } } diff --git a/assets/shaders/geminiglow.frag b/assets/shaders/geminiglow.frag index ea19795..1edf7f7 100644 --- a/assets/shaders/geminiglow.frag +++ b/assets/shaders/geminiglow.frag @@ -50,6 +50,18 @@ void main() float r = clamp(radius, 0.0, min(rectSize.x, rectSize.y) * 0.5); float d = sdRoundBox(p, rectSize * 0.5, r); + // ~1px antialiasing width + float aa = max(fwidth(d), 0.75); + + // Coverage of the shape: 1 inside, 0 outside, soft across the edge + float inside = 1.0 - smoothstep(-aa, aa, d); + + // Only skip work when clearly outside and there's no outer glow + if (glowSize <= 0.0 && d > aa) { + fragColor = vec4(0.0); + return; + } + float ang = atan(p.y, p.x); float a2 = phase * TAU; float u = ang / TAU + 0.5 + phase; @@ -59,20 +71,28 @@ void main() float g = 0.0; if (d > 0.0) { float t = clamp(d / max(glowSize, 0.001), 0.0, 1.0); - g = pow(1.0 - t, 3.0); + g = (glowSize > 0.0) ? pow(1.0 - t, 3.0) : 0.0; } else if (innerGlow > 0.0) { - g = exp(d / innerGlow * 3.0); + float t = clamp(-d / innerGlow, 0.0, 1.0); + g = pow(1.0 - t, 3.0); } float pulse = 0.9 + 0.1 * sin(a2 * 2.0 + ang); g *= pulse; + // Line: inside only, with a soft inner edge AND a soft outer edge float line = 0.0; - if (lineWidth > 0.0) - line = 1.0 - smoothstep(0.0, lineWidth, abs(d)); + if (lineWidth > 0.0) { + float inner = 1.0 - smoothstep(lineWidth - aa, lineWidth + aa, -d); + line = inner * inside; + } float a = clamp(g * intensity + line * 0.9, 0.0, 1.0); + // Antialias the outer silhouette when there is no outer glow + if (glowSize <= 0.0) + a *= inside; + col = mix(col, vec3(1.0), line * 0.25); a += (hash(gl_FragCoord.xy) - 0.5) / 255.0; diff --git a/assets/shaders/geminiglow.frag.qsb b/assets/shaders/geminiglow.frag.qsb index 233240e..487934a 100644 Binary files a/assets/shaders/geminiglow.frag.qsb and b/assets/shaders/geminiglow.frag.qsb differ