diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 14:46:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (patch) | |
| tree | 642150be8703f1d9a0766ff18516f06bfdc8801a /src/macos/Sources | |
| parent | 379f4fe55c7c9c7c8cc7b97dc921c83e2c24698b (diff) | |
| download | pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.tar.gz pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.zip | |
Add optional compact tagline styling for source context
Diffstat (limited to 'src/macos/Sources')
| -rw-r--r-- | src/macos/Sources/AppDelegate.swift | 2 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 133 |
2 files changed, 126 insertions, 9 deletions
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift index eb499f1b..96f4a184 100644 --- a/src/macos/Sources/AppDelegate.swift +++ b/src/macos/Sources/AppDelegate.swift @@ -671,6 +671,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { grid.cols, grid.rows, UInt16((view.cellWidth * scale).rounded()), UInt16((view.cellHeight * scale).rounded())) + pardes_row_metrics(UInt16(max(1,(view.cellWidth*scale).rounded())), UInt16(max(1,(view.cellHeight*scale).rounded())), + UInt16(max(1,(view.taglineCellWidth*scale).rounded())), UInt16(max(1,(view.taglineBandHeight*scale).rounded()))) pump() } diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index 2160047d..b14e1aa0 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -736,12 +736,15 @@ final class PardesView: NSView { guard percent != taglines.percent else { return false } taglines = TaglineMetrics(body: metrics, percent: percent, scale: metricsScale) taglineGlyphCache.removeAll(keepingCapacity: true) + pardes_row_metrics(UInt16(max(1,(cellWidth*metricsScale).rounded())),UInt16(max(1,(cellHeight*metricsScale).rounded())), + UInt16(max(1,(taglines.width*metricsScale).rounded())),UInt16(max(1,(taglines.height*metricsScale).rounded()))) needsDisplay = true return true } var taglinePercent: UInt8 { taglines.percent } var taglineBandHeight: CGFloat { taglines.height } + var taglineCellWidth: CGFloat { taglines.width } /// Seed runtime Config after init and update it after every successful /// adopt/zoom. Points are the native owner's unit; the cell's backing-pixel @@ -905,7 +908,10 @@ final class PardesView: NSView { // Only a canonical presentation advances the attachment baseline. // During a transition deleted panes keep their cropped CGImages alive // here even after both the core placement and image cache entry vanish. - if !animatedPanels { presentedImages = frameImages } + if !animatedPanels { + presentedImages = frameImages + presentedBodyLayers = snapshotBodyLayers() + } presentationSerial &+= 1 let animationWoke = pardes_frame_presented(animatedPanels) let pointerMoved = refreshPresentedPointer() @@ -936,9 +942,9 @@ final class PardesView: NSView { guard changed else { return false } if let button = latchedButton { latchedCell = at - pardes_mouse(button, PARDES_MOUSE_DRAG, at.col, at.row, pointerModifiers) + sendMouse(button, PARDES_MOUSE_DRAG, at.col, at.row, pointerModifiers) } else { - pardes_mouse(PARDES_MOUSE_NONE, PARDES_MOUSE_MOTION, + sendMouse(PARDES_MOUSE_NONE, PARDES_MOUSE_MOTION, at.col, at.row, pointerModifiers) } return true @@ -1159,6 +1165,101 @@ final class PardesView: NSView { width: caretWidth, height: caretHeight), fg, 1) } } + if frozenImages == nil { + drawBodyLayers(ctx, hideCursor:hideCursor) + } else { + for layer in presentedBodyLayers { + layer.cells.withUnsafeBufferPointer { cells in + if let base = cells.baseAddress { drawBodyLayer(ctx, values:layer.values, cells:base, hideCursor:true) } + } + } + } + } + + private struct FrozenBodyLayer { + let values: [Int] + let cells: [pardes_cell_s] + } + private var presentedBodyLayers: [FrozenBodyLayer] = [] + + private func snapshotBodyLayers() -> [FrozenBodyLayer] { + var result: [FrozenBodyLayer] = [] + for index in 0..<pardes_body_layer_limit() { + let values = (0..<11).map { Int(pardes_body_layer_value(index, UInt32($0))) } + if values[0] == 0 { continue } + if let cells = pardes_body_layer_cells(index) { + result.append(FrozenBodyLayer(values:values, cells:Array(UnsafeBufferPointer(start:cells,count:values[0]*values[1])))) + } + } + return result + } + + private func drawBodyLayers(_ ctx: CGContext, hideCursor:Bool) { + for index in 0..<pardes_body_layer_limit() { + let values = (0..<11).map { Int(pardes_body_layer_value(index, UInt32($0))) } + if values[0] == 0 { continue } + if let cells = pardes_body_layer_cells(index) { drawBodyLayer(ctx, values:values, cells:cells, hideCursor:hideCursor) } + } + } + + private func drawBodyLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>, hideCursor: Bool) { + func value(_ field: Int) -> Int { values[field] } + let rows = value(0), cols = value(1), contexts = value(2) + let left = CGFloat(value(3)) * cellWidth, top = CGFloat(value(4)) * cellHeight + let viewport = CGRect(x: left, y: top, width: CGFloat(value(5))*cellWidth, + height: CGFloat(value(6))*cellHeight) + ctx.saveGState() + ctx.clip(to: viewport) + fill(ctx, viewport, themeBG ?? pardesDefaultBG, 1) + for row in 0..<rows { + let compact = row < contexts + let advance = compact ? taglines.width : cellWidth + let height = compact ? taglines.height : cellHeight + let y = top + CGFloat(min(row, contexts))*taglines.height + CGFloat(max(0, row-contexts))*cellHeight + if y >= viewport.maxY { break } + let rowStyle = resolve(cells[row*cols], block: false, ground: themeBG ?? pardesDefaultBG, clearGround: false) + fill(ctx, CGRect(x: left, y: y, width: viewport.width, height: height), rowStyle.bg, 1) + for pass in 0...1 { + for col in 0..<cols { + var cell = cells[row*cols+col] + let isCursor = !hideCursor && col == value(8) && row == value(9) + let style = resolve(cell, block: isCursor && value(10) == 0, + ground: themeBG ?? pardesDefaultBG, clearGround: false) + let x = left + CGFloat(col)*advance + if pass == 0 { + fill(ctx, CGRect(x: x, y: y, width: advance, height: height), style.bg, 1) + continue + } + let text = withUnsafeBytes(of: &cell.text) { bytes in + String(decoding: bytes.prefix(Int(cell.len)), as: UTF8.self) + } + if style.visible && !text.isEmpty && text != " " { + let face = Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, + italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) + let font = compact ? taglines.fonts[face.rawValue] : metrics.fonts[face.rawValue] + ctx.saveGState() + ctx.translateBy(x: 0, y: bounds.height) + ctx.scaleBy(x: 1, y: -1) + let baseline = bounds.height - y - (compact ? taglines.ascent : metrics.ascent) + drawRules(ctx, cell, style, x: x, width: advance, baseline: baseline) + setFill(ctx, style.fg, style.alpha) + ctx.textPosition = CGPoint(x: x, y: baseline) + let line = NSAttributedString(string: text, attributes: [fontAttribute: font]) + CTLineDraw(CTLineCreateWithAttributedString(line as CFAttributedString), ctx) + ctx.restoreGState() + } + if isCursor && value(10) != 0 { + fill(ctx, CGRect(x: x, y: y, width: max(1, advance/8), height: height), style.fg, 1) + } + } + } + } + let border = pardes_topbar_pane_border_rgb() + if border != UInt32(PARDES_COLOR_DEFAULT) { + fill(ctx, CGRect(x: left, y: top+CGFloat(contexts)*taglines.height, + width: viewport.width, height: 1/taglines.scale), border, 1) + } + ctx.restoreGState() } /// What identifies a decoded raster: the pane's lifetime, the page, and the @@ -1523,23 +1624,36 @@ final class PardesView: NSView { // keyboard state; the NSEvent overrides always pass the real mask. Ctrl is // the one the core actually consults — a left press with it held is // goto-definition — so dropping it here would silently delete a feature. + private var sampledBodyPointer: CGPoint? + + private func sendMouse(_ button: pardes_mouse_button_e, _ kind: pardes_mouse_kind_e, + _ col: UInt16, _ row: UInt16, _ mods: UInt32) { + guard let point = sampledBodyPointer else { + pardes_mouse(button, kind, col, row, mods) + return + } + let scale = metricsScale + pardes_mouse_pixel(button, kind, col, row, mods, Float(point.x*scale), Float(point.y*scale), + Float(cellWidth*scale), Float(cellHeight*scale), Float(taglines.width*scale), Float(taglines.height*scale)) + } + func press(_ button: pardes_mouse_button_e, at cell: GridPoint, mods: UInt32 = 0) { - pardes_mouse(button, PARDES_MOUSE_PRESS, cell.col, cell.row, mods) + sendMouse(button, PARDES_MOUSE_PRESS, cell.col, cell.row, mods) fed() } func release(_ button: pardes_mouse_button_e, at cell: GridPoint, mods: UInt32 = 0) { - pardes_mouse(button, PARDES_MOUSE_RELEASE, cell.col, cell.row, mods) + sendMouse(button, PARDES_MOUSE_RELEASE, cell.col, cell.row, mods) fed() } func drag(_ button: pardes_mouse_button_e, to cell: GridPoint, mods: UInt32 = 0) { - pardes_mouse(button, PARDES_MOUSE_DRAG, cell.col, cell.row, mods) + sendMouse(button, PARDES_MOUSE_DRAG, cell.col, cell.row, mods) fed() } func motion(to cell: GridPoint, mods: UInt32 = 0) { - pardes_mouse(PARDES_MOUSE_NONE, PARDES_MOUSE_MOTION, cell.col, cell.row, mods) + sendMouse(PARDES_MOUSE_NONE, PARDES_MOUSE_MOTION, cell.col, cell.row, mods) fed() } @@ -1553,7 +1667,7 @@ final class PardesView: NSView { /// One discrete wheel notch, as opposed to the continuous travel below. func wheel(_ button: pardes_mouse_button_e, at cell: GridPoint, mods: UInt32 = 0) { - pardes_mouse(button, PARDES_MOUSE_PRESS, cell.col, cell.row, mods) + sendMouse(button, PARDES_MOUSE_PRESS, cell.col, cell.row, mods) fed() } @@ -1721,7 +1835,7 @@ final class PardesView: NSView { // before one pump notification so no frame can observe a held button // whose pointer is already outside the displayed scene. if let last = latchedCell { - pardes_mouse(button, PARDES_MOUSE_RELEASE, last.col, last.row, + sendMouse(button, PARDES_MOUSE_RELEASE, last.col, last.row, modifiers(event.modifierFlags)) } latchedCell = nil @@ -1891,6 +2005,7 @@ final class PardesView: NSView { } else { sampled = point } + sampledBodyPointer = sampled let row = min(max(Int(sampled.y / cellHeight), 0), rows - 1) // The column comes from the core, because a tag row's glyphs step at // the tagline face's narrower pitch and a body-pitch click drifts one |
