import AppKit import CoreGraphics /// The card window's fixed geometry — **derived from font metrics, never written down in points** /// (05-card-window.md ▸ Composition: the attributes sidebar has a "fixed narrow width derived from /// font metrics (full relative scaling, 10-accessibility.md)"; 10 ▸ Text: "relative text styles /// everywhere, no fixed point sizes … metrics derive from font metrics, so layout survives the /// largest system text sizes"). /// /// ### The derivation, named once /// /// Every width here is **a character count in the body font**, and the arithmetic behind it is one /// expression used three times: /// /// ``` /// width = characters × averageCharacterAdvance × pointSize + 2 × gutter /// ``` /// /// `averageCharacterAdvance` is the system font's rough average advance for mixed-case Latin text as /// a fraction of its point size — half an em, the classic typesetter's estimate. It is deliberately /// an *estimate* rather than a measurement: the sidebar is sized so a filename, a palette grid and a /// key/value row have room, not so any particular string fits exactly, and a measured advance would /// make this geometry depend on which glyphs happened to be on screen. The gutter is one em, which /// is what keeps the whole thing scaling together. /// /// ### Why the point size is a parameter /// /// So the rule is a pure function and a test can hold it still. `bodyPointSize` below is the one /// place that asks the system what the body font actually is; everything else takes it as an /// argument, which is also what makes "the sidebar is narrower at 11pt and wider at 18pt" a fact a /// suite can assert rather than something to be verified by eye at three text sizes. enum CardWindowMetrics { // MARK: - The unit /// The system font's approximate average advance per character, as a fraction of its point size. static let averageCharacterAdvance: CGFloat = 0.5 /// The horizontal inset on each side of a column: one em, so it scales with everything else. static func gutter(bodyPointSize: CGFloat) -> CGFloat { bodyPointSize } /// A column `characters` body-characters wide, gutters included — the one expression. static func columnWidth(characters: CGFloat, bodyPointSize: CGFloat) -> CGFloat { let text = characters * averageCharacterAdvance * bodyPointSize return (text + 2 * gutter(bodyPointSize: bodyPointSize)).rounded() } /// A line's height in the body font — the vertical counterpart of the advance, used only for the /// window's minimum and default heights. static func lineHeight(bodyPointSize: CGFloat) -> CGFloat { bodyPointSize * 1.4 } // MARK: - The sidebar /// How wide the attributes sidebar is, in characters. Narrow by contract — it holds a /// middle-truncated filename, a palette grid and a key/value row, and nothing in it ever wants /// the window's spare width, which all goes to the body (05 ▸ Composition). static let sidebarCharacters: CGFloat = 26 /// **The sidebar's width, and the only place it is decided.** Fixed for a given text size: the /// window's resize flex goes entirely to the body column, so this is not a fraction of anything. static func sidebarWidth(bodyPointSize: CGFloat) -> CGFloat { columnWidth(characters: sidebarCharacters, bodyPointSize: bodyPointSize) } // MARK: - The body column /// The narrowest the body column is allowed to get — a measure of prose short enough to be a /// floor rather than a preference. static let bodyMinimumCharacters: CGFloat = 44 /// The body column at its resting default: a comfortable measure, which the user then resizes. static let bodyDefaultCharacters: CGFloat = 74 static func bodyMinimumWidth(bodyPointSize: CGFloat) -> CGFloat { columnWidth(characters: bodyMinimumCharacters, bodyPointSize: bodyPointSize) } // MARK: - The rendered body /// One step of structural indent in Preview — a list level, a quote level. One and a half ems, /// which is wide enough for a bullet plus its space and narrow enough that four nested levels /// still leave a measure worth reading. static func previewIndent(bodyPointSize: CGFloat) -> CGFloat { (bodyPointSize * 1.5).rounded() } /// The inside padding of a table cell and the inset of a code block — half a gutter, so the /// rendered body's rhythm is the column's rhythm halved rather than a second, unrelated one. static func previewPadding(bodyPointSize: CGFloat) -> CGFloat { (gutter(bodyPointSize: bodyPointSize) / 2).rounded() } /// The widest an inline image is drawn at. /// /// A number rather than "the text container's width" on purpose: a `NSTextAttachment`'s bounds /// are fixed at build time, so an image sized to the window would have to be rebuilt on every /// resize — and the body column is resizable by contract. A generous cap keeps a screenshot /// legible without letting a 4000-pixel-wide one push the measure around, and an image narrower /// than the cap is never enlarged. static func previewImageMaximumWidth(bodyPointSize: CGFloat) -> CGFloat { columnWidth(characters: 56, bodyPointSize: bodyPointSize) } // MARK: - The window /// The window's minimum size: the sidebar's fixed width plus the body's floor, and tall enough /// for a title, its date line and a few lines of body. static func minimumSize(bodyPointSize: CGFloat) -> CGSize { CGSize( width: sidebarWidth(bodyPointSize: bodyPointSize) + bodyMinimumWidth(bodyPointSize: bodyPointSize), height: (lineHeight(bodyPointSize: bodyPointSize) * 16).rounded() ) } /// The size a card window opens at when there is no last-used size to open at — a first-ever /// card window, and nothing else (05 ▸ Window: "New windows open at the last-used card-window /// size, cascaded"; `AppPreferences.lastCardWindowSize` is that memory). static func defaultSize(bodyPointSize: CGFloat) -> CGSize { CGSize( width: sidebarWidth(bodyPointSize: bodyPointSize) + columnWidth(characters: bodyDefaultCharacters, bodyPointSize: bodyPointSize), height: (lineHeight(bodyPointSize: bodyPointSize) * 32).rounded() ) } // MARK: - The live metric /// The body font's point size as the system currently reports it — the one impure read, kept to /// one line so every derivation above stays testable. @MainActor static var bodyPointSize: CGFloat { NSFont.preferredFont(forTextStyle: .body).pointSize } }