Restructured the source code in the Playdate bindings target.

This commit is contained in:
2026-07-25 12:40:14 +02:00
parent 2e7ee12ec1
commit b435a6e8bd
106 changed files with 3754 additions and 3746 deletions
@@ -0,0 +1,152 @@
internal import CPlaydate
extension Graphics {
/// An image that can be drawn to the screen or used as a drawing target.
/// Wraps `LCDBitmap`.
public final class Bitmap {
let pointer: OpaquePointer
/// Whether this wrapper owns the underlying `LCDBitmap` and frees it
/// on deinit. Bitmaps vended by tables or the system are not owned;
/// keep their owner alive while using them.
let isOwned: Bool
init(pointer: OpaquePointer, isOwned: Bool) {
self.pointer = pointer
self.isOwned = isOwned
}
/// Allocates a new bitmap filled with `backgroundColor`.
public convenience init(width: Int, height: Int, backgroundColor: Color = .clear) {
let pointer = backgroundColor.withLCDColor {
gfx.pointee.newBitmap.unsafelyUnwrapped(Int32(width), Int32(height), $0)
}
self.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
}
/// Loads a bitmap from a file in the game's pdx or Data directory.
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.pointee.loadBitmap.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer, isOwned: true)
}
deinit {
if isOwned {
gfx.pointee.freeBitmap.unsafelyUnwrapped(pointer)
}
}
// MARK: Properties
public var data: Data {
var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0
var mask: UnsafeMutablePointer<UInt8>?
var data: UnsafeMutablePointer<UInt8>?
gfx.pointee.getBitmapData.unsafelyUnwrapped(pointer, &width, &height, &rowBytes, &mask, &data)
return Data(width: Int(width), height: Int(height), rowBytes: Int(rowBytes),
mask: mask, data: data)
}
/// Cached dimensions, so `width`/`height` don't pay a full
/// `getBitmapData` round-trip per access. Only `load(path:)` can
/// change a bitmap's size, which resets the cache.
private var cachedSize: (width: Int, height: Int)?
private var size: (width: Int, height: Int) {
if let cachedSize { return cachedSize }
let data = self.data
let size = (data.width, data.height)
cachedSize = size
return size
}
public var width: Int { size.width }
public var height: Int { size.height }
/// The color of the pixel at (x, y).
public func pixel(x: Int, y: Int) -> SolidColor {
SolidColor(gfx.pointee.getBitmapPixel.unsafelyUnwrapped(pointer, Int32(x), Int32(y)))
}
// MARK: Operations
/// Replaces the bitmap's contents with the image at `path`.
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) }
cachedSize = nil
if let error { throw PlaydateError(cString: error) }
}
/// Fills the bitmap with `color`.
public func clear(color: Color) {
color.withLCDColor { gfx.pointee.clearBitmap.unsafelyUnwrapped(pointer, $0) }
}
public func copy() -> Bitmap {
Bitmap(pointer: gfx.pointee.copyBitmap.unsafelyUnwrapped(pointer).unsafelyUnwrapped, isOwned: true)
}
/// Returns a new bitmap rotated by `degrees` (clockwise) and scaled.
public func rotated(by degrees: Float, xScale: Float = 1, yScale: Float = 1) -> Bitmap? {
var allocatedSize: Int32 = 0
guard let rotated = gfx.pointee.rotatedBitmap.unsafelyUnwrapped(
pointer, degrees, xScale, yScale, &allocatedSize) else { return nil }
return Bitmap(pointer: rotated, isOwned: true)
}
/// Sets a mask image. The mask must match the bitmap's dimensions.
@discardableResult
public func setMask(_ mask: Bitmap?) -> Bool {
gfx.pointee.setBitmapMask.unsafelyUnwrapped(pointer, mask?.pointer) != 0
}
/// The bitmap's mask, if any. The returned bitmap references storage
/// owned by this bitmap.
public var mask: Bitmap? {
guard let mask = gfx.pointee.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
return Bitmap(pointer: mask, isOwned: false)
}
/// Tests whether the opaque pixels of two bitmaps overlap within
/// `rect`, given each bitmap's position and flip.
public func checkMaskCollision(x: Int, y: Int, flip: BitmapFlip = .unflipped,
other: Bitmap, otherX: Int, otherY: Int,
otherFlip: BitmapFlip = .unflipped,
in rect: Rect) -> Bool {
gfx.pointee.checkMaskCollision.unsafelyUnwrapped(
pointer, Int32(x), Int32(y), flip.cValue,
other.pointer, Int32(otherX), Int32(otherY), otherFlip.cValue,
rect.cValue) != 0
}
// MARK: Drawing
/// Draws the bitmap with its upper-left corner at (x, y).
public func draw(x: Int, y: Int, flip: BitmapFlip = .unflipped) {
gfx.pointee.drawBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), flip.cValue)
}
/// Draws the bitmap scaled by (xScale, yScale) with its upper-left
/// corner at (x, y).
public func drawScaled(x: Int, y: Int, xScale: Float, yScale: Float) {
gfx.pointee.drawScaledBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), xScale, yScale)
}
/// Draws the bitmap rotated by `degrees` around its anchor point,
/// where (0.5, 0.5) is the center.
public func drawRotated(x: Int, y: Int, degrees: Float,
centerX: Float = 0.5, centerY: Float = 0.5,
xScale: Float = 1, yScale: Float = 1) {
gfx.pointee.drawRotatedBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y), degrees,
centerX, centerY, xScale, yScale)
}
/// Tiles the bitmap over the given area.
public func tile(x: Int, y: Int, width: Int, height: Int, flip: BitmapFlip = .unflipped) {
gfx.pointee.tileBitmap.unsafelyUnwrapped(pointer, Int32(x), Int32(y),
Int32(width), Int32(height), flip.cValue)
}
}
}
@@ -0,0 +1,57 @@
internal import CPlaydate
extension Graphics {
/// A collection of bitmaps loaded from an image table. Wraps `LCDBitmapTable`.
public final class BitmapTable {
let pointer: OpaquePointer
init(pointer: OpaquePointer) {
self.pointer = pointer
}
/// Allocates a table with room for `count` bitmaps of the given size.
public convenience init(count: Int, width: Int, height: Int) {
let pointer = gfx.pointee.newBitmapTable.unsafelyUnwrapped(Int32(count), Int32(width), Int32(height))
self.init(pointer: pointer.unsafelyUnwrapped)
}
/// Loads an image table from a file.
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
deinit {
gfx.pointee.freeBitmapTable.unsafelyUnwrapped(pointer)
}
/// Replaces the table's contents with the image table at `path`.
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.pointee.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) }
if let error { throw PlaydateError(cString: error) }
}
/// The bitmap at `index`, or `nil` if out of range. The bitmap
/// references storage owned by the table; keep the table alive while
/// using it.
public func bitmap(at index: Int) -> Bitmap? {
guard let bitmap = gfx.pointee.getTableBitmap.unsafelyUnwrapped(pointer, Int32(index)) else {
return nil
}
return Bitmap(pointer: bitmap, isOwned: false)
}
/// The number of bitmaps in the table and the number of cells per row
/// of the source image.
public var info: (count: Int, cellsWide: Int) {
var count: Int32 = 0, width: Int32 = 0
gfx.pointee.getBitmapTableInfo.unsafelyUnwrapped(pointer, &count, &width)
return (Int(count), Int(width))
}
public var count: Int { info.count }
}
}
@@ -0,0 +1,87 @@
internal import CPlaydate
extension Graphics {
/// A font loaded from a .pft file. Wraps `LCDFont`.
public final class Font {
let pointer: OpaquePointer
/// Fonts created from in-memory data reference that data; it is kept
/// alive here.
private let retainedData: UnsafeRawPointer?
init(pointer: OpaquePointer, retainedData: UnsafeRawPointer? = nil) {
self.pointer = pointer
self.retainedData = retainedData
}
/// Loads a font from a file.
public convenience init(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
let pointer = path.withPlaydateCString { gfx.pointee.loadFont.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
/// Creates a font from the contents of a .pft file already in memory.
/// The bytes are copied and retained for the font's lifetime.
public convenience init?(data: UnsafeRawBufferPointer, wide: Bool = false) {
let copy = UnsafeMutableRawPointer.allocate(byteCount: data.count, alignment: 4)
copy.copyMemory(from: data.baseAddress.unsafelyUnwrapped, byteCount: data.count)
let fontData = OpaquePointer(copy)
guard let pointer = gfx.pointee.makeFontFromData.unsafelyUnwrapped(
fontData, wide ? 1 : 0, Int32(data.count)) else {
copy.deallocate()
return nil
}
self.init(pointer: pointer, retainedData: UnsafeRawPointer(copy))
}
deinit {
// Per the C API docs, fonts are freed with the system allocator.
System.systemFree(UnsafeMutableRawPointer(pointer))
retainedData?.deallocate()
}
/// The font's glyph height in pixels.
public var height: Int {
Int(gfx.pointee.getFontHeight.unsafelyUnwrapped(pointer))
}
/// The width of `text` when drawn with this font.
public func textWidth(_ text: String, tracking: Int = 0) -> Int {
text.withPlaydateUTF8 { bytes, count in
Int(gfx.pointee.getTextWidth.unsafelyUnwrapped(pointer, bytes, count,
kUTF8Encoding, Int32(tracking)))
}
}
/// The height of `text` when wrapped to `maxWidth` with this font.
public func textHeight(_ text: String, maxWidth: Int, wrap: TextWrappingMode = .word,
tracking: Int = 0, extraLeading: Int = 0) -> Int {
text.withPlaydateUTF8 { bytes, count in
Int(gfx.pointee.getTextHeightForMaxWidth.unsafelyUnwrapped(
pointer, bytes, count, Int32(maxWidth), kUTF8Encoding,
wrap.cValue, Int32(tracking), Int32(extraLeading)))
}
}
/// The page containing glyph data for the character `codepoint`
/// belongs to. The page references data owned by the font.
public func page(for codepoint: UInt32) -> FontPage? {
guard let page = gfx.pointee.getFontPage.unsafelyUnwrapped(pointer, codepoint) else { return nil }
return FontPage(pointer: page, font: self)
}
/// The glyph for `codepoint`, with its bitmap and advance.
/// The bitmap references data owned by the font.
public func glyph(for codepoint: UInt32) -> (glyph: Glyph, bitmap: Bitmap?, advance: Int)? {
var bitmap: OpaquePointer?
var advance: Int32 = 0
guard let glyph = gfx.pointee.getFontGlyph.unsafelyUnwrapped(pointer, codepoint, &bitmap, &advance) else {
return nil
}
return (Glyph(pointer: glyph, font: self),
bitmap.map { Bitmap(pointer: $0, isOwned: false) },
Int(advance))
}
}
}
@@ -0,0 +1,81 @@
internal import CPlaydate
private var streamAPI: UnsafePointer<playdate_videostream> { Playdate.videoStreamAPI.unsafelyUnwrapped }
extension Graphics {
/// Streams video (and audio) from a file or network connection.
/// Wraps `LCDStreamPlayer`.
public final class StreamPlayer {
let pointer: OpaquePointer
/// Retains the active source so it outlives the stream.
private var retainedSource: AnyObject?
public init() {
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
}
deinit {
streamAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
/// Sets the sizes of the stream's video and audio buffers, in bytes.
public func setBufferSize(video: Int, audio: Int) {
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
}
/// Streams from an open file.
public func setFile(_ file: File.Handle) {
retainedSource = file
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
}
/// Streams from an HTTP connection.
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
retainedSource = connection
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// Streams from a TCP connection.
public func setTCPConnection(_ connection: Network.TCPConnection) {
retainedSource = connection
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// The player used for the stream's audio track. Owned by the stream.
/// The same wrapper is returned on every access, so callbacks
/// registered on it stay valid for the stream's lifetime.
public var filePlayer: Sound.FilePlayer? {
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
if let cached = cachedFilePlayer, cached.pointer == player {
return cached
}
let wrapper = Sound.FilePlayer(pointer: player, isOwned: false)
cachedFilePlayer = wrapper
return wrapper
}
private var cachedFilePlayer: Sound.FilePlayer?
/// The player used for the stream's video track. Owned by the stream.
public var videoPlayer: VideoPlayer? {
guard let player = streamAPI.pointee.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
return VideoPlayer(pointer: player, isOwned: false)
}
/// Advances the stream. Returns `true` if a frame was drawn.
@discardableResult
public func update() -> Bool {
streamAPI.pointee.update.unsafelyUnwrapped(pointer)
}
/// The number of video frames currently buffered.
public var bufferedFrameCount: Int {
Int(streamAPI.pointee.getBufferedFrameCount.unsafelyUnwrapped(pointer))
}
/// The total number of bytes read from the source.
public var bytesRead: UInt32 {
streamAPI.pointee.getBytesRead.unsafelyUnwrapped(pointer)
}
}
}
@@ -0,0 +1,74 @@
internal import CPlaydate
private var tilemapAPI: UnsafePointer<playdate_tilemap> { Playdate.tilemapAPI.unsafelyUnwrapped }
extension Graphics {
/// A grid of tiles drawn from a bitmap table. Wraps `LCDTileMap`.
public final class TileMap {
let pointer: OpaquePointer
/// The image table is retained so the tilemap's tiles stay valid.
private var retainedImageTable: BitmapTable?
public init() {
pointer = tilemapAPI.pointee.newTilemap.unsafelyUnwrapped().unsafelyUnwrapped
}
deinit {
tilemapAPI.pointee.freeTilemap.unsafelyUnwrapped(pointer)
}
/// The bitmap table the tile indexes refer to.
public var imageTable: BitmapTable? {
get { retainedImageTable }
set {
retainedImageTable = newValue
tilemapAPI.pointee.setImageTable.unsafelyUnwrapped(pointer, newValue?.pointer)
}
}
/// Sets the tilemap's size in tiles.
public func setSize(tilesWide: Int, tilesHigh: Int) {
tilemapAPI.pointee.setSize.unsafelyUnwrapped(pointer, Int32(tilesWide), Int32(tilesHigh))
}
/// The tilemap's size in tiles.
public var size: (tilesWide: Int, tilesHigh: Int) {
var wide: Int32 = 0, high: Int32 = 0
tilemapAPI.pointee.getSize.unsafelyUnwrapped(pointer, &wide, &high)
return (Int(wide), Int(high))
}
/// The tilemap's total size in pixels.
public var pixelSize: (width: Int, height: Int) {
var width: UInt32 = 0, height: UInt32 = 0
tilemapAPI.pointee.getPixelSize.unsafelyUnwrapped(pointer, &width, &height)
return (Int(width), Int(height))
}
/// Fills the tilemap with `indexes`, `rowWidth` tiles per row. The
/// tilemap is resized to fit.
public func setTiles(_ indexes: [UInt16], rowWidth: Int) {
var indexes = indexes
indexes.withUnsafeMutableBufferPointer { buffer in
tilemapAPI.pointee.setTiles.unsafelyUnwrapped(pointer, buffer.baseAddress,
Int32(buffer.count), Int32(rowWidth))
}
}
/// Sets the tile index at position (x, y).
public func setTile(x: Int, y: Int, index: UInt16) {
tilemapAPI.pointee.setTileAtPosition.unsafelyUnwrapped(pointer, Int32(x), Int32(y), index)
}
/// The tile index at position (x, y), or `nil` if out of bounds.
public func tile(x: Int, y: Int) -> Int? {
let index = tilemapAPI.pointee.getTileAtPosition.unsafelyUnwrapped(pointer, Int32(x), Int32(y))
return index < 0 ? nil : Int(index)
}
/// Draws the tilemap with its upper-left corner at (x, y).
public func draw(x: Float, y: Float) {
tilemapAPI.pointee.drawAtPoint.unsafelyUnwrapped(pointer, x, y)
}
}
}
@@ -0,0 +1,77 @@
internal import CPlaydate
private var videoAPI: UnsafePointer<playdate_video> { Playdate.videoAPI.unsafelyUnwrapped }
extension Graphics {
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
public final class VideoPlayer {
let pointer: OpaquePointer
let isOwned: Bool
/// Retains the render context bitmap while the player uses it.
private var retainedContext: Bitmap?
init(pointer: OpaquePointer, isOwned: Bool) {
self.pointer = pointer
self.isOwned = isOwned
}
/// Opens the .pdv file at `path`.
public convenience init(path: String) throws(PlaydateError) {
let pointer = path.withPlaydateCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) }
guard let pointer else {
throw PlaydateError(message: "unable to load video: \(path)")
}
self.init(pointer: pointer, isOwned: true)
}
deinit {
if isOwned {
videoAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
}
/// Sets the bitmap the video renders into.
public func setContext(_ context: Bitmap) throws(PlaydateError) {
guard videoAPI.pointee.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
throw PlaydateError(message: error ?? "unable to set video context")
}
retainedContext = context
}
/// The bitmap the video renders into.
public var context: Bitmap? {
guard let context = videoAPI.pointee.getContext.unsafelyUnwrapped(pointer) else { return nil }
return Bitmap(pointer: context, isOwned: false)
}
/// Renders directly into the display framebuffer.
public func useScreenContext() {
retainedContext = nil
videoAPI.pointee.useScreenContext.unsafelyUnwrapped(pointer)
}
/// Renders frame `frame` into the current context.
public func renderFrame(_ frame: Int) throws(PlaydateError) {
guard videoAPI.pointee.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
// Static message: the caller knows the frame it passed, and
// interpolating it would pull integer formatting machinery
// into the device binary.
throw PlaydateError(message: error ?? "unable to render frame")
}
}
/// The most recent error message, if any.
public var error: String? {
String(playdateCString: videoAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The video's dimensions, frame rate, frame count, and current frame.
public var info: (width: Int, height: Int, frameRate: Float, frameCount: Int, currentFrame: Int) {
var width: Int32 = 0, height: Int32 = 0, frameCount: Int32 = 0, currentFrame: Int32 = 0
var frameRate: Float = 0
videoAPI.pointee.getInfo.unsafelyUnwrapped(pointer, &width, &height, &frameRate,
&frameCount, &currentFrame)
return (Int(width), Int(height), frameRate, Int(frameCount), Int(currentFrame))
}
}
}