use blobgroup for window chat interface + slim sidebar
C++ / fmt (pull_request) Successful in 4s
C++ / build (pull_request) Failing after 2m1s
C++ / clang-tidy (pull_request) Failing after 1m54s
JS/TS / fmt (pull_request) Failing after 7s
JS/TS / lint (pull_request) Successful in 7s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 51s

This commit is contained in:
2026-09-01 23:14:21 +02:00
parent b254000737
commit ae5c1fa148
6 changed files with 224 additions and 122 deletions
@@ -161,7 +161,7 @@ Item {
scrollAnim.start(); scrollAnim.start();
} }
cacheBuffer: Math.max(height * 20, 0) cacheBuffer: Math.max(height * 2, 0)
clip: true clip: true
fadeAmount: 0.05 fadeAmount: 0.05
fadeThreshold: Tokens.padding.medium fadeThreshold: Tokens.padding.medium
@@ -11,6 +11,7 @@ CustomClippingRect {
property bool expanded: false property bool expanded: false
property bool highlighted: false property bool highlighted: false
property bool slim: false
required property ChatSession modelData required property ChatSession modelData
signal clicked(content: ChatSession) signal clicked(content: ChatSession)
@@ -20,6 +21,9 @@ CustomClippingRect {
implicitHeight: { implicitHeight: {
let h = 0; let h = 0;
if (slim)
return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
h += infoContainer.implicitHeight; h += infoContainer.implicitHeight;
if (expanded) if (expanded)
@@ -85,6 +89,14 @@ CustomClippingRect {
return h; return h;
} }
opacity: root.slim ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomRect { CustomRect {
id: title id: title
@@ -246,7 +258,7 @@ CustomClippingRect {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small font.pointSize: Tokens.font.size.small
maximumLineCount: 1 maximumLineCount: 1
opacity: !root.expanded ? 1 : 0 opacity: !root.expanded && !root.slim ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet") text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y { Behavior on y {
@@ -347,6 +359,16 @@ CustomClippingRect {
radius: Tokens.rounding.full radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
opacity: root.slim ? 0 : 1
visible: opacity > 0
enabled: visible
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer { StateLayer {
onClicked: root.expanded = !root.expanded onClicked: root.expanded = !root.expanded
} }
@@ -14,6 +14,7 @@ Item {
property alias list: list property alias list: list
property alias model: list.model property alias model: list.model
property bool highlight: false property bool highlight: false
property bool slim: false
signal deleteChatRequest(content: ChatSession) signal deleteChatRequest(content: ChatSession)
signal loadChatRequest(content: ChatSession, index: int) signal loadChatRequest(content: ChatSession, index: int)
@@ -79,6 +80,7 @@ Item {
implicitWidth: ListView.view.width implicitWidth: ListView.view.width
highlighted: root.highlight && ChatState.chatSession === modelData highlighted: root.highlight && ChatState.chatSession === modelData
slim: root.slim
onHighlightedChanged: if (highlighted) onHighlightedChanged: if (highlighted)
list.currentIndex = index list.currentIndex = index
@@ -248,6 +250,14 @@ Item {
font.pointSize: Math.round(18 * 1.2) font.pointSize: Math.round(18 * 1.2)
icon: "add" icon: "add"
isRound: ChatState.fabExpanded isRound: ChatState.fabExpanded
opacity: root.slim ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
label.transform: Rotation { label.transform: Rotation {
origin.y: fabRoot.label.height / 2 origin.y: fabRoot.label.height / 2
@@ -14,6 +14,7 @@ Singleton {
property bool inChat: false property bool inChat: false
property bool fabExpanded: false property bool fabExpanded: false
property bool isWindow: false property bool isWindow: false
property bool windowIsWide: false
property ShellScreen screen property ShellScreen screen
property bool sidebarVisible: Visibilities.getForActive().sidebar property bool sidebarVisible: Visibilities.getForActive().sidebar
@@ -6,127 +6,138 @@ import qs.Services
import qs.Modules.Notifications.Sidebar.Chat import qs.Modules.Notifications.Sidebar.Chat
Item { Item {
id: root id: root
property int animOff property int animOff
property Item currentItem property Item currentItem
property bool loading: false property bool noAnim: false
property int lastIdx: -1 property bool loading: false
property int lastIdx: -1
readonly property Component conversationComp: ChatContent {} readonly property Component conversationComp: ChatContent {}
function loadConversation(chat): void { signal requestClose
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
if (!chat) function loadConversation(chat): void {
return; if (currentItem) {
currentItem.destroy();
currentItem = null;
}
root.loading = true; if (!chat)
return;
const incubator = root.conversationComp.incubateObject(container, { root.loading = true;
chatData: chat
});
const attach = () => { const incubator = root.conversationComp.incubateObject(container, {
incubator.object.anchors.fill = container; chatData: chat
currentItem = incubator.object; });
root.loading = false;
enterAnim.start();
};
if (incubator.status === Component.Ready) const attach = () => {
attach(); incubator.object.anchors.fill = container;
else incubator.object.requestClose.connect(root.requestClose);
incubator.onStatusChanged = status => { currentItem = incubator.object;
if (status === Component.Ready) root.loading = false;
attach(); if (!noAnim)
}; enterAnim.start();
} };
Item { if (incubator.status === Component.Ready)
id: container attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
anchors.fill: parent Item {
layer.enabled: opacity < 1 id: container
objectName: "ConversationContainer"
Component.onCompleted: { anchors.fill: parent
if (ChatState.chatSession) layer.enabled: opacity < 1
root.loadConversation(ChatState.chatSession); objectName: "ConversationContainer"
}
}
LoadingIndicator { Component.onCompleted: {
anchors.centerIn: parent if (ChatState.chatSession)
implicitSize: Tokens.font.size.extraLarge * 4 root.loadConversation(ChatState.chatSession);
opacity: root.loading ? 1 : 0 }
visible: opacity > 0 }
Behavior on opacity { LoadingIndicator {
Anim {} anchors.centerIn: parent
} implicitSize: Tokens.font.size.extraLarge * 4
} opacity: root.loading ? 1 : 0
visible: opacity > 0
Connections { Behavior on opacity {
function onChatSessionChanged(): void { Anim {}
exitAnim.complete(); }
enterAnim.complete(); }
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
target: ChatState Connections {
} function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
SequentialAnimation { if (root.noAnim) {
id: exitAnim root.loadConversation(ChatState.chatSession);
return;
}
Anim { root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
property: "opacity" root.lastIdx = ChatState.currentIdx;
target: container exitAnim.start();
to: 0 }
type: Anim.DefaultEffects
}
ScriptAction { target: ChatState
script: root.loadConversation(ChatState.chatSession) }
}
}
SequentialAnimation { SequentialAnimation {
id: enterAnim id: exitAnim
PropertyAction { Anim {
property: "topMargin" property: "opacity"
target: container.anchors target: container
value: root.animOff to: 0
} type: Anim.DefaultEffects
}
PropertyAction { ScriptAction {
property: "bottomMargin" script: root.loadConversation(ChatState.chatSession)
target: container.anchors }
value: -root.animOff }
}
ParallelAnimation { SequentialAnimation {
Anim { id: enterAnim
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim { PropertyAction {
properties: "topMargin,bottomMargin" property: "topMargin"
target: container.anchors target: container.anchors
to: 0 value: root.animOff
type: Anim.SlowEffects }
}
} PropertyAction {
} property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
} }
@@ -4,38 +4,65 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import ZShell.Blobs
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Chat.Content import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services import qs.Services
RowLayout { Item {
id: root id: root
property int breakpoint: 700 property int breakpoint: Math.round(Config.sidebar.sizes.width * 2) + Tokens.spacing.medium * 2 + Tokens.padding.medium * 3
property color blobColor: Colors.tPalette.m3surfaceContainerLow
readonly property int iconSize: Math.round(Tokens.font.size.extraLarge * (96 / 72)) + Tokens.padding.large * 2
property alias conversationModel: sidebar.model property alias conversationModel: sidebar.model
property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint readonly property bool isWide: root.width >= root.breakpoint
property bool narrowShowsSidebar: true
readonly property bool showList: isWide || !chatOpen
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void { function openConversation(conv: ChatSession, index: int): void {
} }
BlobGroup {
id: blobGroup
color: root.blobColor
smoothing: Tokens.rounding.medium
}
BlobInvertedRect {
anchors.fill: parent
borderBottom: Tokens.padding.small
borderLeft: sidebar.implicitWidth + sidebar.anchors.margins + Tokens.spacing.medium
borderRight: Tokens.padding.small
borderTop: Tokens.padding.small
group: blobGroup
opacity: root.blobColor.a
radius: Tokens.rounding.largeIncreased
}
ChatList { ChatList {
id: sidebar id: sidebar
Layout.fillHeight: true anchors.top: parent.top
Layout.margins: Tokens.padding.medium anchors.left: parent.left
Layout.rightMargin: Tokens.spacing.medium anchors.bottom: parent.bottom
Layout.maximumWidth: Config.sidebar.sizes.width anchors.margins: Tokens.padding.medium
Layout.preferredWidth: root.width / 4 implicitWidth: root.isWide ? Config.sidebar.sizes.width : root.iconSize
Layout.minimumWidth: Config.sidebar.sizes.width / 2
highlight: true highlight: true
slim: !root.isWide
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {
type: Anim.DefaultEffects
}
} }
model: ScriptModel { model: ScriptModel {
values: Chat.chats.values values: Chat.chats.values
} }
@@ -46,9 +73,11 @@ RowLayout {
} }
onDeleteChatRequest: chat => { onDeleteChatRequest: chat => {
if (chat === ChatState.chatSession) {
ChatState.chatSession = null;
ChatState.currentIdx = -1;
}
Chat.chats.remove(chat); Chat.chats.remove(chat);
ChatState.currentIdx = -1;
ChatState.chatSession = null;
} }
onNewChatRequest: { onNewChatRequest: {
const data = Chat.chats.insert(); const data = Chat.chats.insert();
@@ -58,12 +87,34 @@ RowLayout {
} }
Item { Item {
Layout.margins: Tokens.padding.extraLarge id: contentArea
Layout.leftMargin: Tokens.spacing.extraLarge
Layout.topMargin: Tokens.padding.large property int leftMargin: sidebar.implicitWidth
Layout.preferredWidth: Config.sidebar.sizes.width * 2
Layout.fillWidth: true z: 1
Layout.fillHeight: true anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium * 2
anchors.right: parent.right
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins
states: State {
name: "wide"
when: !root.isWide
PropertyChanges {
contentArea.leftMargin: root.iconSize
}
}
transitions: Transition {
to: "wide"
Anim {
type: Anim.DefaultEffects
property: "leftMargin"
}
}
ChatHost { ChatHost {
id: convHost id: convHost
@@ -71,10 +122,11 @@ RowLayout {
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.min(parent.width, 800) implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width)
clip: true clip: true
onImplicitWidthChanged: console.log(implicitWidth) onLoadingChanged: if (!loading)
root.chatOpen = true
} }
ColumnLayout { ColumnLayout {
@@ -102,6 +154,12 @@ RowLayout {
} }
} }
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Component { Component {
id: conversationView id: conversationView