fix an issue with horizontal window margins not respected

This commit is contained in:
2026-07-12 15:23:22 +02:00
parent 6de06ac8e1
commit 19a8f09e5a
5 changed files with 112 additions and 52 deletions
@@ -17,44 +17,40 @@ enum PHThemeWindowAnchor: String, Decodable {
extension PHThemeWindowAnchor {
/// Compute the top-left origin (in screen coordinates) of a window of `size`
/// placed against this anchor within `screen`, offset by `margin` along the
/// anchored edges.
/// placed against this anchor within `rect`.
///
/// Edges the anchor does not reference are ignored, so margins only affect
/// the relevant sides.
func origin(in screen: CGRect, size: CGSize, margin: PHThemeWindowMargin) -> CGPoint {
let top = margin.top ?? 0
let bottom = margin.bottom ?? 0
let leading = margin.leading ?? 0
let trailing = margin.trailing ?? 0
// Default: centered on both axes.
/// `rect` is the content rectangle the window is placed within typically the
/// screen inset by the window's margin (see `PHThemeWindowMargin.inset(of:)`).
/// Single-edge anchors pin to that edge and center along the opposite axis;
/// corner anchors pin to two edges; `.center` centers on both axes.
func origin(in rect: CGRect, size: CGSize) -> CGPoint {
// Default: centered on both axes within the rect.
var point = CGPoint(
x: screen.midX - size.width / 2,
y: screen.midY - size.height / 2
x: rect.midX - size.width / 2,
y: rect.midY - size.height / 2
)
switch self {
case .top:
point.y = screen.maxY - CGFloat(top) - size.height
point.y = rect.maxY - size.height
case .bottom:
point.y = screen.minY + CGFloat(bottom)
point.y = rect.minY
case .leading:
point.x = screen.minX + CGFloat(leading)
point.x = rect.minX
case .trailing:
point.x = screen.maxX - CGFloat(trailing) - size.width
point.x = rect.maxX - size.width
case .topLeading:
point.x = screen.minX + CGFloat(leading)
point.y = screen.maxY - CGFloat(top) - size.height
point.x = rect.minX
point.y = rect.maxY - size.height
case .topTrailing:
point.x = screen.maxX - CGFloat(trailing) - size.width
point.y = screen.maxY - CGFloat(top) - size.height
point.x = rect.maxX - size.width
point.y = rect.maxY - size.height
case .bottomLeading:
point.x = screen.minX + CGFloat(leading)
point.y = screen.minY + CGFloat(bottom)
point.x = rect.minX
point.y = rect.minY
case .bottomTrailing:
point.x = screen.maxX - CGFloat(trailing) - size.width
point.y = screen.minY + CGFloat(bottom)
point.x = rect.maxX - size.width
point.y = rect.minY
case .center:
break
}
@@ -1,9 +1,12 @@
import CoreGraphics
import Foundation
/// Insets (in points) from the screen edges implied by a window's anchor.
/// Insets (in points) from the screen edges, defining the content rectangle a
/// window is sized and placed within.
///
/// Only the edges referenced by the anchor are consumed; the rest are ignored,
/// so a margin only affects the relevant sides of the placement.
/// Margins compose with `width`/`height`: a `width = "100%"` window spans the
/// space *between* the leading/trailing margins rather than the full screen.
/// Unspecified edges are left untouched (treated as 0).
struct PHThemeWindowMargin: Decodable, Equatable {
let top: Double?
let bottom: Double?
@@ -21,4 +24,20 @@ struct PHThemeWindowMargin: Decodable, Equatable {
self.leading = leading
self.trailing = trailing
}
/// Returns `rect` inset by the specified edges; unspecified edges are left
/// untouched (treated as 0). Uses AppKit's coordinate system (origin at the
/// bottom-left, y increasing upward): `top` reduces the max-Y edge.
func inset(of rect: CGRect) -> CGRect {
let l = leading ?? 0
let r = trailing ?? 0
let t = top ?? 0
let b = bottom ?? 0
return CGRect(
x: rect.minX + l,
y: rect.minY + b,
width: rect.width - l - r,
height: rect.height - t - b
)
}
}
+1 -4
View File
@@ -7,16 +7,13 @@ name = "default"
anchor = "top"
height = 30
width = "100%"
shadow = false
blur = 0.0
margin = { leading = 30, top = 30, trailing = 30, bottom = 30 }
[[window]]
name = "bottom"
anchor = "bottom"
height = 30
width = "100%"
shadow = false
blur = 0.0
[[text]]
name = "default"
+17 -8
View File
@@ -52,13 +52,14 @@ extension BarWindow {
let screenFrame = controller.screen.frame
let themeWindow = controller.window
let size = CGSize(
width: (themeWindow.width ?? .percentage(1.0)).resolve(against: screenFrame.width),
height: CGFloat(themeWindow.height ?? 30)
)
// Absolute origin wins over anchor/margin.
// Absolute origin bypasses the anchor/margin system entirely: width
// resolves against the full screen and the window sits at the exact
// coordinates given.
if let origin = themeWindow.origin {
let size = CGSize(
width: (themeWindow.width ?? .percentage(1.0)).resolve(against: screenFrame.width),
height: CGFloat(themeWindow.height ?? 30)
)
return NSRect(
x: CGFloat(origin.x),
y: CGFloat(origin.y),
@@ -67,10 +68,18 @@ extension BarWindow {
)
}
let anchor = themeWindow.anchor ?? .top
// Margins define a content rectangle within the screen: the window is
// sized against it (so `width = "100%"` spans only between the margins,
// not the full screen) and placed by the anchor inside it.
let margin = themeWindow.margin ?? .init()
let contentRect = margin.inset(of: screenFrame)
let size = CGSize(
width: (themeWindow.width ?? .percentage(1.0)).resolve(against: contentRect.width),
height: CGFloat(themeWindow.height ?? 30)
)
let anchor = themeWindow.anchor ?? .top
return NSRect(
origin: anchor.origin(in: screenFrame, size: size, margin: margin),
origin: anchor.origin(in: contentRect, size: size),
size: size
)
}