summaryrefslogtreecommitdiff
path: root/src/macos
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-01 13:46:44 -0300
committerGabriel Schneider <[email protected]>2026-09-01 13:57:41 -0300
commitcce6b18a49870086982f9a0e1fda90ed170b9fba (patch)
treee5ce990d0a1d43503375d891bc51f283e0a1c215 /src/macos
parentae9325a5cb128d0d952afb8f9feaaca68e5e37a2 (diff)
downloadpardes-cce6b18a49870086982f9a0e1fda90ed170b9fba.tar.gz
pardes-cce6b18a49870086982f9a0e1fda90ed170b9fba.zip
macos: one tagline rule for both hosts, a kqueue beside the inotify, and effects that compile
Three things this shell had its own copy of, and in each case the fix is that it stops having one. **The tagline band.** A pane tag draws at `gui_tagline_font_percent` of the body face and the band it sits on shrinks with it, while the grid row stays body-sized — so something has to decide where the shorter band sits in the taller row. This shell decided by centring, always, which is precisely the case `config.gui_topbar_pane_border_px` exists to prevent: the topbar's unused half-band meets the first pane tag's unused half-band and the window background shows through the seam. The strip is as wide as the bands are short — on a 20-pixel cell, 4 physical pixels at the default 82%, 10 at 50%, 14 at 30% — so it grew as the tagline face shrank and read as "the tagline is wrong on the mac" rather than as one missing rule. The rule is `pardes.taglineBandOffset` in the core now and both pixel hosts call it: row zero bottom-aligned, the first pane-tag row top-aligned, the two joined by `gui_topbar_pane_border_px` in the theme's scrollbar-track colour, every row between centred, and a `Tagbottom` band on the final row flush with the window edge — with the sub-cell strip beneath it painted in that band's own colour, because the core grid holds only whole cells and a window is any height it likes. `pardes_tagline_band_offset`, `pardes_topbar_pane_border_px` and `pardes_topbar_pane_border_rgb` carry it over the C ABI as PHYSICAL pixels: the host multiplies its points by the backing scale going in and divides coming out, which is the snapping `Metrics` already does for the cell, and is what keeps a one-pixel rule one pixel instead of a two-pixel smear. **The watch.** `file_watch.zig` was one mark/reconcile transaction over `inotify`, so the tty shell, the SDL window and the detached daemon all watched nothing off Linux: an edit made outside pardes never reached the pane, and a PDF replaced on disk kept rendering the old inode. It is the same transaction over two kernels now — `init`, `wait`, `stop`, `drain`, `markDir` and `unmarkDir` are still the whole of it, and the hosts wait on a kqueue and poll it exactly as they did the old descriptor. A macOS mark is TWO filters, because a kqueue directory filter reports its entries changing and never a write to a file already inside it: the parent mark follows rename-over saves, `markFile` catches in-place writes, and `remarkFile` re-arms the file filter once a rename has moved the inode. That is the same pair the AppKit host's DispatchSources already used for the same reason. Directory marks are deduplicated here by device and inode, because each `EVFILT_VNODE` filter needs a descriptor of its own and inotify did that deduplication itself; `stop` and `drain` wake through the one `EVFILT_USER` filter, since a kqueue cannot simply be read the way an inotify descriptor can. **The effects.** The three `crt.ci.metal` entry points are `extern "C" [[stitchable]]`. `CIKernel.kernels(withMetalString:)` compiles that source at runtime, looks for stitchable functions, and rejects the WHOLE source with "cannot find a valid stitchable Metal function in the source" when it finds none — so `ScenePostprocessor.init?` returned nil and every scene effect and panel transition silently degraded to the plain CoreText draw. The `effect_sources.zig` test pins the exact spelling of all three, and `draw-effect` in the e2e suite catches the degradation rather than the spelling. Beside them, the offscreen harness owes the core a PRESENTATION. Its window is borderless and never ordered front, so AppKit runs no display cycle and `pardes_frame_presented` — whose only caller is `draw(_:)` — never fired. The core holds pointer gestures inert while a layout mutation has not reached a backend, which for an unpresenting harness is the rest of the script: the first pane a script opened silently killed every later click, drag and Look. So `readFrame` presents what it just rendered, into a bitmap nobody reads. `PARDES_CHROME` also looks under `/Applications`, where a browser's executable lives inside an application bundle and never on `PATH`. The macOS goldens are regenerated; docs/macos.md, config.md, detached.md, web.md and the design PDF follow.
Diffstat (limited to 'src/macos')
-rw-r--r--src/macos/Sources/PardesView.swift105
-rw-r--r--src/macos/pardes.h21
2 files changed, 118 insertions, 8 deletions
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index aa8ac425..10c7fd8b 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -327,11 +327,17 @@ private struct Metrics {
private struct TaglineMetrics {
let percent: UInt8
let fonts: [CTFont]
- let baseline: CGFloat
+ /// The tagline face's OWN ascent. Deliberately not folded together with the
+ /// band's top offset the way it used to be: that offset is per-row now, and
+ /// a baseline carrying one row's offset would pin every band back to centre.
+ let ascent: CGFloat
let xOffset: CGFloat
- let top: CGFloat
let height: CGFloat
let asciiGlyphs: [[CGGlyph]]
+ /// Kept so the band rules below can work in the physical pixels the core
+ /// states them in, and hand back points.
+ private let scale: CGFloat
+ private let bodyCellHeight: CGFloat
init(body: Metrics, percent: UInt8, scale rawScale: CGFloat) {
let scale = max(1, rawScale)
@@ -339,6 +345,8 @@ private struct TaglineMetrics {
(value * scale).rounded(rule) / scale
}
self.percent = percent
+ self.scale = scale
+ bodyCellHeight = body.cellHeight
let size = CTFontGetSize(body.fonts[0]) * CGFloat(percent) / 100
let faces = body.fonts.map { CTFontCreateCopyWithAttributes($0, size, nil, nil) }
fonts = faces
@@ -349,9 +357,7 @@ private struct TaglineMetrics {
// clamp here too: a malformed/custom host value must not paint a tag
// over the body row below it.
height = min(body.cellHeight, measuredHeight)
- top = snap(max(0, (body.cellHeight - height) / 2), .toNearestOrAwayFromZero)
- let ascent = max(1 / scale, snap(CTFontGetAscent(face), .toNearestOrAwayFromZero))
- baseline = snap(top + ascent, .toNearestOrAwayFromZero)
+ ascent = max(1 / scale, snap(CTFontGetAscent(face), .toNearestOrAwayFromZero))
xOffset = snap(max(0, (body.cellWidth - advance(face, 0x4D)) / 2), .toNearestOrAwayFromZero)
asciiGlyphs = faces.map { font in
var chars = Array(UniChar(0)..<UniChar(128))
@@ -360,6 +366,33 @@ private struct TaglineMetrics {
return glyphs
}
}
+
+ private var cellPixels: UInt32 { UInt32(max(1, (bodyCellHeight * scale).rounded())) }
+ private var bandPixels: UInt32 { UInt32(max(1, (height * scale).rounded())) }
+
+ /// Where this band sits inside row `row`, measured down from the row's top.
+ /// The rule belongs to the core (`pardes_tagline_band_offset`) so that this
+ /// shell and the SDL one cannot disagree about it: row zero and the first
+ /// pane-tag row meet at a shared rule, a Tagbottom band on the last row
+ /// faces the window edge, and everything between is centred.
+ func top(row: Int, canvasHeight: CGFloat) -> CGFloat {
+ CGFloat(pardes_tagline_band_offset(
+ UInt16(clamping: row), Float(canvasHeight * scale), cellPixels, bandPixels)) / scale
+ }
+
+ /// Thickness of the rule joining the topbar band to the first pane-tag band,
+ /// zero when the two are meant to join directly.
+ var borderThickness: CGFloat {
+ CGFloat(pardes_topbar_pane_border_px(cellPixels, bandPixels)) / scale
+ }
+
+ /// Top of that rule. Placed the way the SDL shell places it, in WHOLE
+ /// pixels: half the rule above the topbar/pane boundary, floored, so a
+ /// one-pixel rule lands on the boundary instead of smeared across two rows.
+ var borderTop: CGFloat {
+ let px = pardes_topbar_pane_border_px(cellPixels, bandPixels)
+ return (CGFloat(cellPixels * UInt32(PARDES_TOPBAR_H)) - CGFloat(px / 2)) / scale
+ }
}
private func modifiers(_ flags: NSEvent.ModifierFlags) -> UInt32 {
@@ -794,9 +827,15 @@ final class PardesView: NSView {
// that feel slow. Antialiasing is off because touching rects share an
// edge, and blending that edge twice draws a visible seam.
ctx.setShouldAntialias(false)
+ // Band geometry is stated against the whole canvas: the last row's rule
+ // depends on where the window edge is, not just on the row index.
+ let canvasHeight = bounds.height
for row in 0..<rows {
let base = row * cols
let y = CGFloat(row) * cellHeight
+ // One offset per ROW, not per run: it is the same answer for every
+ // cell in the row, and it is a call across the ABI.
+ let bandTop = taglines.top(row: row, canvasHeight: canvasHeight)
var start = 0
var color = resolve(cells[base], block: blockY == row && blockX == 0,
ground: ground, clearGround: clearGround).bg
@@ -814,7 +853,7 @@ final class PardesView: NSView {
// bgClear runs are the ground showing through, and the ground is
// already clear — painting them would be painting the hole shut.
if color != bgClear {
- let bandY = tagline ? y + taglines.top : y
+ let bandY = tagline ? y + bandTop : y
let bandHeight = tagline ? taglines.height : cellHeight
fill(ctx, CGRect(x: CGFloat(start) * cellWidth, y: bandY,
width: CGFloat(col - start) * cellWidth, height: bandHeight),
@@ -826,6 +865,50 @@ final class PardesView: NSView {
}
}
+ // The rule joining the topbar band to the first pane-tag band. Both were
+ // centred in their own rows until now, and centring two reduced-height
+ // bands is exactly what leaves their unused half-bands touching with a
+ // strip of window background showing between them — the case
+ // config.gui_topbar_pane_border_px exists to close. Drawn only when that
+ // first pane row really carries a band, so a session whose second row is
+ // pane BODY gets no stray line across it.
+ let ruleThickness = taglines.borderThickness
+ let ruleColor = pardes_topbar_pane_border_rgb()
+ if ruleThickness > 0, ruleColor != UInt32(PARDES_COLOR_DEFAULT),
+ rows > Int(PARDES_TOPBAR_H) {
+ let base = Int(PARDES_TOPBAR_H) * cols
+ let joins = (0..<cols).contains { col in
+ let flags = cells[base + col].flags
+ return flags & UInt8(PARDES_CELL_DEFAULT) == 0
+ && flags & UInt8(PARDES_CELL_TAGLINE) != 0
+ }
+ if joins {
+ fill(ctx, CGRect(x: 0, y: taglines.borderTop, width: bounds.width,
+ height: ruleThickness), ruleColor, 1)
+ }
+ }
+
+ // The core grid contains only complete cells, so any window height that
+ // is not a whole multiple of the cell leaves a strip below the last row.
+ // A Tagbottom band there is bottom-aligned against the window edge, and
+ // without this that strip shows the page colour under it.
+ let gridBottom = CGFloat(rows) * cellHeight
+ if gridBottom < canvasHeight, rows > 0 {
+ let base = (rows - 1) * cols
+ for col in 0..<cols {
+ let cell = cells[base + col]
+ guard cell.flags & UInt8(PARDES_CELL_DEFAULT) == 0,
+ cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0 else { continue }
+ let bg = resolve(cell, block: false, ground: ground,
+ clearGround: clearGround).bg
+ if bg != bgClear {
+ fill(ctx, CGRect(x: 0, y: gridBottom, width: bounds.width,
+ height: canvasHeight - gridBottom), bg, 1)
+ }
+ break
+ }
+ }
+
// CTFontDrawGlyphs lays glyph outlines out with +y up, and isFlipped hands
// us a y-down CTM, so drawing text directly in view space renders every
// line mirrored. Un-flip once for the whole glyph pass and convert each
@@ -852,6 +935,7 @@ final class PardesView: NSView {
for row in 0..<rows {
drawRow(ctx, cells, base: row * cols, cols: cols,
baseline: height - (CGFloat(row) * cellHeight + metrics.ascent),
+ taglineTop: taglines.top(row: row, canvasHeight: canvasHeight),
blockCol: blockY == row ? blockX : -1)
}
ctx.restoreGState()
@@ -876,7 +960,8 @@ final class PardesView: NSView {
let fg = resolve(cell, block: false,
ground: themeBG ?? pardesDefaultBG, clearGround: false).fg
let tagline = cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0
- let caretY = CGFloat(y) * cellHeight + (tagline ? taglines.top : 0)
+ let caretY = CGFloat(y) * cellHeight
+ + (tagline ? taglines.top(row: y, canvasHeight: bounds.height) : 0)
let caretHeight = tagline ? taglines.height : cellHeight
ctx.setShouldAntialias(false)
fill(ctx, CGRect(x: CGFloat(x) * cellWidth, y: caretY,
@@ -1044,6 +1129,10 @@ final class PardesView: NSView {
base: Int,
cols: Int,
baseline: CGFloat,
+ /// Where this row's tagline band starts, from the row's top. Passed in
+ /// rather than recomputed per cell: it is one answer per row, and the
+ /// glyph has to sit in the band the background pass painted.
+ taglineTop: CGFloat,
blockCol: Int
) {
var runFace = Face.regular
@@ -1069,7 +1158,7 @@ final class PardesView: NSView {
if cell.flags & UInt8(PARDES_CELL_DEFAULT) != 0 { continue }
let tagline = cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0
let cellBaseline = tagline
- ? baseline + metrics.ascent - taglines.baseline
+ ? baseline + metrics.ascent - (taglineTop + taglines.ascent)
: baseline
// clearGround: false — this pass only reads `fg`, and a glyph is
// never the hole in the ground.
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
index e9d7bb41..8b798df6 100644
--- a/src/macos/pardes.h
+++ b/src/macos/pardes.h
@@ -257,6 +257,27 @@ uint32_t pardes_theme_bg(void);
// polls it and rebuilds only tagline glyph/metric state.
uint8_t pardes_gui_tagline_font_percent(void);
+// Where a reduced-height tagline band sits inside its body-sized grid row, in
+// PHYSICAL PIXELS down from the row's top, and the rule joining the topbar band
+// to the first pane-tag band. The core owns this geometry so that this shell and
+// the SDL one cannot disagree about it; centring every band is what this shell
+// used to do, and it left a strip of window background between the two.
+//
+// Points in, pixels out: multiply by the backing scale before calling and divide
+// the answer back, the same snapping the cell metrics already use.
+uint32_t pardes_tagline_band_offset(uint16_t row, float canvas_h, uint32_t cell_h,
+ uint32_t tagline_h);
+uint32_t pardes_topbar_pane_border_px(uint32_t cell_h, uint32_t tagline_h);
+
+// Colour of that rule: a compiled override, else the theme's scrollbar track.
+// PARDES_COLOR_DEFAULT before there is a session to ask — do not draw it then.
+uint32_t pardes_topbar_pane_border_rgb(void);
+
+// Rows the global topbar occupies, and therefore the row index of the FIRST
+// pane tagline — the one the rule above joins the topbar band to. Mirrors
+// pardes.TOPBAR_H, and src/macos.zig asserts the two agree.
+#define PARDES_TOPBAR_H 1u
+
// Persistent full-window effect switches and their display-clock time. A zero
// flags word means draw the CoreText frame directly; any nonzero combination
// is one postprocess pass. Snapshot by value, with no borrowed storage.