弹窗高度随内容自动调整:数据到达后变高不再露出滚动条
弹窗打开时按平铺布局测一次高度,之后公网 IP、归属地等数据陆续到达,内容变高(实测 860 → 911pt)而窗口不变,底部露出一小截滚动条。 - PageScroll 在弹窗里用 onGeometryChange 记下滚动内容高度与可视高度,差值通过 reportPopoverOverflow 环境值交给面板 - StatusPanel.adjustHeight(by:):按差值调整窗口高度、顶边不动,上限为菜单栏下方到屏幕底边的可用高度,屏幕放不下时才保留滚动 - 打开时的平铺测量保留作为起始高度;展开 / 收起区块、数据刷新都会自动校正
This commit is contained in:
1 parent
84542374bc
commit
165963de84
6 files changed
+66
-12
No files matched your search
@@ -7,6 +7,10 @@
|
||||
|
||||
### 2026-09-16
|
||||
|
||||
#### 修复
|
||||
|
||||
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
|
||||
|
||||
#### 样式
|
||||
|
||||
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
|
||||
|
||||
@@ -16,19 +16,24 @@ final class StatusPanel: NSPanel {
|
||||
private var lastDismissal = Date.distantPast
|
||||
|
||||
/// 面板隐藏时不保留 SwiftUI 视图,避免后台随数据刷新重绘
|
||||
private let makeContent: () -> NSView
|
||||
private var makeContent: (() -> NSView)?
|
||||
/// 平铺布局(不含滚动容器)的视图,只用来测量内容的自然高度
|
||||
private let makeMeasuringContent: () -> NSView
|
||||
private let width: CGFloat
|
||||
|
||||
init<Content: View, Measuring: View>(width: CGFloat, content: @escaping () -> Content, measuring: @escaping () -> Measuring) {
|
||||
self.width = width
|
||||
makeContent = { NSHostingView(rootView: content()) }
|
||||
makeMeasuringContent = { NSHostingView(rootView: measuring()) }
|
||||
super.init(contentRect: NSRect(x: 0, y: 0, width: width, height: DS.Size.panelMinHeight),
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: true)
|
||||
// 打开时按平铺布局测出的高度只是起点:真实内容会随数据刷新变高变矮,由滚动区域上报差值再校正
|
||||
makeContent = { [weak self] in
|
||||
NSHostingView(rootView: content().environment(\.reportPopoverOverflow) { overflow in
|
||||
self?.adjustHeight(by: overflow)
|
||||
})
|
||||
}
|
||||
isFloatingPanel = true
|
||||
level = .statusBar
|
||||
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient, .ignoresCycle]
|
||||
@@ -56,7 +61,7 @@ final class StatusPanel: NSPanel {
|
||||
self.anchor = anchor
|
||||
anchorScreen = screen
|
||||
|
||||
let content = makeContent()
|
||||
guard let content = makeContent?() else { return }
|
||||
setFrame(targetFrame(), display: false)
|
||||
content.frame = NSRect(origin: .zero, size: frame.size)
|
||||
contentView = PanelContainer.make(containing: content, cornerRadius: DS.Radius.xl)
|
||||
@@ -81,6 +86,15 @@ final class StatusPanel: NSPanel {
|
||||
invalidateShadow()
|
||||
}
|
||||
|
||||
/// 滚动内容比可视区域高出(或矮出)一截时调整窗口高度,顶边不动;屏幕放不下时停在最高处,由滚动条兜底
|
||||
func adjustHeight(by overflow: CGFloat) {
|
||||
guard contentView != nil, abs(overflow) > 1 else { return }
|
||||
let height = min(max(frame.height + overflow, DS.Size.panelMinHeight), availableHeight())
|
||||
guard abs(height - frame.height) > 1 else { return }
|
||||
setFrame(NSRect(x: frame.minX, y: frame.maxY - height, width: width, height: height), display: true)
|
||||
invalidateShadow()
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
guard isVisible, !isPinned else { return }
|
||||
lastDismissal = Date()
|
||||
@@ -116,16 +130,22 @@ final class StatusPanel: NSPanel {
|
||||
let visible = (anchorScreen ?? NSScreen.main)?.visibleFrame ?? .zero
|
||||
let margin = DS.Space.s2
|
||||
let top = min(anchor.minY - DS.Size.panelGap, visible.maxY)
|
||||
let available = max(DS.Size.panelMinHeight, top - visible.minY - margin)
|
||||
|
||||
let natural = makeMeasuringContent().fittingSize.height
|
||||
let height = min(max(natural, DS.Size.panelMinHeight), available)
|
||||
let height = min(max(natural, DS.Size.panelMinHeight), availableHeight())
|
||||
|
||||
var x = anchor.midX - width / 2
|
||||
x = min(max(x, visible.minX + margin), visible.maxX - width - margin)
|
||||
return NSRect(x: x, y: top - height, width: width, height: height)
|
||||
}
|
||||
|
||||
/// 菜单栏下方到屏幕底边(留一点边距)能放下的最大高度
|
||||
private func availableHeight() -> CGFloat {
|
||||
let visible = (anchorScreen ?? NSScreen.main)?.visibleFrame ?? .zero
|
||||
let top = min(anchor.minY - DS.Size.panelGap, visible.maxY)
|
||||
return max(DS.Size.panelMinHeight, top - visible.minY - DS.Space.s2)
|
||||
}
|
||||
|
||||
// MARK: 事件
|
||||
|
||||
private func installMonitors() {
|
||||
|
||||
@@ -9,6 +9,8 @@ extension EnvironmentValues {
|
||||
@Entry var isDetailPage = false
|
||||
/// 菜单栏弹窗:区块去掉卡片底色,用细分隔线隔开,排得更紧凑
|
||||
@Entry var isPopover = false
|
||||
/// 菜单栏弹窗:滚动内容的实际高度比可视区域高出(负数为矮出)多少,交给面板调整窗口高度
|
||||
@Entry var reportPopoverOverflow: (@MainActor @Sendable (CGFloat) -> Void)? = nil
|
||||
}
|
||||
|
||||
// MARK: - 卡片
|
||||
@@ -851,7 +853,10 @@ struct DSSlider: View {
|
||||
struct PageScroll<Content: View>: View {
|
||||
@Environment(\.isSnapshot) private var isSnapshot
|
||||
@Environment(\.isPopover) private var isPopover
|
||||
@Environment(\.reportPopoverOverflow) private var reportOverflow
|
||||
@ViewBuilder var content: Content
|
||||
@State private var contentHeight: CGFloat = 0
|
||||
@State private var viewportHeight: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
// 弹窗里区块之间靠分隔线隔开,不再额外留间距
|
||||
@@ -863,10 +868,27 @@ struct PageScroll<Content: View>: View {
|
||||
stack
|
||||
} else {
|
||||
// 内容放得下时不回弹,避免点击时整页轻微抖动
|
||||
ScrollView { stack.overlayScrollers() }
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
ScrollView {
|
||||
stack
|
||||
.overlayScrollers()
|
||||
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height in
|
||||
contentHeight = height
|
||||
report()
|
||||
}
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height in
|
||||
viewportHeight = height
|
||||
report()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 弹窗里内容高度一变(数据刷新、展开收起),就让面板把窗口调到正好放下;屏幕放不下时才出现滚动
|
||||
private func report() {
|
||||
guard let reportOverflow, contentHeight > 0, viewportHeight > 0 else { return }
|
||||
reportOverflow(contentHeight - viewportHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 徽章 / 标签
|
||||
|
||||
@@ -59,10 +59,14 @@ system language by default; switch it in Settings → General → Language.
|
||||
<!-- changelog:start -->
|
||||
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
|
||||
|
||||
Latest release **0.3.2** (2026-09-16) · **6** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
|
||||
Latest release **0.3.2** (2026-09-16) · **7** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
|
||||
|
||||
<details open>
|
||||
<summary><b>2026-09-16</b> · Unreleased · 5 style · 1 added</summary>
|
||||
<summary><b>2026-09-16</b> · Unreleased · 1 fixed · 5 style · 1 added</summary>
|
||||
|
||||
**Fixed**
|
||||
|
||||
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
|
||||
|
||||
**Style**
|
||||
|
||||
|
||||
+6
-2
@@ -55,10 +55,14 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
|
||||
<!-- changelog:start -->
|
||||
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
|
||||
|
||||
最新版本 **0.3.2**(2026-09-16) · 开发中 **6** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
|
||||
最新版本 **0.3.2**(2026-09-16) · 开发中 **7** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
|
||||
|
||||
<details open>
|
||||
<summary><b>2026-09-16</b> · 未发布 · 样式 5 · 新增 1</summary>
|
||||
<summary><b>2026-09-16</b> · 未发布 · 修复 1 · 样式 5 · 新增 1</summary>
|
||||
|
||||
**修复**
|
||||
|
||||
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
|
||||
|
||||
**样式**
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ settings:
|
||||
SWIFT_VERSION: "6.0"
|
||||
MARKETING_VERSION: "0.3.2"
|
||||
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
|
||||
CURRENT_PROJECT_VERSION: "0078"
|
||||
CURRENT_PROJECT_VERSION: "0081"
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
|
||||
CODE_SIGN_STYLE: Manual
|
||||
|
||||
Reference in new issue
Block a user