better scrollbar in codeblocks + enter anim for delegates
This commit is contained in:
@@ -12,6 +12,78 @@ Item {
|
||||
|
||||
property ChatSession chatData
|
||||
property bool following: true
|
||||
// qmlformat off
|
||||
readonly property var keys: [
|
||||
Qt.Key_A,
|
||||
Qt.Key_B,
|
||||
Qt.Key_C,
|
||||
Qt.Key_D,
|
||||
Qt.Key_E,
|
||||
Qt.Key_F,
|
||||
Qt.Key_G,
|
||||
Qt.Key_H,
|
||||
Qt.Key_I,
|
||||
Qt.Key_J,
|
||||
Qt.Key_K,
|
||||
Qt.Key_L,
|
||||
Qt.Key_M,
|
||||
Qt.Key_N,
|
||||
Qt.Key_O,
|
||||
Qt.Key_P,
|
||||
Qt.Key_Q,
|
||||
Qt.Key_R,
|
||||
Qt.Key_S,
|
||||
Qt.Key_T,
|
||||
Qt.Key_U,
|
||||
Qt.Key_V,
|
||||
Qt.Key_W,
|
||||
Qt.Key_X,
|
||||
Qt.Key_Y,
|
||||
Qt.Key_Z,
|
||||
|
||||
Qt.Key_Agrave,
|
||||
Qt.Key_Aacute,
|
||||
Qt.Key_Acircumflex,
|
||||
Qt.Key_Atilde,
|
||||
Qt.Key_Adiaeresis,
|
||||
Qt.Key_Aring,
|
||||
Qt.Key_AE,
|
||||
|
||||
Qt.Key_Ccedilla,
|
||||
|
||||
Qt.Key_Egrave,
|
||||
Qt.Key_Eacute,
|
||||
Qt.Key_Ecircumflex,
|
||||
Qt.Key_Ediaeresis,
|
||||
|
||||
Qt.Key_Igrave,
|
||||
Qt.Key_Iacute,
|
||||
Qt.Key_Icircumflex,
|
||||
Qt.Key_Idiaeresis,
|
||||
|
||||
Qt.Key_ETH,
|
||||
|
||||
Qt.Key_Ntilde,
|
||||
|
||||
Qt.Key_Ograve,
|
||||
Qt.Key_Oacute,
|
||||
Qt.Key_Ocircumflex,
|
||||
Qt.Key_Otilde,
|
||||
Qt.Key_Odiaeresis,
|
||||
Qt.Key_Ooblique,
|
||||
|
||||
Qt.Key_Ugrave,
|
||||
Qt.Key_Uacute,
|
||||
Qt.Key_Ucircumflex,
|
||||
Qt.Key_Udiaeresis,
|
||||
|
||||
Qt.Key_Yacute,
|
||||
Qt.Key_ydiaeresis,
|
||||
|
||||
Qt.Key_THORN,
|
||||
Qt.Key_ssharp
|
||||
]
|
||||
// qmlformat on
|
||||
|
||||
signal requestClose
|
||||
|
||||
@@ -27,6 +99,21 @@ Item {
|
||||
input.text = "";
|
||||
}
|
||||
|
||||
function focusInput(): void {
|
||||
Qt.callLater(() => input.forceActiveFocus());
|
||||
}
|
||||
|
||||
onChatDataChanged: {
|
||||
if (chatData)
|
||||
focusInput();
|
||||
}
|
||||
Keys.onPressed: e => {
|
||||
if (root.keys.includes(e.key)) {
|
||||
input.insert(input.length, e.text);
|
||||
focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
@@ -68,9 +155,11 @@ Item {
|
||||
id: list
|
||||
|
||||
property bool userScrolledUp: false
|
||||
property real lastCHeight: 0.0
|
||||
|
||||
function scrollToBottom(): void {
|
||||
Qt.callLater(() => positionViewAtBeginning());
|
||||
scrollAnim.to = 0;
|
||||
scrollAnim.start();
|
||||
}
|
||||
|
||||
cacheBuffer: height * 20
|
||||
@@ -80,47 +169,67 @@ Item {
|
||||
model: root.chatData.messagesModel
|
||||
spacing: 0
|
||||
rotation: 180
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: -100
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
// add: Transition {
|
||||
// Anim {
|
||||
// from: 10
|
||||
// property: "y"
|
||||
// }
|
||||
// }
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
id: scrollBar
|
||||
|
||||
flickable: list
|
||||
parent: list.parent
|
||||
anchors.top: list.top
|
||||
anchors.right: list.right
|
||||
anchors.bottom: list.bottom
|
||||
|
||||
transform: Rotation {
|
||||
origin.y: list.height / 2
|
||||
origin.x: scrollBar.width / 2
|
||||
angle: 180
|
||||
|
||||
axis {
|
||||
y: 0
|
||||
x: 1
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: MessageDelegate {
|
||||
rotation: 180
|
||||
}
|
||||
|
||||
// displaced: Transition {
|
||||
// Anim {
|
||||
// property: "y"
|
||||
// }
|
||||
// }
|
||||
// move: Transition {
|
||||
// Anim {
|
||||
// property: "y"
|
||||
// }
|
||||
// }
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
positionViewAtBeginning();
|
||||
forceActiveFocus();
|
||||
}
|
||||
onAtYEndChanged: {
|
||||
if (atYEnd)
|
||||
onAtYBeginningChanged: {
|
||||
if (atYBeginning)
|
||||
userScrolledUp = false;
|
||||
}
|
||||
onContentHeightChanged: {
|
||||
if (!userScrolledUp && atYEnd)
|
||||
scrollToBottom();
|
||||
}
|
||||
onCountChanged: {
|
||||
if (!userScrolledUp)
|
||||
scrollToBottom();
|
||||
if (userScrolledUp && Chat.busy) {
|
||||
const delta = contentHeight - lastCHeight;
|
||||
contentY += delta;
|
||||
}
|
||||
|
||||
lastCHeight = contentHeight;
|
||||
}
|
||||
onMovingChanged: {
|
||||
if (moving)
|
||||
userScrolledUp = !atYEnd;
|
||||
userScrolledUp = !atYBeginning;
|
||||
}
|
||||
|
||||
Anim {
|
||||
@@ -179,7 +288,7 @@ Item {
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: {
|
||||
listLoader.item.positionViewAtBeginning();
|
||||
listLoader.item.scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,11 +303,13 @@ Item {
|
||||
bg.color: Colors.tPalette.m3surfaceContainerLowest
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
|
||||
focus: true
|
||||
placeholderText: qsTr("Send a message")
|
||||
sendIcon.font.pointSize: Tokens.font.size.large
|
||||
sendIcon.icon: "arrow_upward"
|
||||
sendIcon.padding: Tokens.padding.extraSmall
|
||||
|
||||
Component.onCompleted: root.focusInput()
|
||||
Keys.onPressed: e => {
|
||||
if (e.key == Qt.Key_Return) {
|
||||
if (!(e.modifiers & Qt.ShiftModifier)) {
|
||||
|
||||
@@ -31,7 +31,7 @@ TextAreaBase {
|
||||
enabled: !root.activeFocus
|
||||
manualPressOverride: tapHandler.pressed
|
||||
|
||||
onClicked: root.focus = true
|
||||
onClicked: root.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
TextEditBase {
|
||||
id: root
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
readOnly: true
|
||||
anchors.margins: Tokens.padding.medium
|
||||
textFormat: Text.MarkdownText
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
preventStealing: false
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
@@ -7,7 +8,7 @@ import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property string language
|
||||
@@ -15,16 +16,11 @@ CustomClippingRect {
|
||||
property bool copied: false
|
||||
property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
|
||||
property color codeHeaderColor: Colors.palette.m3outline
|
||||
|
||||
// Highlighter spans for the current code; refreshed when the code or
|
||||
// its language changes. Highlighting runs off the GUI thread; the
|
||||
// token drops results that arrive after the code already changed.
|
||||
property var codeSpans: []
|
||||
property int highlightToken: 0
|
||||
|
||||
function refresh() {
|
||||
const token = ++root.highlightToken;
|
||||
codeSpans = [];
|
||||
CodeHighlighter.highlight(root.code, root.language, root, token);
|
||||
}
|
||||
|
||||
@@ -84,10 +80,14 @@ CustomClippingRect {
|
||||
let pos = 0;
|
||||
for (let i = 0; i < spans.length; i++) {
|
||||
const span = spans[i];
|
||||
if (span.start > pos)
|
||||
out += escapeHtml(code.slice(pos, span.start));
|
||||
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(span.start, span.start + span.length)) + "</font>";
|
||||
pos = span.start + span.length;
|
||||
const start = Math.min(span.start, code.length);
|
||||
const end = Math.min(span.start + span.length, code.length);
|
||||
if (end <= pos)
|
||||
continue;
|
||||
if (start > pos)
|
||||
out += escapeHtml(code.slice(pos, start));
|
||||
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(start, end)) + "</font>";
|
||||
pos = end;
|
||||
}
|
||||
if (pos < code.length)
|
||||
out += escapeHtml(code.slice(pos));
|
||||
@@ -100,7 +100,10 @@ CustomClippingRect {
|
||||
color: root.codeBackgroundColor
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
onLanguageChanged: refresh()
|
||||
onLanguageChanged: {
|
||||
codeSpans = [];
|
||||
refresh();
|
||||
}
|
||||
onCodeChanged: refresh()
|
||||
|
||||
RowLayout {
|
||||
@@ -212,7 +215,7 @@ CustomClippingRect {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
CustomClippingRect {
|
||||
id: codeRect
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -226,6 +229,21 @@ CustomClippingRect {
|
||||
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
|
||||
color: CodeColors.active.bg
|
||||
|
||||
CustomText {
|
||||
id: code
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: implicitWidth * (0 - codeFlick.visibleArea.xPosition) + Tokens.padding.small
|
||||
anchors.margins: Tokens.padding.small
|
||||
text: root.highlightedHtml(root.code, root.codeSpans)
|
||||
textFormat: Text.RichText
|
||||
color: CodeColors.active.normal
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.small
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: codeFlick
|
||||
|
||||
@@ -234,15 +252,23 @@ CustomClippingRect {
|
||||
|
||||
CustomScrollBar.horizontal: CustomScrollBar {
|
||||
flickable: codeFlick
|
||||
parent: codeFlick.parent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
TextAreaBase.flickable: TextAreaBase {
|
||||
id: codeText
|
||||
|
||||
color: CodeColors.active.normal
|
||||
layer.enabled: true
|
||||
leftInset: Tokens.padding.small
|
||||
clip: false
|
||||
font.family: Config.appearance.font.family.mono
|
||||
font.pointSize: Tokens.font.size.small
|
||||
textFormat: Text.RichText
|
||||
text: root.highlightedHtml(root.code, root.codeSpans)
|
||||
text: code.text
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ Item {
|
||||
|
||||
implicitHeight: bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
|
||||
|
||||
CustomRect {
|
||||
CustomClippingRect {
|
||||
id: bubble
|
||||
|
||||
radius: Tokens.rounding.medium
|
||||
@@ -33,16 +33,21 @@ Item {
|
||||
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2
|
||||
anchors.right: root.isUser ? parent.right : undefined
|
||||
|
||||
Behavior on implicitHeight {
|
||||
// enabled: root.segment.running
|
||||
|
||||
Anim {}
|
||||
}
|
||||
// Behavior on implicitHeight {
|
||||
// enabled: !root.segment.running
|
||||
//
|
||||
// Anim {
|
||||
// type: Anim.DefaultEffects
|
||||
// }
|
||||
// }
|
||||
|
||||
// User messages stay a plain editable text field.
|
||||
TextEditBase {
|
||||
id: msgText
|
||||
|
||||
property string cachedText: root.segment.text
|
||||
property bool cancelled: false
|
||||
|
||||
visible: root.isUser
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
@@ -65,27 +70,29 @@ Item {
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key == Qt.Key_Escape) {
|
||||
text = root.segment.text;
|
||||
cancelled = true;
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
const old = root.segment.text;
|
||||
if (old !== text)
|
||||
root.edit(text);
|
||||
|
||||
readOnly = true;
|
||||
}
|
||||
onReadOnlyChanged: {
|
||||
if (readOnly) {
|
||||
animateCursor = false;
|
||||
root.forceActiveFocus();
|
||||
textFormat = CustomText.MarkdownText;
|
||||
|
||||
if (cancelled) {
|
||||
text = cachedText;
|
||||
cancelled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
root.edit(text);
|
||||
} else {
|
||||
var raw = root.segment.text;
|
||||
textFormat = CustomText.PlainText;
|
||||
text = raw;
|
||||
text = cachedText;
|
||||
forceActiveFocus();
|
||||
cursorPosition = text.length;
|
||||
animateCursor = true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
@@ -7,7 +8,6 @@ import qs.Services
|
||||
Column {
|
||||
id: root
|
||||
|
||||
// Top-level markdown blocks (see MarkdownParser / LlmSegment.markdown).
|
||||
required property var blocks
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
@@ -15,8 +15,10 @@ Column {
|
||||
Repeater {
|
||||
id: blockRep
|
||||
|
||||
model: root.blocks
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.blocks
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
|
||||
@@ -50,7 +52,7 @@ Column {
|
||||
DelegateChoice {
|
||||
roleValue: LlmMarkdown.Type.Heading
|
||||
|
||||
delegate: CustomText {
|
||||
delegate: TextEditBase {
|
||||
required property var modelData
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
@@ -59,10 +61,11 @@ Column {
|
||||
anchors.margins: Tokens.padding.medium
|
||||
text: modelData.text
|
||||
textFormat: Text.MarkdownText
|
||||
readOnly: true
|
||||
font.bold: true
|
||||
font.pointSize: {
|
||||
if (modelData.level <= 1)
|
||||
return Tokens.font.size.larger;
|
||||
return Tokens.font.size.large;
|
||||
if (modelData.level === 2)
|
||||
return Tokens.font.size.normal;
|
||||
return Tokens.font.size.smaller;
|
||||
@@ -74,17 +77,13 @@ Column {
|
||||
DelegateChoice {
|
||||
roleValue: LlmMarkdown.Type.Text
|
||||
|
||||
delegate: CustomText {
|
||||
delegate: BubbleEdit {
|
||||
required property var modelData
|
||||
|
||||
color: Colors.palette.m3onSurface
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
text: modelData.text
|
||||
textFormat: Text.MarkdownText
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,9 @@ MouseArea {
|
||||
Anim {
|
||||
target: root
|
||||
property: "x"
|
||||
to: changeAnim.next ? -root.width / 2 : root.width / 2
|
||||
to: changeAnim.next ? root.width / 4 : -root.width / 4
|
||||
from: 0
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
@@ -92,6 +93,7 @@ MouseArea {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +103,8 @@ MouseArea {
|
||||
Anim {
|
||||
target: root
|
||||
property: "x"
|
||||
from: changeAnim.next ? root.width / 2 : -root.width / 2
|
||||
from: changeAnim.next ? -root.width / 4 : root.width / 4
|
||||
type: Anim.FastEffects
|
||||
to: 0
|
||||
}
|
||||
|
||||
@@ -109,30 +112,21 @@ MouseArea {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 0
|
||||
type: Anim.FastEffects
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Behavior on implicitHeight {
|
||||
// enabled: !root.isUser && root.current.streaming
|
||||
//
|
||||
// Anim {}
|
||||
// }
|
||||
|
||||
onCurrentChanged: {
|
||||
console.log(modelData.generations.indexOf(current));
|
||||
Binding {
|
||||
property: "contentY"
|
||||
restoreMode: Binding.RestoreNone
|
||||
target: root.ListView.view
|
||||
value: root.y + root.height + Tokens.padding.large * 2 - root.ListView.view.height
|
||||
when: root.reasoningExpanded && root.ListView.view && (root.y + root.height + Tokens.padding.large * 2 > root.ListView.view.contentY + root.ListView.view.height)
|
||||
}
|
||||
|
||||
// Binding {
|
||||
// property: "contentY"
|
||||
// restoreMode: Binding.RestoreNone
|
||||
// target: root.ListView.view
|
||||
// value: root.y - Tokens.padding.large * 2
|
||||
// when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY)
|
||||
// }
|
||||
|
||||
Anim {
|
||||
id: restoreAnim
|
||||
|
||||
@@ -148,20 +142,6 @@ MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
// onImplicitHeightChanged: console.log("LAYOUT:", layout.implicitHeight)
|
||||
|
||||
// add: Transition {
|
||||
// Anim {
|
||||
// from: 10
|
||||
// property: "y"
|
||||
// }
|
||||
// }
|
||||
// move: Transition {
|
||||
// Anim {
|
||||
// property: "y"
|
||||
// }
|
||||
// }
|
||||
|
||||
Repeater {
|
||||
id: segmentRep
|
||||
|
||||
|
||||
Reference in New Issue
Block a user