Files
playdate-kit/Sources/PlaydateKit/Graphics/Graphics.swift
T

316 lines
14 KiB
Swift
Raw Normal View History

2026-07-24 10:33:45 +02:00
internal import CPlaydate
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
public enum Graphics {}
2026-07-24 10:33:45 +02:00
2026-09-18 13:15:08 +00:00
/// `playdate->graphics`.
var gfx: UnsafePointer<playdate_graphics> { Playdate.graphicsAPI.unsafelyUnwrapped }
2026-07-24 10:33:45 +02:00
extension Graphics {
2026-07-24 10:33:45 +02:00
// MARK: - Screen constants
2026-09-18 13:15:08 +00:00
/// Screen width, in pixels (`LCD_COLUMNS`).
2026-07-24 10:33:45 +02:00
public static let columns = 400
2026-09-18 13:15:08 +00:00
/// Screen height, in pixels (`LCD_ROWS`).
2026-07-24 10:33:45 +02:00
public static let rows = 240
2026-09-18 13:15:08 +00:00
/// Framebuffer row stride, in bytes (`LCD_ROWSIZE`).
2026-07-24 10:33:45 +02:00
public static let rowSize = 52
// MARK: - Drawing state
public static func clear(color: Color = .white) {
color.withLCDColor { gfx.pointee.clear.unsafelyUnwrapped($0) }
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Shown where the display is offset; clears dirty areas in the sprite system.
2026-07-24 10:33:45 +02:00
public static func setBackgroundColor(_ color: SolidColor) {
gfx.pointee.setBackgroundColor.unsafelyUnwrapped(color.cValue)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Applies to bitmaps, and so text. Returns the previous mode.
2026-07-24 10:33:45 +02:00
@discardableResult
public static func setDrawMode(_ mode: DrawMode) -> DrawMode {
DrawMode(gfx.pointee.setDrawMode.unsafelyUnwrapped(mode.cValue))
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Offsets subsequent drawing by (`dx`, `dy`) pixels; may be negative.
2026-07-24 10:33:45 +02:00
public static func setDrawOffset(dx: Int, dy: Int) {
gfx.pointee.setDrawOffset.unsafelyUnwrapped(Int32(dx), Int32(dy))
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// In world coordinates (translated by the draw offset). Cleared each update.
2026-07-24 10:33:45 +02:00
public static func setClipRect(x: Int, y: Int, width: Int, height: Int) {
gfx.pointee.setClipRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height))
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-09-18 13:15:08 +00:00
/// In screen coordinates (ignoring the draw offset).
2026-07-24 10:33:45 +02:00
public static func setScreenClipRect(x: Int, y: Int, width: Int, height: Int) {
gfx.pointee.setScreenClipRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height))
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-07-24 10:33:45 +02:00
public static func clearClipRect() {
gfx.pointee.clearClipRect.unsafelyUnwrapped()
2026-07-24 10:33:45 +02:00
}
public static func setLineCapStyle(_ style: LineCapStyle) {
gfx.pointee.setLineCapStyle.unsafelyUnwrapped(style.cValue)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// 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.
2026-07-24 10:33:45 +02:00
public static func setStencil(_ image: Bitmap?, tile: Bool = false) {
gfx.pointee.setStencilImage.unsafelyUnwrapped(image?.pointer, tile ? 1 : 0)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// `nil` targets the display framebuffer. Not retained; keep `target` alive until
/// the matching `popContext()`.
2026-07-24 10:33:45 +02:00
public static func pushContext(_ target: Bitmap? = nil) {
gfx.pointee.pushContext.unsafelyUnwrapped(target?.pointer)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Restores the previous context's drawing settings. No-op if none.
2026-07-24 10:33:45 +02:00
public static func popContext() {
gfx.pointee.popContext.unsafelyUnwrapped()
2026-07-24 10:33:45 +02:00
}
// MARK: - Shapes
2026-09-18 13:15:08 +00:00
/// `width` is in pixels.
2026-07-24 10:33:45 +02:00
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)
2026-07-24 10:33:45 +02:00
}
}
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),
2026-07-24 10:33:45 +02:00
Int32(x3), Int32(y3), $0)
}
}
2026-09-18 13:15:08 +00:00
/// Stroked inside its frame.
2026-07-24 10:33:45 +02:00
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)
2026-07-24 10:33:45 +02:00
}
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-07-24 10:33:45 +02:00
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)
2026-07-24 10:33:45 +02:00
}
}
public static func fillRect(_ rect: Rect, color: Color) {
fillRect(x: rect.left, y: rect.top, width: rect.width, height: rect.height, color: color)
}
2026-09-18 13:15:08 +00:00
/// Stroked inside the rect. `radius` and `lineWidth` are in pixels.
2026-07-24 10:33:45 +02:00
public static func drawRoundRect(x: Int, y: Int, width: Int, height: Int, radius: Int,
lineWidth: Int, color: Color) {
color.withLCDColor {
gfx.pointee.drawRoundRect.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height),
2026-07-24 10:33:45 +02:00
Int32(radius), Int32(lineWidth), $0)
}
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-09-18 13:15:08 +00:00
/// `radius` is in pixels.
2026-07-24 10:33:45 +02:00
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),
2026-07-24 10:33:45 +02:00
Int32(radius), $0)
}
}
2026-09-18 13:15:08 +00:00
/// `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)
}
2026-09-18 13:15:08 +00:00
/// Stroked inside the rect. Differing angles draw only that arc (degrees clockwise
/// from the top).
2026-07-24 10:33:45 +02:00
public static func drawEllipse(x: Int, y: Int, width: Int, height: Int, lineWidth: Int,
startAngle: Float = 0, endAngle: Float = 0, color: Color) {
color.withLCDColor {
gfx.pointee.drawEllipse.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height),
2026-07-24 10:33:45 +02:00
Int32(lineWidth), startAngle, endAngle, $0)
}
}
2026-09-18 13:15:08 +00:00
/// Differing angles fill only that wedge (degrees clockwise from the top).
2026-07-24 10:33:45 +02:00
public static func fillEllipse(x: Int, y: Int, width: Int, height: Int,
startAngle: Float = 0, endAngle: Float = 0, color: Color) {
color.withLCDColor {
gfx.pointee.fillEllipse.unsafelyUnwrapped(Int32(x), Int32(y), Int32(width), Int32(height),
2026-07-24 10:33:45 +02:00
startAngle, endAngle, $0)
}
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-09-18 13:15:08 +00:00
/// The last point connects back to the first.
2026-07-24 10:33:45 +02:00
public static func fillPolygon(points: [(x: Int, y: Int)], color: Color,
fillRule: PolygonFillRule = .nonZero) {
withUnsafeTemporaryAllocation(of: Int32.self, capacity: points.count * 2) { coordinates in
var index = 0
for point in points {
coordinates[index] = Int32(point.x)
coordinates[index + 1] = Int32(point.y)
index += 2
}
color.withLCDColor { cColor in
gfx.pointee.fillPolygon.unsafelyUnwrapped(Int32(points.count), coordinates.baseAddress,
2026-07-24 10:33:45 +02:00
cColor, fillRule.cValue)
}
}
}
2026-09-18 13:15:08 +00:00
/// Slow in bulk; prefer bitmaps or framebuffer writes for many pixels.
2026-07-24 10:33:45 +02:00
public static func setPixel(x: Int, y: Int, color: Color) {
color.withLCDColor { gfx.pointee.setPixel.unsafelyUnwrapped(Int32(x), Int32(y), $0) }
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// The 8×8 pattern whose upper-left corner is (x, y) in `bitmap`.
2026-07-24 10:33:45 +02:00
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))
2026-07-24 10:33:45 +02:00
var pattern = Pattern(bytes: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
if let source = UnsafeRawPointer(bitPattern: UInt(color)) {
withUnsafeMutableBytes(of: &pattern.bytes) { destination in
destination.copyMemory(from: UnsafeRawBufferPointer(start: source, count: 16))
}
}
return pattern
}
// MARK: - Text
2026-09-18 13:15:08 +00:00
/// Uses the current font, or the system font if none is set. Returns the drawn width.
2026-07-24 10:33:45 +02:00
@discardableResult
public static func drawText(_ text: String, x: Int, y: Int) -> Int {
2026-09-18 13:15:08 +00:00
text.withCString { cString in
Int(gfx.pointee.drawText.unsafelyUnwrapped(cString, text.utf8.count,
kUTF8Encoding, Int32(x), Int32(y)))
2026-07-24 10:33:45 +02:00
}
}
2026-09-18 13:15:08 +00:00
/// Wrapped and aligned inside the rect, with the current font.
2026-07-24 10:33:45 +02:00
public static func drawText(_ text: String, x: Int, y: Int, width: Int, height: Int,
wrap: TextWrappingMode = .word, align: TextAlignment = .left) {
2026-09-18 13:15:08 +00:00
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)
2026-07-24 10:33:45 +02:00
}
}
2026-09-18 13:15:08 +00:00
/// 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)
}
2026-09-18 13:15:08 +00:00
/// Not retained; keep `font` alive while set.
2026-07-24 10:33:45 +02:00
public static func setFont(_ font: Font) {
gfx.pointee.setFont.unsafelyUnwrapped(font.pointer)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Extra space between letters, in pixels.
2026-07-24 10:33:45 +02:00
public static var textTracking: Int {
get { Int(gfx.pointee.getTextTracking.unsafelyUnwrapped()) }
set { gfx.pointee.setTextTracking.unsafelyUnwrapped(Int32(newValue)) }
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Pixels added to the font's own leading for multi-line text.
2026-07-24 10:33:45 +02:00
public static func setTextLeading(_ lineHeightAdjustment: Int) {
gfx.pointee.setTextLeading.unsafelyUnwrapped(Int32(lineHeightAdjustment))
2026-07-24 10:33:45 +02:00
}
// MARK: - Framebuffer
2026-09-18 13:15:08 +00:00
/// 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)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// 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)
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Simulator only: white pixels overlay the display in translucent red. `nil` on device.
2026-07-24 10:33:45 +02:00
public static var debugBitmap: Bitmap? {
guard let getDebugBitmap = gfx.pointee.getDebugBitmap,
2026-07-24 10:33:45 +02:00
let pointer = getDebugBitmap() else { return nil }
return Bitmap(pointer: pointer, isOwned: false)
}
2026-09-18 13:15:08 +00:00
/// Not a copy; owned by the system.
2026-07-24 10:33:45 +02:00
public static var displayBufferBitmap: Bitmap? {
guard let pointer = gfx.pointee.getDisplayBufferBitmap.unsafelyUnwrapped() else { return nil }
2026-07-24 10:33:45 +02:00
return Bitmap(pointer: pointer, isOwned: false)
}
public static func copyFrameBufferBitmap() -> Bitmap? {
guard let pointer = gfx.pointee.copyFrameBufferBitmap.unsafelyUnwrapped() else { return nil }
2026-07-24 10:33:45 +02:00
return Bitmap(pointer: pointer, isOwned: true)
}
2026-09-18 13:15:08 +00:00
/// Marks rows `start`...`end` (inclusive) as changed by direct framebuffer writes.
2026-07-24 10:33:45 +02:00
public static func markUpdatedRows(from start: Int, to end: Int) {
gfx.pointee.markUpdatedRows.unsafelyUnwrapped(Int32(start), Int32(end))
2026-07-24 10:33:45 +02:00
}
2026-09-18 13:15:08 +00:00
/// Flushes the framebuffer. The system does this after each update.
2026-07-24 10:33:45 +02:00
public static func display() {
gfx.pointee.display.unsafelyUnwrapped()
2026-07-24 10:33:45 +02:00
}
}