diff --git a/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml b/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml
index 9b7a2f943c7b85c3588f38f70ffacc3a3fb02db6..8f21bb78c0bd40dfb1c912c9833bcf0cc8305526 100644
--- a/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml
+++ b/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml
@@ -28,6 +28,7 @@ import org.ukui.quick.platform 1.0
MouseArea {
id: cardMouseArea
+ hoverEnabled: true
readonly property Search.InstantResultState instantResult: searchView.instantResultState
readonly property bool isCalculation: instantResult.valid
@@ -43,8 +44,41 @@ MouseArea {
DtThemeBackground {
anchors.fill: parent
useStyleTransparency: false
+ alpha: 0.5
radius: GlobalTheme.kRadiusMenu
- backgroundColor: GlobalTheme.BaseActive
+ backgroundColor: GlobalTheme.kContainGeneralNormal
+ }
+
+ ShaderEffect {
+ id: animatedBackground
+ anchors.fill: parent
+
+ property real time: 0
+ property vector2d resolution: Qt.vector2d(width, height)
+ property real cornerRadius: GlobalTheme.kRadiusMenu
+
+ fragmentShader: "qrc:/ukui-search/org.ukui.search/ui/QuickSearchCardBackground.frag"
+
+ // 窗口隐藏后暂停动画,动画更新限制约为30FPS
+ Timer {
+ interval: 33
+ repeat: true
+ running: cardMouseArea.visible && searchView.visible
+ onTriggered: animatedBackground.time = (animatedBackground.time + interval) % 1000000
+ }
+ }
+
+ ShaderEffect {
+ id: focusBorder
+
+ anchors.fill: parent
+ visible: cardMouseArea.containsMouse || cardMouseArea.containsPress || cardMouseArea.activeFocus
+
+ property vector2d resolution: Qt.vector2d(width, height)
+ property real cornerRadius: GlobalTheme.kRadiusMenu
+ property real borderWidth: 1.0
+
+ fragmentShader: "qrc:/ukui-search/org.ukui.search/ui/QuickSearchCardBorder.frag"
}
Loader {
diff --git a/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag
new file mode 100644
index 0000000000000000000000000000000000000000..fe22c4be2bcb9d21df9fa37cb03d503532b7333f
--- /dev/null
+++ b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag
@@ -0,0 +1,163 @@
+uniform lowp float qt_Opacity;
+uniform highp float time;
+uniform highp vec2 resolution;
+uniform highp float cornerRadius;
+
+varying highp vec2 qt_TexCoord0;
+
+//设计稿相关CSS代码:
+//第一层:渐变遮罩 background: linear-gradient(180deg, #ffffffb3 0%, #ffffff8f 52%, #ffffff00 100%);
+//第二层:GLSL动画 透明度为100%
+//第三层:遮罩层 box-shadow: inset 0px -8px 32px 0px rgba(255, 255, 255, 0.25)
+
+// 动画流动速度,数值越大流动越快。
+const float SPEED = 0.2;
+// 有机形变强度,控制颜色区域的流体扭曲程度。
+const float DISTORTION = 1.0;
+// 旋涡扭曲强度。
+const float SWIRL = 0.5;
+// 参与混合的动态颜色数量。
+const int COLOR_COUNT = 3;
+// 颜色团聚合度,数值越大边缘越集中。
+const float DIST_POWER = 4.8;
+// 颜色边缘的颗粒扰动强度。
+const float GRAIN_MIXER = 0.4;
+// 动态颜色内阴影的透明度。
+const float ANIMATION_OPACITY = 1.0;
+// 对应 box-shadow 的 rgba(255, 255, 255, 0.25)。
+const float EDGE_HIGHLIGHT_OPACITY = 0.25;
+// 对应 box-shadow 的 32px blur,使用距离场平滑模拟。
+const float EDGE_HIGHLIGHT_BLUR_RADIUS = 32.0;
+
+// 动态渐变使用的三种颜色,第四个分量为各颜色自身透明度。
+const vec4 COLOR_0 = vec4(0.1412, 0.5843, 1.0, 0.9);
+const vec4 COLOR_1 = vec4(0.5560, 0.3805, 1.0, 0.7);
+const vec4 COLOR_2 = vec4(0.8319, 0.8991, 1.0, 1.0);
+
+float hash21(vec2 position)
+{
+ position = fract(position * vec2(0.3183099, 0.3678794)) + 0.1;
+ position += dot(position, position + 19.19);
+ return fract(position.x * position.y);
+}
+
+float valueNoise(vec2 position)
+{
+ vec2 integerPart = floor(position);
+ vec2 fractionalPart = fract(position);
+ float bottomLeft = hash21(integerPart);
+ float bottomRight = hash21(integerPart + vec2(1.0, 0.0));
+ float topLeft = hash21(integerPart + vec2(0.0, 1.0));
+ float topRight = hash21(integerPart + vec2(1.0, 1.0));
+ vec2 interpolation = fractionalPart * fractionalPart * (3.0 - 2.0 * fractionalPart);
+
+ return mix(mix(bottomLeft, bottomRight, interpolation.x),
+ mix(topLeft, topRight, interpolation.x), interpolation.y);
+}
+
+vec2 rotate(vec2 position, float angle)
+{
+ return mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * position;
+}
+
+vec2 getPosition(int index, float currentTime)
+{
+ float floatIndex = float(index);
+ float phase = floatIndex * 0.37;
+ float horizontalSpeed = 0.6 + fract(floatIndex / 3.0) * 0.9;
+ float verticalSpeed = 0.8 + fract((floatIndex + 1.0) / 4.0);
+
+ return 0.5 + 0.5 * vec2(sin(currentTime * horizontalSpeed + phase),
+ cos(currentTime * verticalSpeed + phase * 1.5));
+}
+
+float roundedRectangleDistance(vec2 uv)
+{
+ vec2 halfSize = resolution * 0.5;
+ vec2 position = (uv - 0.5) * resolution;
+ vec2 cornerDistance = abs(position) - halfSize + vec2(cornerRadius);
+
+ return length(max(cornerDistance, 0.0))
+ + min(max(cornerDistance.x, cornerDistance.y), 0.0)
+ - cornerRadius;
+}
+
+float roundedRectangleMask(vec2 uv)
+{
+ return 1.0 - smoothstep(-1.0, 1.0, roundedRectangleDistance(uv));
+}
+
+vec4 compositeOver(vec4 background, vec4 foreground)
+{
+ return foreground + background * (1.0 - foreground.a);
+}
+
+void main()
+{
+ vec2 uv = qt_TexCoord0;
+ float currentTime = (time * 0.001 + 41.5) * SPEED;
+ vec2 grainUv = uv * 1000.0;
+ float mixerGrain = 0.4 * GRAIN_MIXER * (valueNoise(grainUv) - 0.5);
+
+ float radius = smoothstep(0.0, 1.0, length(uv - 0.5));
+ float center = 1.0 - radius;
+
+ for (float index = 1.0; index <= 2.0; index++) {
+ uv.x += DISTORTION * center / index
+ * sin(currentTime + index * 0.4 * smoothstep(0.0, 1.0, uv.y))
+ * cos(0.2 * currentTime + index * 2.4 * smoothstep(0.0, 1.0, uv.y));
+ uv.y += DISTORTION * center / index
+ * cos(currentTime + index * 2.0 * smoothstep(0.0, 1.0, uv.x));
+ }
+
+ vec2 rotatedUv = rotate(uv - 0.5, -3.0 * SWIRL * radius) + 0.5;
+ vec3 color = vec3(0.0);
+ float alpha = 0.0;
+ float totalWeight = 0.0;
+
+ for (int index = 0; index < COLOR_COUNT; index++) {
+ vec4 currentColor = COLOR_0;
+ if (index == 1)
+ currentColor = COLOR_1;
+ else if (index == 2)
+ currentColor = COLOR_2;
+
+ vec2 colorPosition = getPosition(index, currentTime) + mixerGrain;
+ float distance = pow(length(rotatedUv - colorPosition), DIST_POWER);
+ float weight = 1.0 / (distance + 1e-3);
+
+ color += currentColor.rgb * currentColor.a * weight;
+ alpha += currentColor.a * weight;
+ totalWeight += weight;
+ }
+
+ color /= max(1e-4, totalWeight);
+ alpha = clamp(alpha / max(1e-4, totalWeight), 0.0, 1.0);
+
+ // 第三层:基于 inset 0px -8px 32px 0px rgba(255, 255, 255, 0.25)
+ // 的强度和模糊范围,扩展为四周向内渐隐的白色内阴影。
+ // 距离场在圆角矩形内为负值,取反后得到到最近边缘的内部距离。
+ float distanceFromEdge = max(0.0, -roundedRectangleDistance(qt_TexCoord0));
+ float edgeHighlightMask = 1.0 - smoothstep(0.0,
+ EDGE_HIGHLIGHT_BLUR_RADIUS, distanceFromEdge);
+ float edgeHighlightOpacity = EDGE_HIGHLIGHT_OPACITY * edgeHighlightMask;
+ vec4 result = vec4(vec3(edgeHighlightOpacity), edgeHighlightOpacity);
+
+ // 第二层:与第三层使用相同范围的动态颜色内阴影。
+ vec3 dynamicColor = color / max(1e-4, alpha);
+ float dynamicEdgeOpacity = alpha * edgeHighlightOpacity * ANIMATION_OPACITY;
+ result = compositeOver(result, vec4(dynamicColor * dynamicEdgeOpacity, dynamicEdgeOpacity));
+
+ // 第一层:对应 linear-gradient(180deg, #ffffffb3 0%, #ffffff8f 52%, #ffffff00 100%)。
+ float gradientOpacity;
+ if (qt_TexCoord0.y <= 0.52) {
+ gradientOpacity = mix(0.702, 0.561,
+ qt_TexCoord0.y / 0.52);
+ } else {
+ gradientOpacity = mix(0.561, 0.0,
+ (qt_TexCoord0.y - 0.52) / 0.48);
+ }
+ result = compositeOver(result, vec4(vec3(gradientOpacity), gradientOpacity));
+
+ gl_FragColor = result * roundedRectangleMask(qt_TexCoord0) * qt_Opacity;
+}
diff --git a/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag
new file mode 100644
index 0000000000000000000000000000000000000000..25be326f3ae93dd613be1231ee6aefeb4bf606d1
--- /dev/null
+++ b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag
@@ -0,0 +1,44 @@
+uniform lowp float qt_Opacity;
+uniform highp vec2 resolution;
+uniform highp float cornerRadius;
+uniform highp float borderWidth;
+
+varying highp vec2 qt_TexCoord0;
+// 边框渐变参数:border: 1px solid;
+// border-image: linear-gradient(270deg, #0036f5 0%, #4e75ff 34%, #8a41ff 71%, #e888f8 100%) 1;
+float roundedRectangleDistance(vec2 uv)
+{
+ vec2 halfSize = resolution * 0.5;
+ vec2 position = (uv - 0.5) * resolution;
+ vec2 cornerDistance = abs(position) - halfSize + vec2(cornerRadius);
+
+ return length(max(cornerDistance, 0.0))
+ + min(max(cornerDistance.x, cornerDistance.y), 0.0)
+ - cornerRadius;
+}
+
+vec3 borderColor(float position)
+{
+ const vec3 firstColor = vec3(0.0, 0.2118, 0.9608);
+ const vec3 secondColor = vec3(0.3059, 0.4588, 1.0);
+ const vec3 thirdColor = vec3(0.5412, 0.2549, 1.0);
+ const vec3 fourthColor = vec3(0.9098, 0.5333, 0.9725);
+
+ if (position <= 0.34)
+ return mix(firstColor, secondColor, position / 0.34);
+ if (position <= 0.71)
+ return mix(secondColor, thirdColor, (position - 0.34) / 0.37);
+ return mix(thirdColor, fourthColor, (position - 0.71) / 0.29);
+}
+
+void main()
+{
+ float distance = roundedRectangleDistance(qt_TexCoord0);
+ float outerCoverage = 1.0 - smoothstep(-0.5, 0.5, distance);
+ float innerCoverage = smoothstep(-borderWidth - 0.5,
+ -borderWidth + 0.5, distance);
+ float alpha = outerCoverage * innerCoverage;
+ vec3 color = borderColor(1.0 - qt_TexCoord0.x);
+
+ gl_FragColor = vec4(color * alpha, alpha) * qt_Opacity;
+}
diff --git a/ukui-search-qml/src/qml.qrc b/ukui-search-qml/src/qml.qrc
index 64c17d2133c967ef38c1be3ebae0478398352b6a..b242140579cb09c82db5a0b4038b31e958a72aed 100644
--- a/ukui-search-qml/src/qml.qrc
+++ b/ukui-search-qml/src/qml.qrc
@@ -17,5 +17,8 @@
org.ukui.search/ui/ResultItem.qml
org.ukui.search/ui/ResultSortFilterButton.qml
org.ukui.search/ui/InstantResultCard.qml
+
+ org.ukui.search/ui/QuickSearchCardBackground.frag
+ org.ukui.search/ui/QuickSearchCardBorder.frag