From 0d0b397ce1e7f9fcd1f361865d5fbb75359d6a0c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 26 Jul 2026 00:31:58 +0200 Subject: [PATCH] Added convenience overloads to Rect and also, Collection conformance in the Playdate bindings target. --- .../Graphics/Classes/BitmapTable.swift | 14 +++++ Sources/PlaydateKit/Graphics/Graphics.swift | 56 +++++++++++++++++++ .../Graphics/Structures/Graphics.Rect.swift | 6 ++ Tests/PlaydateKit/MockPlaydate.swift | 6 ++ Tests/PlaydateKit/WrapperTests.swift | 10 ++++ 5 files changed, 92 insertions(+) diff --git a/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift b/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift index 792f4a2..9cbebe0 100644 --- a/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift +++ b/Sources/PlaydateKit/Graphics/Classes/BitmapTable.swift @@ -56,3 +56,17 @@ extension Graphics { public var count: Int { info.count } } } + +extension Graphics.BitmapTable: RandomAccessCollection { + public var startIndex: Int { 0 } + public var endIndex: Int { count } + + /// The bitmap at `position`. The bitmap references storage owned by the + /// table; keep the table alive while using it. + public subscript(position: Int) -> Graphics.Bitmap { + guard let bitmap = bitmap(at: position) else { + preconditionFailure("bitmap table index out of range") + } + return bitmap + } +} diff --git a/Sources/PlaydateKit/Graphics/Graphics.swift b/Sources/PlaydateKit/Graphics/Graphics.swift index 810f173..2ccd4ad 100644 --- a/Sources/PlaydateKit/Graphics/Graphics.swift +++ b/Sources/PlaydateKit/Graphics/Graphics.swift @@ -46,11 +46,21 @@ extension Graphics { gfx.pointee.setClipRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height)) } + /// Sets the clip rect in world coordinates (affected by the draw offset). + public static func setClipRect(_ rect: Rect) { + setClipRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height) + } + /// Sets the clip rect in screen coordinates (unaffected by the draw offset). public static func setScreenClipRect(x: Int, y: Int, width: Int, height: Int) { gfx.pointee.setScreenClipRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height)) } + /// Sets the clip rect in screen coordinates (unaffected by the draw offset). + public static func setScreenClipRect(_ rect: Rect) { + setScreenClipRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height) + } + /// Clears the current clip rect. public static func clearClipRect() { gfx.pointee.clearClipRect.unsafelyUnwrapped() @@ -103,6 +113,11 @@ extension Graphics { } } + /// Draws the outline of a rectangle, stroked inside its frame. + public static func drawRect(_ rect: Rect, color: Color) { + drawRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height, color: color) + } + /// Fills the rectangle with `color`. public static func fillRect(x: Int, y: Int, width: Int, height: Int, color: Color) { color.withLCDColor { @@ -110,6 +125,11 @@ extension Graphics { } } + /// Fills the rectangle with `color`. + public static func fillRect(_ rect: Rect, color: Color) { + fillRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height, color: color) + } + /// Draws the outline of a rectangle with rounded corners, stroked with /// `lineWidth`. public static func drawRoundRect(x: Int, y: Int, width: Int, height: Int, radius: Int, @@ -120,6 +140,13 @@ extension Graphics { } } + /// Draws the outline of a rectangle with rounded corners, stroked with + /// `lineWidth`. + public static func drawRoundRect(_ rect: Rect, radius: Int, lineWidth: Int, color: Color) { + drawRoundRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height, + radius: radius, lineWidth: lineWidth, color: color) + } + /// Fills a rectangle with rounded corners. public static func fillRoundRect(x: Int, y: Int, width: Int, height: Int, radius: Int, color: Color) { color.withLCDColor { @@ -128,6 +155,12 @@ extension Graphics { } } + /// Fills a rectangle with rounded corners. + public static func fillRoundRect(_ rect: Rect, radius: Int, color: Color) { + fillRoundRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height, + radius: radius, color: color) + } + /// Draws an ellipse stroked inside the rect. If the angles differ, draws /// an arc from `startAngle` to `endAngle` (clockwise degrees, 0 at top). public static func drawEllipse(x: Int, y: Int, width: Int, height: Int, lineWidth: Int, @@ -148,6 +181,22 @@ extension Graphics { } } + /// Draws an ellipse stroked inside the rect. If the angles differ, draws + /// an arc from `startAngle` to `endAngle` (clockwise degrees, 0 at top). + public static func drawEllipse(in rect: Rect, lineWidth: Int, + startAngle: Float = 0, endAngle: Float = 0, color: Color) { + drawEllipse(x: rect.left, y: rect.top, width: rect.width, height: rect.height, + lineWidth: lineWidth, startAngle: startAngle, endAngle: endAngle, color: color) + } + + /// Fills an ellipse inside the rect. If the angles differ, fills the + /// wedge from `startAngle` to `endAngle` (clockwise degrees, 0 at top). + public static func fillEllipse(in rect: Rect, + startAngle: Float = 0, endAngle: Float = 0, color: Color) { + fillEllipse(x: rect.left, y: rect.top, width: rect.width, height: rect.height, + startAngle: startAngle, endAngle: endAngle, color: color) + } + /// Fills the polygon described by the points, connecting the last point /// back to the first. public static func fillPolygon(points: [(x: Int, y: Int)], color: Color, @@ -205,6 +254,13 @@ extension Graphics { } } + /// Draws `text` wrapped and aligned inside the given rectangle. + public static func drawText(_ text: String, in rect: Rect, + wrap: TextWrappingMode = .word, align: TextAlignment = .left) { + drawText(text, x: rect.left, y: rect.top, width: rect.width, height: rect.height, + wrap: wrap, align: align) + } + /// Sets the font used by subsequent text drawing. public static func setFont(_ font: Font) { gfx.pointee.setFont.unsafelyUnwrapped(font.pointer) diff --git a/Sources/PlaydateKit/Graphics/Structures/Graphics.Rect.swift b/Sources/PlaydateKit/Graphics/Structures/Graphics.Rect.swift index 1748ddf..7bb5e21 100644 --- a/Sources/PlaydateKit/Graphics/Structures/Graphics.Rect.swift +++ b/Sources/PlaydateKit/Graphics/Structures/Graphics.Rect.swift @@ -33,6 +33,12 @@ extension Graphics { top: Int32(top), bottom: Int32(bottom)) } + /// The rect's width. + public var width: Int { right - left } + + /// The rect's height. + public var height: Int { bottom - top } + /// Returns the rect offset by (dx, dy). public func translated(dx: Int, dy: Int) -> Rect { Rect(left: left + dx, right: right + dx, top: top + dy, bottom: bottom + dy) diff --git a/Tests/PlaydateKit/MockPlaydate.swift b/Tests/PlaydateKit/MockPlaydate.swift index 8c2a5df..4ad2d74 100644 --- a/Tests/PlaydateKit/MockPlaydate.swift +++ b/Tests/PlaydateKit/MockPlaydate.swift @@ -186,6 +186,12 @@ enum Mock { Mock.record("getTableBitmap(\(index))") return index == 0 ? Mock.fakePointer() : nil } + gfxAPI.pointee.getBitmapTableInfo = { _, count, cellsWide in + Mock.record("getBitmapTableInfo") + // One bitmap, matching getTableBitmap vending index 0 only. + count?.pointee = 1 + cellsWide?.pointee = 1 + } } // MARK: - Sprite diff --git a/Tests/PlaydateKit/WrapperTests.swift b/Tests/PlaydateKit/WrapperTests.swift index a8527d3..2f72989 100644 --- a/Tests/PlaydateKit/WrapperTests.swift +++ b/Tests/PlaydateKit/WrapperTests.swift @@ -89,6 +89,16 @@ struct WrapperTests { #expect(Mock.eventCount("freeBitmapTable") == 1) } + @Test func bitmapTableIsACollection() { + let table = Graphics.BitmapTable(count: 4, width: 8, height: 8) + #expect(table.count == 1) // the mock reports a single bitmap + #expect(table.first != nil) + + var visited = 0 + for _ in table { visited += 1 } + #expect(visited == 1) + } + // MARK: Sprite @Test func spriteUserdataRecoversWrapperInCallbacks() {