Swift 6.4 migration and exhancements (#1)

This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it.

Reviewed-on: #1
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-09-18 13:15:08 +00:00
committed by javier
parent d0a561b91f
commit c5037dd716
105 changed files with 1390 additions and 1397 deletions
@@ -1,21 +1,21 @@
internal import CPlaydate
extension Graphics {
/// An image that can be drawn to the screen or used as a drawing target.
/// Wraps `LCDBitmap`.
/// A drawable image and drawing target. Wraps `LCDBitmap`. Bitmaps borrowed from
/// tables, fonts, video players, or the system live only as long as their owner.
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.
/// Whether deinit frees the `LCDBitmap`.
let isOwned: Bool
/// Kept alive because this bitmap shares its pixels.
private let owner: Bitmap?
init(pointer: OpaquePointer, isOwned: Bool) {
init(pointer: OpaquePointer, isOwned: Bool, owner: Bitmap? = nil) {
self.pointer = pointer
self.isOwned = isOwned
self.owner = owner
}
/// 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)
@@ -23,10 +23,10 @@ extension Graphics {
self.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
}
/// Loads a bitmap from a file in the game's pdx or Data directory.
/// `path` is 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) }
let pointer = path.withCString { gfx.pointee.loadBitmap.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer, isOwned: true)
}
@@ -37,62 +37,85 @@ extension Graphics {
}
}
// MARK: Properties
// MARK: Size and pixel data
/// The bitmap's dimensions, row stride, and raw storage.
public var data: Data {
let raw = rawData()
return Data(width: raw.width, height: raw.height, rowBytes: raw.rowBytes,
hasMask: raw.mask != nil)
}
/// 1 bit per pixel, MSB first, `height` rows of `rowBytes` bytes. The span is valid
/// only inside `body`, and empty if the bitmap has no data.
public func withPixelData<Result, Failure: Error>(
_ body: (inout MutableSpan<UInt8>) throws(Failure) -> Result
) throws(Failure) -> Result {
let raw = rawData()
var span = UnsafeMutableBufferPointer(
start: raw.data, count: raw.data == nil ? 0 : raw.height * raw.rowBytes).mutableSpan
return try body(&span)
}
/// Laid out like the pixel data; valid only inside `body`. Returns `nil` without
/// calling `body` if the bitmap has no mask.
public func withMaskData<Result, Failure: Error>(
_ body: (inout MutableSpan<UInt8>) throws(Failure) -> Result
) throws(Failure) -> Result? {
let raw = rawData()
guard let mask = raw.mask else { return nil }
var span = UnsafeMutableBufferPointer(start: mask, count: raw.height * raw.rowBytes).mutableSpan
return try body(&span)
}
private func rawData() -> (width: Int, height: Int, rowBytes: Int,
mask: UnsafeMutablePointer<UInt8>?, data: UnsafeMutablePointer<UInt8>?) {
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)
return (Int(width), Int(height), Int(rowBytes), mask, 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.
/// Saves a `getBitmapData` call per access. Reset by `load(path:)`, the only resizer.
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)
let raw = rawData()
let size = (raw.width, raw.height)
cachedSize = size
return size
}
/// The bitmap's width, in pixels.
/// Width, in pixels.
public var width: Int { size.width }
/// The bitmap's height, in pixels.
/// Height, in pixels.
public var height: Int { size.height }
/// The color of the pixel at (x, y).
/// `.black` or `.white`, or `.clear` if out of bounds or masked out.
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`.
/// Replaces the contents, and possibly the size, 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) }
path.withCString { 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) }
}
/// Returns a new copy of the bitmap.
public func copy() -> Bitmap {
Bitmap(pointer: gfx.pointee.copyBitmap.unsafelyUnwrapped(pointer).unsafelyUnwrapped, isOwned: true)
}
/// Returns a new bitmap rotated by `degrees` (clockwise) and scaled.
/// `degrees` is clockwise. Returns `nil` on failure.
public func rotated(by degrees: Float, xScale: Float = 1, yScale: Float = 1) -> Bitmap? {
var allocatedSize: Int32 = 0
guard let rotated = gfx.pointee.rotatedBitmap.unsafelyUnwrapped(
@@ -100,21 +123,21 @@ extension Graphics {
return Bitmap(pointer: rotated, isOwned: true)
}
/// Sets a mask image. The mask must match the bitmap's dimensions.
/// Returns `false` if `mask` is `nil` or a different size.
@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.
/// Shares this bitmap's mask data, and keeps this bitmap alive.
public var mask: Bitmap? {
// Owned by the caller; pixels are shared with `self`.
guard let mask = gfx.pointee.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
return Bitmap(pointer: mask, isOwned: false)
return Bitmap(pointer: mask, isOwned: true, owner: self)
}
/// Tests whether the opaque pixels of two bitmaps overlap within
/// `rect`, given each bitmap's position and flip.
/// Whether opaque pixels of both bitmaps overlap within the non-empty `rect`.
/// `false` if either bitmap lies entirely outside `rect`.
public func checkMaskCollision(x: Int, y: Int, flip: BitmapFlip = .unflipped,
other: Bitmap, otherX: Int, otherY: Int,
otherFlip: BitmapFlip = .unflipped,
@@ -127,19 +150,18 @@ extension Graphics {
// MARK: Drawing
/// Draws the bitmap with its upper-left corner at (x, y).
/// (x, y) is the upper-left corner.
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).
/// (x, y) is the upper-left corner. Negative scales flip the bitmap.
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.
/// Scales, then rotates, placing the anchor (`centerX`, `centerY`) at (x, y). Anchors
/// are proportional: (0.5, 0.5) is the center, (0, 0) the unrotated upper-left.
public func drawRotated(x: Int, y: Int, degrees: Float,
centerX: Float = 0.5, centerY: Float = 0.5,
xScale: Float = 1, yScale: Float = 1) {
@@ -147,7 +169,7 @@ extension Graphics {
centerX, centerY, xScale, yScale)
}
/// Tiles the bitmap over the given area.
/// Tiles the `width` × `height` rect whose upper-left corner is (x, y).
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)
@@ -1,7 +1,8 @@
internal import CPlaydate
extension Graphics {
/// A collection of bitmaps loaded from an image table. Wraps `LCDBitmapTable`.
/// An image table. Wraps `LCDBitmapTable`. Its bitmaps are borrowed and invalid once
/// the table is freed.
public final class BitmapTable {
let pointer: OpaquePointer
@@ -9,16 +10,15 @@ extension Graphics {
self.pointer = pointer
}
/// Allocates a table with room for `count` bitmaps of the given size.
/// Room for `count` bitmaps of `width` × `height` pixels.
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) }
let pointer = path.withCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) }
guard let pointer else { throw PlaydateError(cString: error) }
self.init(pointer: pointer)
}
@@ -27,16 +27,13 @@ extension Graphics {
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) }
path.withCString { 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.
/// `nil` if out of range.
public func bitmap(at index: Int) -> Bitmap? {
guard let bitmap = gfx.pointee.getTableBitmap.unsafelyUnwrapped(pointer, Int32(index)) else {
return nil
@@ -44,15 +41,13 @@ extension Graphics {
return Bitmap(pointer: bitmap, isOwned: false)
}
/// The number of bitmaps in the table and the number of cells per row
/// of the source image.
/// Bitmap count and cells across 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))
}
/// The number of bitmaps in the table.
public var count: Int { info.count }
}
}
@@ -61,8 +56,7 @@ 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.
/// Traps if out of range.
public subscript(position: Int) -> Graphics.Bitmap {
guard let bitmap = bitmap(at: position) else {
preconditionFailure("bitmap table index out of range")
+20 -21
View File
@@ -1,11 +1,11 @@
internal import CPlaydate
extension Graphics {
/// A font loaded from a .pft file. Wraps `LCDFont`.
/// A .pft bitmap font. Wraps `LCDFont`. Glyph bitmaps are borrowed and don't retain
/// the font; keep it alive while using them.
public final class Font {
let pointer: OpaquePointer
/// Fonts created from in-memory data reference that data; it is kept
/// alive here.
/// `makeFontFromData` doesn't copy its buffer, so it lives as long as the font.
private let retainedData: UnsafeRawPointer?
init(pointer: OpaquePointer, retainedData: UnsafeRawPointer? = nil) {
@@ -13,19 +13,20 @@ extension Graphics {
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) }
let pointer = path.withCString { 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) {
/// `data`: an uncompressed .pft file minus its 16-byte header; copied for the font's
/// lifetime. `wide` must match the header flag for glyphs above U+1FFFF.
public convenience init?(data: Span<UInt8>, wide: Bool = false) {
let copy = UnsafeMutableRawPointer.allocate(byteCount: data.count, alignment: 4)
copy.copyMemory(from: data.baseAddress.unsafelyUnwrapped, byteCount: data.count)
data.withUnsafeBytes { bytes in
copy.copyMemory(from: bytes.baseAddress.unsafelyUnwrapped, byteCount: bytes.count)
}
let fontData = OpaquePointer(copy)
guard let pointer = gfx.pointee.makeFontFromData.unsafelyUnwrapped(
fontData, wide ? 1 : 0, Int32(data.count)) else {
@@ -36,43 +37,41 @@ extension Graphics {
}
deinit {
// Per the C API docs, fonts are freed with the system allocator.
// There is no freeFont; fonts are released with `realloc(font, 0)`.
System.systemFree(UnsafeMutableRawPointer(pointer))
retainedData?.deallocate()
}
/// The font's glyph height in pixels.
/// Height, in pixels.
public var height: Int {
Int(gfx.pointee.getFontHeight.unsafelyUnwrapped(pointer))
}
/// The width of `text` when drawn with this font.
/// Width in pixels; `tracking` is pixels between characters.
public func textWidth(_ text: String, tracking: Int = 0) -> Int {
text.withPlaydateUTF8 { bytes, count in
Int(gfx.pointee.getTextWidth.unsafelyUnwrapped(pointer, bytes, count,
text.withCString { cString in
Int(gfx.pointee.getTextWidth.unsafelyUnwrapped(pointer, cString, text.utf8.count,
kUTF8Encoding, Int32(tracking)))
}
}
/// The height of `text` when wrapped to `maxWidth` with this font.
/// Height in pixels of `text` wrapped to `maxWidth` pixels.
public func textHeight(_ text: String, maxWidth: Int, wrap: TextWrappingMode = .word,
tracking: Int = 0, extraLeading: Int = 0) -> Int {
text.withPlaydateUTF8 { bytes, count in
text.withCString { cString in
Int(gfx.pointee.getTextHeightForMaxWidth.unsafelyUnwrapped(
pointer, bytes, count, Int32(maxWidth), kUTF8Encoding,
pointer, cString, text.utf8.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.
/// `nil` if none. Codepoints differing only in their low 8 bits share a page.
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.
/// `nil` if the font has no glyph for `codepoint`.
public func glyph(for codepoint: UInt32) -> (glyph: Glyph, bitmap: Bitmap?, advance: Int)? {
var bitmap: OpaquePointer?
var advance: Int32 = 0
@@ -1,15 +1,16 @@
internal import CPlaydate
/// The cached `playdate->graphics->videostream` C API table.
/// `playdate->graphics->videostream`.
private var streamAPI: UnsafePointer<playdate_videostream> { Playdate.videoStreamAPI.unsafelyUnwrapped }
extension Graphics {
/// Streams video (and audio) from a file or network connection.
/// Wraps `LCDStreamPlayer`.
/// Streams video and audio from a file or connection. Wraps `LCDStreamPlayer`.
/// Retains its source until replaced.
public final class StreamPlayer {
let pointer: OpaquePointer
/// Retains the active source so it outlives the stream.
/// The C player reads the source; non-copyable `File.Handle` needs its own slot.
private var retainedSource: AnyObject?
private var retainedFile: File.Handle?
public init() {
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
@@ -19,32 +20,32 @@ extension Graphics {
streamAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
/// Sets the sizes of the stream's video and audio buffers, in bytes.
/// Buffer sizes, 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
/// Takes ownership; the handle closes when replaced or on deinit.
public func setFile(_ file: consuming File.Handle) {
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
retainedFile = consume file
retainedSource = nil
}
/// 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)
retainedFile = nil
}
/// 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 func setTCPConnection(_ connection: Network.TCPConnection) {
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
retainedSource = connection
retainedFile = nil
}
/// Borrowed. The same wrapper is returned while the underlying player is unchanged,
/// so callbacks registered on it persist.
public var filePlayer: Sound.FilePlayer? {
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
if let cached = cachedFilePlayer, cached.pointer == player {
@@ -57,24 +58,23 @@ extension Graphics {
private var cachedFilePlayer: Sound.FilePlayer?
/// The player used for the stream's video track. Owned by the stream.
/// Borrowed; keep this player alive while using it.
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.
/// 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.
/// Bytes read from the source so far.
public var bytesRead: UInt32 {
streamAPI.pointee.getBytesRead.unsafelyUnwrapped(pointer)
}
@@ -1,13 +1,13 @@
internal import CPlaydate
/// The cached `playdate->graphics->tilemap` C API table.
/// `playdate->graphics->tilemap`.
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.
/// The C tilemap holds only a raw pointer to its table.
private var retainedImageTable: BitmapTable?
public init() {
@@ -18,7 +18,7 @@ extension Graphics {
tilemapAPI.pointee.freeTilemap.unsafelyUnwrapped(pointer)
}
/// The bitmap table the tile indexes refer to.
/// Retained while set.
public var imageTable: BitmapTable? {
get { retainedImageTable }
set {
@@ -27,47 +27,52 @@ extension Graphics {
}
}
/// 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.
/// Tile image size times tile counts.
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 all tiles row by row, resizing to `rowWidth` × `indexes.count / rowWidth`.
/// `indexes.count` must be a multiple of `rowWidth`.
public func setTiles(_ indexes: Span<UInt16>, rowWidth: Int) {
indexes.withUnsafeBufferPointer { buffer in
// Non-const in C, but only read (and copied).
tilemapAPI.pointee.setTiles.unsafelyUnwrapped(
pointer, UnsafeMutablePointer(mutating: buffer.baseAddress),
Int32(buffer.count), Int32(rowWidth))
}
}
/// Sets the tile index at position (x, y).
/// Sets all tiles row by row, resizing to `rowWidth` × `indexes.count / rowWidth`.
/// `indexes.count` must be a multiple of `rowWidth`.
public func setTiles(_ indexes: [UInt16], rowWidth: Int) {
indexes.withUnsafeBufferPointer { setTiles($0.span, rowWidth: rowWidth) }
}
/// `x` is the column, `y` the row, `index` an image table index.
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.
/// The image table index at column `x`, row `y`; `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).
/// (x, y) is the upper-left corner, in pixels.
public func draw(x: Float, y: Float) {
tilemapAPI.pointee.drawAtPoint.unsafelyUnwrapped(pointer, x, y)
}
@@ -1,14 +1,15 @@
internal import CPlaydate
/// The cached `playdate->graphics->video` C API table.
/// `playdate->graphics->video`.
private var videoAPI: UnsafePointer<playdate_video> { Playdate.videoAPI.unsafelyUnwrapped }
extension Graphics {
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
public final class VideoPlayer {
let pointer: OpaquePointer
/// `false` for players vended by a `StreamPlayer`.
let isOwned: Bool
/// Retains the render context bitmap while the player uses it.
/// The C player holds only a raw pointer to its context.
private var retainedContext: Bitmap?
init(pointer: OpaquePointer, isOwned: Bool) {
@@ -16,9 +17,8 @@ extension Graphics {
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) }
let pointer = path.withCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) }
guard let pointer else {
throw PlaydateError(message: "unable to load video: \(path)")
}
@@ -31,7 +31,7 @@ extension Graphics {
}
}
/// Sets the bitmap the video renders into.
/// Retains `context`; throws with `error`. Its mask isn't drawn; use an opaque one.
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")
@@ -39,34 +39,32 @@ extension Graphics {
retainedContext = context
}
/// The bitmap the video renders into.
/// Borrowed. If none was set, the player allocates one the size of the video.
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.
/// Releases any retained context.
public func useScreenContext() {
retainedContext = nil
videoAPI.pointee.useScreenContext.unsafelyUnwrapped(pointer)
}
/// Renders frame `frame` into the current context.
/// Renders into the current context; throws with `error`.
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.
// Static: interpolating `frame` would link integer formatting.
throw PlaydateError(message: error ?? "unable to render frame")
}
}
/// The most recent error message, if any.
/// The most recent error message.
public var error: String? {
String(playdateCString: videoAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The video's dimensions, frame rate, frame count, and current frame.
/// Size in pixels, frame rate in frames per second, frame count, 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
@@ -1,7 +1,7 @@
internal import CPlaydate
extension Graphics {
/// Mirroring applied when drawing a bitmap.
/// Mirroring applied when drawing a bitmap. Wraps `LCDBitmapFlip`.
public enum BitmapFlip: UInt32, Sendable {
case unflipped = 0
case flippedX = 1
@@ -1,22 +1,17 @@
internal import CPlaydate
extension Graphics {
/// A drawing color: solid or an 8×8 pattern.
/// Wraps `LCDColor`: a solid color or an 8×8 pattern.
public enum Color: Sendable {
/// Solid black.
case black
/// Solid white.
case white
/// Transparent; leaves the destination unchanged.
/// Leaves the destination unchanged.
case clear
/// Inverts the destination pixels.
/// Inverts the destination.
case xor
/// An 8×8 two-color pattern.
case pattern(Pattern)
/// Materializes the `LCDColor` for the duration of `body`. Pattern
/// colors pass a pointer to a temporary, so the value must not be
/// stored beyond the call.
/// For `.pattern`, the `LCDColor` points to a copy valid only during `body`.
func withLCDColor<Result>(_ body: (LCDColor) -> Result) -> Result {
switch self {
case .black: return body(LCDColor(kColorBlack.rawValue))
@@ -1,23 +1,19 @@
internal import CPlaydate
extension Graphics {
/// How source pixels combine with the destination when drawing.
/// Wraps `LCDBitmapDrawMode`: how bitmap and text pixels combine with the destination.
public enum DrawMode: UInt32, Sendable {
/// Source pixels replace the destination.
case copy = 0
/// White source pixels are treated as transparent.
/// White source pixels are transparent.
case whiteTransparent = 1
/// Black source pixels are treated as transparent.
/// Black source pixels are transparent.
case blackTransparent = 2
/// Opaque source pixels draw white.
case fillWhite = 3
/// Opaque source pixels draw black.
case fillBlack = 4
/// Source pixels are XORed with the destination.
case xor = 5
/// The inverse of `xor`.
case nxor = 6
/// Source pixels draw inverted.
case inverted = 7
init(_ mode: LCDBitmapDrawMode) { self = DrawMode(rawValue: UInt32(mode.rawValue)) ?? .copy }
@@ -1,9 +1,11 @@
internal import CPlaydate
extension Graphics {
/// The end cap style used when drawing lines.
/// Line end caps. Wraps `LCDLineCapStyle`.
public enum LineCapStyle: UInt32, Sendable {
/// Flat, ending at the endpoint.
case butt = 0
/// Square, extending past the endpoint.
case square = 1
case round = 2
@@ -1,9 +1,11 @@
internal import CPlaydate
extension Graphics {
/// The winding rule used by `fillPolygon`.
/// Winding rule for `fillPolygon(points:color:fillRule:)`. Wraps `LCDPolygonFillRule`.
public enum PolygonFillRule: UInt32, Sendable {
/// Fills points with a nonzero winding number.
case nonZero = 0
/// Fills points crossed by an odd number of edges.
case evenOdd = 1
var cValue: LCDPolygonFillRule { LCDPolygonFillRule(LCDPolygonFillRule.RawValue(rawValue)) }
@@ -1,11 +1,13 @@
internal import CPlaydate
extension Graphics {
/// A solid color, for APIs that cannot take a pattern.
/// A color for APIs that cannot take a pattern. Wraps `LCDSolidColor`.
public enum SolidColor: UInt32, Sendable {
case black = 0
case white = 1
/// Transparent.
case clear = 2
/// Inverts the destination.
case xor = 3
init(_ color: LCDSolidColor) { self = SolidColor(rawValue: UInt32(color.rawValue)) ?? .clear }
@@ -1,10 +1,12 @@
internal import CPlaydate
extension Graphics {
/// The encoding of text passed to the text functions.
/// Text encoding for the C text functions. Wraps `PDStringEncoding`.
/// The Swift text wrappers always pass UTF-8.
public enum StringEncoding: UInt32, Sendable {
case ascii = 0
case utf8 = 1
/// UTF-16, little-endian.
case utf16LittleEndian = 2
var cValue: PDStringEncoding { PDStringEncoding(PDStringEncoding.RawValue(rawValue)) }
@@ -1,7 +1,7 @@
internal import CPlaydate
extension Graphics {
/// Horizontal alignment for `drawText(in:)`.
/// Alignment for the rect-bounded `drawText` overloads. Wraps `PDTextAlignment`.
public enum TextAlignment: UInt32, Sendable {
case left = 0
case center = 1
@@ -1,8 +1,10 @@
internal import CPlaydate
extension Graphics {
/// How text wraps in `drawText(in:)`.
/// Wrapping for the rect-bounded `drawText` overloads and `Font.textHeight`.
/// Wraps `PDTextWrappingMode`.
public enum TextWrappingMode: UInt32, Sendable {
/// No wrapping; text past the edge is clipped.
case clip = 0
case character = 1
case word = 2
+62 -70
View File
@@ -3,102 +3,95 @@ internal import CPlaydate
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
public enum Graphics {}
/// The cached `playdate->graphics` C API table.
/// `playdate->graphics`.
var gfx: UnsafePointer<playdate_graphics> { Playdate.graphicsAPI.unsafelyUnwrapped }
extension Graphics {
// MARK: - Screen constants
/// The width of the screen in pixels (`LCD_COLUMNS`).
/// Screen width, in pixels (`LCD_COLUMNS`).
public static let columns = 400
/// The height of the screen in pixels (`LCD_ROWS`).
/// Screen height, in pixels (`LCD_ROWS`).
public static let rows = 240
/// The stride of a framebuffer row in bytes (`LCD_ROWSIZE`).
/// Framebuffer row stride, in bytes (`LCD_ROWSIZE`).
public static let rowSize = 52
// MARK: - Drawing state
/// Clears the entire display, filling it with `color`.
public static func clear(color: Color = .white) {
color.withLCDColor { gfx.pointee.clear.unsafelyUnwrapped($0) }
}
/// Sets the background color shown when the display is offset or for
/// clear pixels in drawn images.
/// Shown where the display is offset; clears dirty areas in the sprite system.
public static func setBackgroundColor(_ color: SolidColor) {
gfx.pointee.setBackgroundColor.unsafelyUnwrapped(color.cValue)
}
/// Sets the mode that determines how source pixels combine with the
/// destination. Returns the previous mode.
/// Applies to bitmaps, and so text. Returns the previous mode.
@discardableResult
public static func setDrawMode(_ mode: DrawMode) -> DrawMode {
DrawMode(gfx.pointee.setDrawMode.unsafelyUnwrapped(mode.cValue))
}
/// Offsets all subsequent drawing by (dx, dy).
/// Offsets subsequent drawing by (`dx`, `dy`) pixels; may be negative.
public static func setDrawOffset(dx: Int, dy: Int) {
gfx.pointee.setDrawOffset.unsafelyUnwrapped(Int32(dx), Int32(dy))
}
/// Sets the clip rect in world coordinates (affected by the draw offset).
/// In world coordinates (translated by the draw offset). Cleared each update.
public static func setClipRect(x: Int, y: Int, width: Int, height: Int) {
gfx.pointee.setClipRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height))
}
/// Sets the clip rect in world coordinates (affected by the draw offset).
/// In world coordinates (translated by the draw offset). Cleared each update.
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).
/// In screen coordinates (ignoring 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).
/// In screen coordinates (ignoring 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()
}
/// Sets the end cap style used by subsequent line drawing.
public static func setLineCapStyle(_ style: LineCapStyle) {
gfx.pointee.setLineCapStyle.unsafelyUnwrapped(style.cValue)
}
/// Sets the stencil applied to subsequent drawing. If `tile` is `true`
/// the stencil image is tiled, and its width must be a multiple of 32.
/// Pass `nil` to clear the stencil.
/// Pixels draw only where the stencil is white; `nil` clears it. A tiled stencil's
/// width must be a multiple of 32. Not retained; keep it alive while set.
public static func setStencil(_ image: Bitmap?, tile: Bool = false) {
gfx.pointee.setStencilImage.unsafelyUnwrapped(image?.pointer, tile ? 1 : 0)
}
/// Pushes a new drawing context targeting `target`, or the display if
/// `target` is `nil`.
/// `nil` targets the display framebuffer. Not retained; keep `target` alive until
/// the matching `popContext()`.
public static func pushContext(_ target: Bitmap? = nil) {
gfx.pointee.pushContext.unsafelyUnwrapped(target?.pointer)
}
/// Pops the top drawing context off the stack.
/// Restores the previous context's drawing settings. No-op if none.
public static func popContext() {
gfx.pointee.popContext.unsafelyUnwrapped()
}
// MARK: - Shapes
/// Draws a line from (x1, y1) to (x2, y2) with the given stroke width.
/// `width` is in pixels.
public static func drawLine(x1: Int, y1: Int, x2: Int, y2: Int, width: Int, color: Color) {
color.withLCDColor {
gfx.pointee.drawLine.unsafelyUnwrapped(Int32(x1), Int32(y1), Int32(x2), Int32(y2), Int32(width), $0)
}
}
/// Fills the triangle with vertices (x1, y1), (x2, y2), and (x3, y3).
public static func fillTriangle(x1: Int, y1: Int, x2: Int, y2: Int, x3: Int, y3: Int, color: Color) {
color.withLCDColor {
gfx.pointee.fillTriangle.unsafelyUnwrapped(Int32(x1), Int32(y1), Int32(x2), Int32(y2),
@@ -106,32 +99,29 @@ extension Graphics {
}
}
/// Draws the outline of a rectangle, stroked inside its frame.
/// Stroked inside its frame.
public static func drawRect(x: Int, y: Int, width: Int, height: Int, color: Color) {
color.withLCDColor {
gfx.pointee.drawRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height), $0)
}
}
/// Draws the outline of a rectangle, stroked inside its frame.
/// 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 {
gfx.pointee.fillRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height), $0)
}
}
/// 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`.
/// Stroked inside the rect. `radius` and `lineWidth` are in pixels.
public static func drawRoundRect(x: Int, y: Int, width: Int, height: Int, radius: Int,
lineWidth: Int, color: Color) {
color.withLCDColor {
@@ -140,14 +130,13 @@ extension Graphics {
}
}
/// Draws the outline of a rectangle with rounded corners, stroked with
/// `lineWidth`.
/// Stroked inside the rect. `radius` and `lineWidth` are in pixels.
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.
/// `radius` is in pixels.
public static func fillRoundRect(x: Int, y: Int, width: Int, height: Int, radius: Int, color: Color) {
color.withLCDColor {
gfx.pointee.fillRoundRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height),
@@ -155,14 +144,14 @@ extension Graphics {
}
}
/// Fills a rectangle with rounded corners.
/// `radius` is in pixels.
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).
/// Stroked inside the rect. Differing angles draw only that arc (degrees clockwise
/// from the top).
public static func drawEllipse(x: Int, y: Int, width: Int, height: Int, lineWidth: Int,
startAngle: Float = 0, endAngle: Float = 0, color: Color) {
color.withLCDColor {
@@ -171,8 +160,7 @@ extension Graphics {
}
}
/// Fills an ellipse inside the rect. If the angles differ, fills the
/// wedge from `startAngle` to `endAngle` (clockwise degrees, 0 at top).
/// Differing angles fill only that wedge (degrees clockwise from the top).
public static func fillEllipse(x: Int, y: Int, width: Int, height: Int,
startAngle: Float = 0, endAngle: Float = 0, color: Color) {
color.withLCDColor {
@@ -181,24 +169,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).
/// Stroked inside the rect. Differing angles draw only that arc (degrees clockwise
/// from the 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).
/// Differing angles fill only that wedge (degrees clockwise from the 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.
/// The last point connects back to the first.
public static func fillPolygon(points: [(x: Int, y: Int)], color: Color,
fillRule: PolygonFillRule = .nonZero) {
withUnsafeTemporaryAllocation(of: Int32.self, capacity: points.count * 2) { coordinates in
@@ -215,12 +201,12 @@ extension Graphics {
}
}
/// Sets the pixel at (x, y) in the current drawing context.
/// Slow in bulk; prefer bitmaps or framebuffer writes for many pixels.
public static func setPixel(x: Int, y: Int, color: Color) {
color.withLCDColor { gfx.pointee.setPixel.unsafelyUnwrapped(Int32(x), Int32(y), $0) }
}
/// Reads an 8×8 pattern from the bitmap starting at (x, y).
/// The 8×8 pattern whose upper-left corner is (x, y) in `bitmap`.
public static func colorToPattern(from bitmap: Bitmap, x: Int, y: Int) -> Pattern {
var color: LCDColor = 0
gfx.pointee.setColorToPattern.unsafelyUnwrapped(&color, bitmap.pointer, Int32(x), Int32(y))
@@ -235,88 +221,94 @@ extension Graphics {
// MARK: - Text
/// Draws `text` at (x, y) using the current font. Returns the drawn width.
/// Uses the current font, or the system font if none is set. Returns the drawn width.
@discardableResult
public static func drawText(_ text: String, x: Int, y: Int) -> Int {
text.withPlaydateUTF8 { bytes, count in
Int(gfx.pointee.drawText.unsafelyUnwrapped(bytes, count,
text.withCString { cString in
Int(gfx.pointee.drawText.unsafelyUnwrapped(cString, text.utf8.count,
kUTF8Encoding, Int32(x), Int32(y)))
}
}
/// Draws `text` wrapped and aligned inside the given rectangle.
/// Wrapped and aligned inside the rect, with the current font.
public static func drawText(_ text: String, x: Int, y: Int, width: Int, height: Int,
wrap: TextWrappingMode = .word, align: TextAlignment = .left) {
text.withPlaydateUTF8 { bytes, count in
gfx.pointee.drawTextInRect.unsafelyUnwrapped(bytes, count, kUTF8Encoding,
text.withCString { cString in
gfx.pointee.drawTextInRect.unsafelyUnwrapped(cString, text.utf8.count, kUTF8Encoding,
Int32(x), Int32(y), Int32(width), Int32(height),
wrap.cValue, align.cValue)
}
}
/// Draws `text` wrapped and aligned inside the given rectangle.
/// Wrapped and aligned inside the rect, with the current font.
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.
/// Not retained; keep `font` alive while set.
public static func setFont(_ font: Font) {
gfx.pointee.setFont.unsafelyUnwrapped(font.pointer)
}
/// Extra space added between letters, in pixels.
/// Extra space between letters, in pixels.
public static var textTracking: Int {
get { Int(gfx.pointee.getTextTracking.unsafelyUnwrapped()) }
set { gfx.pointee.setTextTracking.unsafelyUnwrapped(Int32(newValue)) }
}
/// Adjusts the line height used when drawing multi-line text.
/// Pixels added to the font's own leading for multi-line text.
public static func setTextLeading(_ lineHeightAdjustment: Int) {
gfx.pointee.setTextLeading.unsafelyUnwrapped(Int32(lineHeightAdjustment))
}
// MARK: - Framebuffer
/// The current working framebuffer. Rows are `rowSize` bytes.
/// Call `markUpdatedRows(from:to:)` after writing directly.
public static var frame: UnsafeMutablePointer<UInt8>? {
gfx.pointee.getFrame.unsafelyUnwrapped()
/// The working framebuffer: `rows` rows of `rowSize` bytes, 1 bit per pixel, MSB first,
/// last 2 bytes of each row unused. The span is valid only inside `body`; `nil` if
/// there is no framebuffer. Call `markUpdatedRows(from:to:)` after writing.
public static func withFrame<Result, Failure: Error>(
_ body: (inout MutableSpan<UInt8>) throws(Failure) -> Result
) throws(Failure) -> Result? {
guard let frame = gfx.pointee.getFrame.unsafelyUnwrapped() else { return nil }
var span = UnsafeMutableBufferPointer(start: frame, count: rows * rowSize).mutableSpan
return try body(&span)
}
/// The framebuffer currently shown on the display. Rows are `rowSize` bytes.
public static var displayFrame: UnsafeMutablePointer<UInt8>? {
gfx.pointee.getDisplayFrame.unsafelyUnwrapped()
/// The last frame shown, laid out like `withFrame(_:)`. The span is valid only inside
/// `body`; `nil` if there is no framebuffer.
public static func withDisplayFrame<Result, Failure: Error>(
_ body: (Span<UInt8>) throws(Failure) -> Result
) throws(Failure) -> Result? {
guard let frame = gfx.pointee.getDisplayFrame.unsafelyUnwrapped() else { return nil }
return try body(UnsafeBufferPointer(start: frame, count: rows * rowSize).span)
}
/// A bitmap view of the display framebuffer. Simulator only; `nil` on device.
/// Simulator only: white pixels overlay the display in translucent red. `nil` on device.
public static var debugBitmap: Bitmap? {
guard let getDebugBitmap = gfx.pointee.getDebugBitmap,
let pointer = getDebugBitmap() else { return nil }
return Bitmap(pointer: pointer, isOwned: false)
}
/// A bitmap referencing the display framebuffer (not a copy).
/// Not a copy; owned by the system.
public static var displayBufferBitmap: Bitmap? {
guard let pointer = gfx.pointee.getDisplayBufferBitmap.unsafelyUnwrapped() else { return nil }
return Bitmap(pointer: pointer, isOwned: false)
}
/// A copy of the working framebuffer as a new bitmap.
public static func copyFrameBufferBitmap() -> Bitmap? {
guard let pointer = gfx.pointee.copyFrameBufferBitmap.unsafelyUnwrapped() else { return nil }
return Bitmap(pointer: pointer, isOwned: true)
}
/// Tells the system which rows (inclusive) were changed by direct
/// framebuffer writes and need redisplay.
/// Marks rows `start`...`end` (inclusive) as changed by direct framebuffer writes.
public static func markUpdatedRows(from start: Int, to end: Int) {
gfx.pointee.markUpdatedRows.unsafelyUnwrapped(Int32(start), Int32(end))
}
/// Manually flushes the framebuffer to the display. Only needed when
/// drawing outside the normal update cycle.
/// Flushes the framebuffer. The system does this after each update.
public static func display() {
gfx.pointee.display.unsafelyUnwrapped()
}
@@ -1,18 +1,12 @@
extension Graphics.Bitmap {
/// The bitmap's dimensions, row stride, and raw pixel/mask storage.
/// The pointers are owned by the bitmap.
public struct Data {
/// The bitmap's width, in pixels.
/// A bitmap's layout. Read pixels with `withPixelData(_:)` and `withMaskData(_:)`.
public struct Data: Sendable {
/// Width, in pixels.
public let width: Int
/// The bitmap's height, in pixels.
/// Height, in pixels.
public let height: Int
/// The stride of one row of pixel data, in bytes.
/// Row stride of the pixel and mask data, in bytes.
public let rowBytes: Int
/// The bitmap's mask data, or `nil` if it has no mask. One bit per
/// pixel; rows are `rowBytes` wide.
public let mask: UnsafeMutablePointer<UInt8>?
/// The bitmap's pixel data. One bit per pixel; rows are `rowBytes`
/// wide.
public let data: UnsafeMutablePointer<UInt8>?
public let hasMask: Bool
}
}
@@ -1,13 +1,12 @@
internal import CPlaydate
extension Graphics {
/// A page of glyphs within a font. Wraps `LCDFontPage`.
/// Keep the font alive while using its pages.
/// A page of 256 glyphs in a font. Wraps `LCDFontPage`. Retains its font.
public struct FontPage {
let pointer: OpaquePointer
let font: Font
/// The glyph for `codepoint` within this page, with its bitmap and advance.
/// `nil` if `codepoint` isn't on this page. The bitmap doesn't retain the font.
public func glyph(for codepoint: UInt32) -> (glyph: Glyph, bitmap: Bitmap?, advance: Int)? {
var bitmap: OpaquePointer?
var advance: Int32 = 0
@@ -1,13 +1,12 @@
internal import CPlaydate
extension Graphics {
/// A single glyph within a font. Wraps `LCDFontGlyph`.
/// Keep the font alive while using its glyphs.
/// A glyph in a font. Wraps `LCDFontGlyph`. Retains its font.
public struct Glyph {
let pointer: OpaquePointer
let font: Font
/// The kerning adjustment between this glyph and the next character.
/// Kerning adjustment between `glyphCode` and `nextCode`.
public func kerning(glyphCode: UInt32, nextCode: UInt32) -> Int {
Int(gfx.pointee.getGlyphKerning.unsafelyUnwrapped(pointer, glyphCode, nextCode))
}
@@ -1,16 +1,15 @@
internal import CPlaydate
extension Graphics {
/// An integer rectangle mirroring `LCDRect`. `right` and `bottom` are
/// not inclusive.
/// A rectangle, in pixels. Mirrors `LCDRect`: `right` and `bottom` are exclusive.
public struct Rect: Sendable {
public var left: Int
/// Exclusive.
public var right: Int
public var top: Int
/// Exclusive.
public var bottom: Int
/// Creates a rect from its edges. `right` and `bottom` are not
/// inclusive.
public init(left: Int, right: Int, top: Int, bottom: Int) {
self.left = left
self.right = right
@@ -18,7 +17,7 @@ extension Graphics {
self.bottom = bottom
}
/// Creates a rect from an origin and size.
/// `(x, y)` is the upper-left corner.
public init(x: Int, y: Int, width: Int, height: Int) {
self.init(left: x, right: x + width, top: y, bottom: y + height)
}
@@ -33,13 +32,10 @@ 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)
}
@@ -1,21 +1,44 @@
extension Graphics {
/// An 8×8 two-color pattern: 8 rows of image data followed by 8 rows of mask.
/// An 8×8 pattern. Mirrors `LCDPattern`: 8 image rows then 8 mask rows,
/// one byte per row, one bit per pixel.
public struct Pattern: Sendable {
/// The pattern's 8 rows of image data followed by 8 rows of mask,
/// one byte per row.
public var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
/// Creates a pattern from 8 rows of image data and 8 rows of mask.
public init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
self.bytes = bytes
}
/// Creates an opaque pattern from 8 rows of image data.
/// An opaque pattern (mask rows all `0xff`).
public init(rows r: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
bytes = (r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
}
}
}
// InlineArray needs macOS 26 on the host; device and Linux are unrestricted.
// Conversions reinterpret the same 16 bytes.
@available(macOS 26, *)
extension Graphics.Pattern {
public init(bytes: [16 of UInt8]) {
self.init(bytes: unsafeBitCast(bytes, to: Bytes.self))
}
/// An opaque pattern (mask rows all `0xff`).
public init(rows: [8 of UInt8]) {
self.init(bytes: [16 of UInt8] { $0 < 8 ? rows[$0] : 0xff })
}
/// `bytes` as an inline array.
public var inlineBytes: [16 of UInt8] {
get { unsafeBitCast(bytes, to: [16 of UInt8].self) }
set { bytes = unsafeBitCast(newValue, to: Bytes.self) }
}
}
extension Graphics.Pattern {
typealias Bytes = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
}