mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
* fix(app): rest the iPhone composer at the screen's bottom clearance The composer sat 50pt above the bottom edge on every iPhone: the dock padded the rectangular home-indicator safe area (34pt) and the composer stacked another 16pt under it. On iPhone Duo that safe area is also taller than needed; iOS 27.1's bottom-bar layout guide, which follows the screen's corners, rests 26pt from the edge. The native window-layout probe now publishes that clearance, and WindowLayoutProvider overrides the safe-area context's bottom with it, so every useSafeAreaInsets() consumer reads one bottom value and null falls back to the system inset. The docked composer applies max(bottom, 16) once and keeps 16pt above the keyboard through the translate view's transform; the gap is not animated as padding because layout updates through the keyboard transition are expensive on Android Fabric. Co-authored-by: Elsa Granger <zeyugao@outlook.com> * fix(app): keep the Import session gap under the wide new workspace composer The composer's own bottom spacing used to separate it from the Import session pill on the wide form. With that spacing moved to the dock, the centered slot padded below the pill instead, so the pill touched the composer. The pill now owns its gap above. Co-authored-by: Elsa Granger <zeyugao@outlook.com> --------- Co-authored-by: Elsa Granger <zeyugao@outlook.com>
125 lines
3.8 KiB
Swift
125 lines
3.8 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 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
|
|
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 }
|
|
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 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)
|
|
}
|
|
let payload: [String: Any] = [
|
|
"bounds": rect(bounds),
|
|
"verticalBarEdge": barEdge,
|
|
"verticalBar": barPayload,
|
|
"bottomClearance": bottomClearance,
|
|
"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]
|
|
}
|
|
}
|