Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/design/P0_UI_Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@
| 控件间距 | 14 pt | 18 px |
| 圆角 | 20 pt,暂沿用 | 20 px |

栏左右边缘与实际 Finder 窗口对齐。弹出斜杠菜单、问题或结果时,窗口向上扩展,输入栏底边不漂移。普通窗口空间不足时先上移 Finder,必要时缩短;仅恢复未被用户再次修改的布局。全屏采用窗口内侧底边
底栏是绝对定位的:底边永远钉在屏幕底部(Dock 之上),不随访达窗口的高度、位置或是否贴底而改变;横向范围与访达窗口左右对齐,超出屏幕时收进可见区域。以底栏为核心:斜杠菜单、问题与结果气泡一律向上展开,面板只向上长高,底栏在屏幕上的位置唯一确定。Finder 窗口本身从不被移动或缩放。收起动画向屏幕下方滑出整条底栏。全屏同样贴屏幕底部

拖动或缩放期间隐藏,鼠标释放且位置稳定 140 ms 后显示。仅识别 Finder 文件浏览窗口;空格打开的 Quick Look 不替换关联窗口。关闭、最小化或切换其他应用隐藏栏,任务继续独立运行。

## 材质与操作

磨砂为默认值。macOS 使用 `NSVisualEffectView`,macOS 26+ 可选 `NSGlassEffectView`;减少透明度时降级。浏览器仅提供 CSS 磨砂预览。原生外观状态以 SQLite 为准,跨窗口事件同步;旧 localStorage 偏好只导入一次。

`+` 选择文件,纸飞机提交,齿轮打开全部设置。输入 `/` 列出 `/model`、`/thinking`、`/speed`、`/settings`;方向键选择,Enter 确认,Esc 收起。模型和参数只影响新任务。未连接或服务未声明的能力不伪造可用选项。
选中即上下文:访达选区自动跟随,不在底栏上方重复展示文件名。`+` 按钮用于手动选文件;有文件时按钮右上角显示数量角标,手动选择状态下 `+` 变为 `×`,点击清除并回到跟随访达选区。纸飞机提交,齿轮打开全部设置。输入 `/` 列出 `/model`、`/thinking`、`/speed`、`/settings`;方向键选择,Enter 确认,Esc 收起。模型和参数只影响新任务。未连接或服务未声明的能力不伪造可用选项。

快捷键通过实际按键录入,录入时临时停用已有全局快捷键,结束或失焦后恢复。中文输入法确认不触发提交,发送与连接操作防止连点,并显示加载、失败与保存结果。

Expand Down
Binary file modified docs/previews/control-bar-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/previews/control-bar-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/previews/settings-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/previews/settings-general.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/qa/P0_Acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,20 @@ GitHub 登录新增令牌方式:OAuth 应用尚未注册时,界面提供「

- `pnpm build` 通过;`pnpm test` Playwright 21 项通过(新增文件浮层可见性与运行态两项)。
- `swift test` 5 项通过;两份 `cargo clippy -D warnings` 通过。

## 底栏绝对定位与选区交互(本轮)

上一轮把“文件选中提示”做成可见浮层,方向做反了:选中就是选中,用户不需要
在底栏上方再看一遍文件名。本轮改为——访达选区零展示、自动跟随;`+` 按钮带
数量角标,手动选文件时 `+` 变 `×`,点击清除并回到跟随访达。文件浮层只剩瞬时
提示(错误/结果)使用。

底栏定位从“贴在访达窗口下沿”改为绝对规则:底边永远钉在屏幕底部(Dock 之上),
横向与访达窗口对齐、超屏收进可见区域;气泡一律向上展开,面板只向上长高。
此前访达窗口较高时底栏被顶到屏幕中部、看起来“到处乱跑”的问题从根上消除。
收起动画相应改为向屏幕下方滑出(`tuckDistance` 取负)。

- `swift test` 6 项通过(屏幕底部锚定、窗口高度无关、气泡向上不动栏、超屏收边、全屏同规则、拖动回落)。
- `pnpm build` 通过;`pnpm test` Playwright 21 项通过(角标/变叉清除、不再渲染文件列表)。
- 真机复验:解锁后唤起底栏,确认贴屏幕底部、访达窗口任意移动/缩放底栏不动、
选文件只有角标无上方提示。
27 changes: 10 additions & 17 deletions src-tauri/native/Sources/FleqiGlass/AttachmentGeometry.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Foundation

/// 底栏的定位规则:底栏永远贴在 Finder 窗口下边缘下方,与窗口左右对齐。
/// 底栏的定位规则:底栏是绝对定位的——底边永远钉在屏幕底部(Dock 之上),
/// 不随访达窗口的高度、位置或是否贴底而改变;横向范围与访达窗口对齐。
/// 面板只改变高度,底栏本体的位置由 `barOffset`(底栏顶边到面板顶边的距离)决定,
/// 因此气泡向上或向下展开都不会让底栏移动。这里从不修改 Finder 窗口的框架。
/// 气泡一律向上展开。这里从不修改 Finder 窗口的框架。
struct AttachmentGeometry {
static let gap: CGFloat = 8
struct Placement {
var panel: CGRect
/// 底栏下方到屏幕可见区域底部的空间,气泡向下展开时可用
/// 底栏下方没有可用空间:气泡永远向上展开,保留字段供界面判断
var spaceBelow: CGFloat
/// 底栏上方到屏幕可见区域顶部的空间,气泡向上展开时可用。
var spaceAbove: CGFloat
Expand All @@ -22,20 +22,13 @@ struct AttachmentGeometry {
) -> Placement? {
guard host.width > 200, host.height > 100 else { return nil }
let height = max(barHeight, contentHeight)
// 全屏窗口没有窗口外的空间:把底栏放在窗口内侧底部,气泡只能向上展开。
let barTop = fullscreen ? host.minY + gap + barHeight : host.minY - gap
let spaceBelow = max(0, barTop - barHeight - visible.minY)
let width = min(host.width, visible.width)
let x = min(max(host.minX, visible.minX), visible.maxX - width)
// AppKit 中 y 轴向上:面板底边贴屏幕可见区域底部,顶边随内容增高。
let panel = CGRect(x: x, y: visible.minY, width: width, height: height)
let barTop = panel.maxY - barOffset
let spaceAbove = max(0, visible.maxY - barTop)
// barOffset 是面板顶边到底栏顶边的距离:面板只向上(气泡在上)或向下
// (气泡在下)长高,底栏本身不动。AppKit 中面板顶边是 maxY。
var topEdge = barTop + barOffset
let lowest = visible.minY + height
topEdge = min(max(topEdge, lowest), visible.maxY)
return Placement(
panel: CGRect(x: host.minX, y: topEdge - height, width: host.width, height: height),
spaceBelow: spaceBelow,
spaceAbove: spaceAbove
)
return Placement(panel: panel, spaceBelow: 0, spaceAbove: spaceAbove)
}
}

Expand Down
5 changes: 3 additions & 2 deletions src-tauri/native/Sources/FleqiGlass/HostBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,9 @@ public func fleqiHostCommand(_ pointer: UnsafePointer<CChar>?) -> UnsafeMutableP
if let value=request["barOffset"] as? Double,value.isFinite {host.barOffset=CGFloat(min(1600,max(0,value)))}
if let panel=host.panel {
let motion=NativeWindowMotion.forWindow(panel)
// 收起时只需上移“底栏顶边偏移 + 底栏高度”,气泡不会露在窗口外。
motion.tuckDistance = host.barOffset + host.barHeight + 2
// 收起时向屏幕下方滑出“面板高度 - 底栏顶边偏移 + 2”,把底栏整条推出屏幕底边;
// 面板底边本就贴屏幕底部,气泡在上方不会露出来。
motion.tuckDistance = -(host.height - host.barOffset) - 2
}
host.layout()
return jsonResult(["ok":true,"spaceBelow":Double(host.spaceBelow),"spaceAbove":Double(host.spaceAbove)])
Expand Down
6 changes: 3 additions & 3 deletions src-tauri/native/Sources/FleqiGlass/WindowMotion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ final class NativeWindowMotion {
let target=fullFrame ?? window.frame
let anchor=origin ?? CGPoint(x:target.midX,y:target.maxY)
let contracted=compact ? target.offsetBy(dx:0,dy:8):FleqiMotion.contracted(target,around:anchor)
// 仅底栏(attachment)使用“从宿主窗口后方滑入/滑出 + 高斯模糊”:
// 收起距离按底栏自身高度计算,气泡在上或在下都不会露出多余部分
let tuck=attachment ? target.offsetBy(dx:0,dy:tuckDistance>0 ? tuckDistance : target.height):contracted
// 仅底栏(attachment)使用“滑入/滑出 + 高斯模糊”:底栏钉在屏幕底部,
// 收起距离为负(向屏幕下方滑出),出现时从底边下方滑回原位
let tuck=attachment ? target.offsetBy(dx:0,dy:tuckDistance != 0 ? tuckDistance : target.height):contracted
let startFrame=tuck
let time=FleqiMotion.reduced ? 0:(compact ? FleqiMotion.feedbackDuration:FleqiMotion.windowDuration)
if visible && !window.isVisible {
Expand Down
64 changes: 35 additions & 29 deletions src-tauri/native/Tests/FleqiGlassTests/AttachmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,55 @@ import XCTest
final class AttachmentTests: XCTestCase {
let screen = CGRect(x: 0, y: 24, width: 1440, height: 850)

func testBarStaysAlignedUnderTheWindowAndNeverMovesTheWindow() throws {
let host = CGRect(x: 140, y: 400, width: 1000, height: 600)
let bar = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
XCTAssertEqual(bar.panel.minX, host.minX)
XCTAssertEqual(bar.panel.width, host.width)
// 底栏顶边在窗口下边缘下方 8pt,且面板整体位于窗口之外。
XCTAssertEqual(host.minY - bar.panel.maxY, 8)
// 气泡向下展开:面板向屏幕下方长高,顶边(底栏位置)不动。
let popup = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 320, barOffset: 0, fullscreen: false))
XCTAssertEqual(popup.panel.maxY, bar.panel.maxY)
XCTAssertLessThan(popup.panel.minY, bar.panel.minY)
func testBarIsPinnedToTheScreenBottomRegardlessOfWindow() throws {
// 窗口高、窗口矮、窗口贴底:底栏面板必须完全一致地钉在屏幕底部。
let tall = CGRect(x: 140, y: 40, width: 1000, height: 800)
let short = CGRect(x: 140, y: 700, width: 1000, height: 200)
let tight = CGRect(x: 140, y: 24, width: 1000, height: 120)
for host in [tall, short, tight] {
let placement = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
XCTAssertEqual(placement.panel.minY, screen.minY, "面板底边必须贴屏幕可见区域底部")
XCTAssertEqual(placement.panel.maxY, screen.minY + 56, "底栏顶边位置必须唯一确定")
XCTAssertEqual(placement.panel.minX, host.minX)
XCTAssertEqual(placement.panel.width, host.width)
XCTAssertEqual(placement.spaceBelow, 0)
}
// Finder 窗口本身从不被移动或缩放:以上只读几何,不产生任何窗口操作。
}

func testBubbleAboveKeepsTheBarInPlace() throws {
let host = CGRect(x: 0, y: 120, width: 1000, height: 600)
let bar = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
// 气泡向上展开:面板向上长高,底栏顶边仍然固定在窗口下方 8pt
// 气泡向上展开:面板向上长高,底栏顶边仍然钉在同一个位置
let above = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 320, barOffset: 264, fullscreen: false))
XCTAssertEqual(above.panel.maxY, bar.panel.maxY + 264)
XCTAssertEqual(above.panel.minY, bar.panel.minY)
XCTAssertEqual(above.panel.minY + 56, bar.panel.minY + 56)
// barOffset 是面板顶边到底栏顶边的距离:320 - 264 = 56,与无气泡时一致。
XCTAssertEqual(above.panel.maxY - 264, bar.panel.maxY, accuracy: 0.001)
}

func testSpaceReportingForBubbleDirection() throws {
let roomy = CGRect(x: 0, y: 700, width: 1000, height: 400)
let roomyPlacement = try XCTUnwrap(AttachmentGeometry.layout(host: roomy, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
XCTAssertGreaterThan(roomyPlacement.spaceBelow, 200)
XCTAssertGreaterThan(roomyPlacement.spaceAbove, 150)
// 窗口贴着屏幕底部时,下方没有空间,气泡必须向上。
let tight = CGRect(x: 0, y: 24, width: 1000, height: 400)
let tightPlacement = try XCTUnwrap(AttachmentGeometry.layout(host: tight, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
XCTAssertEqual(tightPlacement.spaceBelow, 0)
XCTAssertGreaterThan(tightPlacement.spaceAbove, 200)
// 面板始终留在屏幕可见区域内,Finder 窗口本身不被改动。
XCTAssertGreaterThanOrEqual(tightPlacement.panel.minY, screen.minY)
XCTAssertLessThanOrEqual(tightPlacement.panel.maxY, screen.maxY)
let host = CGRect(x: 0, y: 700, width: 1000, height: 400)
let placement = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
// 底栏下方永远没有空间(贴着屏幕底),气泡只能向上。
XCTAssertEqual(placement.spaceBelow, 0)
XCTAssertEqual(placement.spaceAbove, screen.maxY - (screen.minY + 56), accuracy: 0.001)
XCTAssertGreaterThan(placement.spaceAbove, 700)
}

func testPanelClampedToScreenWidth() throws {
// 窗口比屏幕还宽、或偏出屏幕左右边缘时,面板收进可见区域。
let wide = CGRect(x: -200, y: 400, width: 3000, height: 600)
let placement = try XCTUnwrap(AttachmentGeometry.layout(host: wide, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: false))
XCTAssertGreaterThanOrEqual(placement.panel.minX, screen.minX)
XCTAssertLessThanOrEqual(placement.panel.maxX, screen.maxX)
XCTAssertEqual(placement.panel.width, screen.width)
}

func testFullscreenKeepsTheBarInsideTheWindow() throws {
func testFullscreenUsesTheSameScreenBottomAnchor() throws {
let host = CGRect(x: 0, y: 24, width: 1440, height: 850)
let placement = try XCTUnwrap(AttachmentGeometry.layout(host: host, visible: screen, barHeight: 56, contentHeight: 56, barOffset: 0, fullscreen: true))
XCTAssertEqual(placement.panel.minY, host.minY + 8)
XCTAssertLessThan(placement.spaceBelow, 20)
XCTAssertEqual(placement.panel.minY, screen.minY)
XCTAssertEqual(placement.spaceBelow, 0)
}

func testDragStaysHiddenUntilReleaseAndSettle() {
Expand Down
Loading
Loading