mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
On the iPhone Duo inner screen in portrait, the workspace header, the left sidebar's first row and the explorer tab rail now use the top band beside the status region. The status region is published as the window's top-right obstruction, the same slot desktop window controls use, so the row owning that corner pads past it. Content gains 56pt. Refs #7924 Co-authored-by: Elsa Granger <zeyugao@outlook.com>
141 lines
4.5 KiB
Swift
141 lines
4.5 KiB
Swift
import ExpoModulesCore
|
|
import UIKit
|
|
|
|
public final class PaseoWindowLayoutModule: Module {
|
|
public func definition() -> ModuleDefinition {
|
|
Name("PaseoWindowLayout")
|
|
View(WindowLayoutView.self) {
|
|
Events("onGeometryChange")
|
|
}
|
|
}
|
|
}
|
|
|
|
final class WindowLayoutView: ExpoView {
|
|
let onGeometryChange = EventDispatcher()
|
|
private var updateScheduled = false
|
|
private var previousPayload: [String: Any]?
|
|
private var divisions: [CGRect] = []
|
|
private var barGuide: UILayoutGuide?
|
|
private var barEdge = "none"
|
|
private var bottomBarGuide: UILayoutGuide?
|
|
private var topBarGuide: UILayoutGuide?
|
|
private var occlusions: [CGRect] = []
|
|
private var measuredBounds = CGRect.zero
|
|
|
|
required init(appContext: AppContext? = nil) {
|
|
super.init(appContext: appContext)
|
|
isUserInteractionEnabled = false
|
|
#if compiler(>=6.4)
|
|
if #available(iOS 27.1, *) {
|
|
registerForTraitChanges(UITraitCollection.systemTraitsAffectingVerticalBarEdge) {
|
|
(view: WindowLayoutView, _) in
|
|
view.setNeedsUpdateConstraints()
|
|
view.scheduleUpdate()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
previousPayload = nil
|
|
bottomBarGuide = nil
|
|
topBarGuide = nil
|
|
setNeedsUpdateConstraints()
|
|
scheduleUpdate()
|
|
}
|
|
|
|
override func safeAreaInsetsDidChange() {
|
|
super.safeAreaInsetsDidChange()
|
|
setNeedsUpdateConstraints()
|
|
scheduleUpdate()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
if bounds != measuredBounds {
|
|
measuredBounds = bounds
|
|
setNeedsUpdateConstraints()
|
|
}
|
|
scheduleUpdate()
|
|
}
|
|
|
|
override func updateConstraints() {
|
|
#if compiler(>=6.4)
|
|
if #available(iOS 27.1, *) {
|
|
// UIKit tracks these reads and invalidates constraints when regions change.
|
|
divisions = reservedRegions(kind: .division).compactMap { $0.isActive ? $0.frame : nil }
|
|
// Frames already include the system's interactive margins.
|
|
occlusions = reservedRegions(kind: .occlusion).compactMap { $0.isActive ? $0.frame : nil }
|
|
let edge: String
|
|
switch traitCollection.verticalBarEdge {
|
|
case .leading: edge = "leading"
|
|
case .trailing: edge = "trailing"
|
|
default: edge = "none"
|
|
}
|
|
if bottomBarGuide == nil {
|
|
// Where a bottom bar rests: above the home indicator, refined to the
|
|
// screen's corners rather than the rectangular safe area.
|
|
bottomBarGuide = layoutGuide(for: .bar(onEdge: UIRectEdge.bottom, extent: 44))
|
|
}
|
|
if topBarGuide == nil {
|
|
// Where a top bar rests. Beside a partial status region it can start above the
|
|
// rectangular safe area; content still keeps out of the occlusions.
|
|
topBarGuide = layoutGuide(for: .bar(onEdge: UIRectEdge.top, extent: 44))
|
|
}
|
|
if edge != barEdge {
|
|
barEdge = edge
|
|
barGuide = edge == "none" ? nil : layoutGuide(
|
|
for: .bar(onEdge: edge == "trailing" ? .right : .left, extent: 44)
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
super.updateConstraints()
|
|
scheduleUpdate()
|
|
}
|
|
|
|
private func scheduleUpdate() {
|
|
guard !updateScheduled else { return }
|
|
updateScheduled = true
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { return }
|
|
self.updateScheduled = false
|
|
self.publish()
|
|
}
|
|
}
|
|
|
|
private func publish() {
|
|
guard window != nil, bounds.width > 0, bounds.height > 0 else { return }
|
|
let bar = barGuide?.layoutFrame
|
|
var barPayload: Any = NSNull()
|
|
if let bar, bar.width > 0, bar.height > 0 { barPayload = rect(bar) }
|
|
// Before UIKit resolves the guide its frame has no height: publish null (the
|
|
// system inset applies) rather than a false zero clearance.
|
|
var bottomClearance: Any = NSNull()
|
|
if let bottomBar = bottomBarGuide?.layoutFrame, bottomBar.height > 0 {
|
|
bottomClearance = max(0, bounds.maxY - bottomBar.maxY)
|
|
}
|
|
var topBar: Any = NSNull()
|
|
if let frame = topBarGuide?.layoutFrame, frame.width > 0, frame.height > 0 {
|
|
topBar = rect(frame)
|
|
}
|
|
let payload: [String: Any] = [
|
|
"bounds": rect(bounds),
|
|
"verticalBarEdge": barEdge,
|
|
"verticalBar": barPayload,
|
|
"bottomClearance": bottomClearance,
|
|
"topBar": topBar,
|
|
"occlusions": occlusions.map(rect),
|
|
"divisions": divisions.map(rect)
|
|
]
|
|
guard !NSDictionary(dictionary: payload).isEqual(to: previousPayload ?? [:]) else { return }
|
|
previousPayload = payload
|
|
onGeometryChange(payload)
|
|
}
|
|
|
|
private func rect(_ frame: CGRect) -> [String: CGFloat] {
|
|
["x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height]
|
|
}
|
|
}
|