diff --git a/.github/config.toml b/.github/config.toml new file mode 100644 index 00000000..16ed8db9 --- /dev/null +++ b/.github/config.toml @@ -0,0 +1,7 @@ +[unstable] +unstable-options = true +config-include = true +check-cfg = ["features", "names", "values", "output"] +panic-abort-tests = true +mtime-on-use = true +avoid-dev-deps = true diff --git a/.github/workflows/post.yml b/.github/workflows/post.yml index 2106d463..d5bb16fd 100644 --- a/.github/workflows/post.yml +++ b/.github/workflows/post.yml @@ -1,3 +1,4 @@ +name: Post on: release: types: [published] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ba55abc..9cd9b838 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: os: - macos-latest - ubuntu-latest - - windows-latest + # - windows-latest sdk: - latest @@ -36,6 +36,11 @@ jobs: - name: Cache uses: Swatinem/rust-cache@v1 + - name: Config + run: | + mkdir -p .cargo + cp -rf .github/config.toml .cargo/config.toml + - name: Install Playdate SDK ${{ matrix.sdk }} id: sdk uses: pd-rs/get-playdate-sdk@main @@ -45,37 +50,31 @@ jobs: - name: SDK ${{ steps.sdk.outputs.version }} installed run: which pdc && pdc --version - - name: Install cargo-action-fmt - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: cargo-action-fmt - - name: Test Crates run: | FEATURES_1=bindgen-runtime - FEATURES_2=bindgen-runtime,bindings-derive-debug,error-ctx - FEATURES_3=bindgen-runtime,bindings-derive-debug,bindings-documentation + FEATURES_2=bindgen-runtime,bindings-derive-debug,bindings-documentation,error-ctx - cargo test -p=playdate-sys -- --nocapture cargo test -p=playdate-sys --features=$FEATURES_1 -- --nocapture cargo test -p=playdate-sys --features=$FEATURES_2 -- --nocapture - cargo test -p=playdate-sys --features=$FEATURES_3 -- --nocapture cargo test -p=playdate-fs --lib --no-default-features --features=$FEATURES_1 -- --nocapture cargo test -p=playdate-fs --lib --no-default-features --features=$FEATURES_2 -- --nocapture - cargo test -p=playdate-fs --lib --no-default-features --features=$FEATURES_3 -- --nocapture cargo test -p=playdate-sound --lib --no-default-features --features=$FEATURES_1 -- --nocapture cargo test -p=playdate-sound --lib --no-default-features --features=$FEATURES_2 -- --nocapture - cargo test -p=playdate-sound --lib --no-default-features --features=$FEATURES_3 -- --nocapture cargo test -p=playdate-color --lib --no-default-features --features=$FEATURES_1 -- --nocapture cargo test -p=playdate-color --lib --no-default-features --features=$FEATURES_2 -- --nocapture - cargo test -p=playdate-color --lib --no-default-features --features=$FEATURES_3 -- --nocapture cargo test -p=playdate-controls --lib --no-default-features --features=$FEATURES_1 -- --nocapture cargo test -p=playdate-controls --lib --no-default-features --features=$FEATURES_2 -- --nocapture - cargo test -p=playdate-controls --lib --no-default-features --features=$FEATURES_3 -- --nocapture + + cargo test -p=playdate-menu --lib --no-default-features --features=$FEATURES_1 -- --nocapture + cargo test -p=playdate-menu --lib --no-default-features --features=$FEATURES_2 -- --nocapture + + cargo test -p=playdate-graphics --lib --no-default-features --features=$FEATURES_1 -- --nocapture + cargo test -p=playdate-graphics --lib --no-default-features --features=$FEATURES_2 -- --nocapture - name: Examples run: | @@ -85,6 +84,8 @@ jobs: cargo build --target=thumbv7em-none-eabihf -p=playdate-controls --examples --features=$FEATURES -Zbuild-std cargo build --target=thumbv7em-none-eabihf -p=playdate-color --examples --features=$FEATURES -Zbuild-std cargo build --target=thumbv7em-none-eabihf -p=playdate-sound --examples --features=$FEATURES -Zbuild-std + cargo build --target=thumbv7em-none-eabihf -p=playdate-menu --examples --features=$FEATURES -Zbuild-std + cargo build --target=thumbv7em-none-eabihf -p=playdate-graphics --examples --features=$FEATURES -Zbuild-std # Imitate docs.rs environment - name: Test in no-sdk environment @@ -106,8 +107,8 @@ jobs: matrix: os: - macos-latest - - ubuntu-latest - - windows-latest + # - ubuntu-latest + # - windows-latest sdk: - latest @@ -116,6 +117,11 @@ jobs: - name: Cache uses: Swatinem/rust-cache@v1 + - name: Config + run: | + mkdir -p .cargo + cp -rf .github/config.toml .cargo/config.toml + - name: Install Playdate SDK ${{ matrix.sdk }} id: sdk uses: pd-rs/get-playdate-sdk@main @@ -125,29 +131,24 @@ jobs: - name: SDK ${{ steps.sdk.outputs.version }} installed run: which pdc && pdc --version - - name: Install cargo-action-fmt - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: cargo-action-fmt - - name: Test Utils run: | cargo test -p=playdate-build-utils --all-features cargo test -p=playdate-build --all-features cargo test -p=playdate-tool --all-features - - name: Check + - name: Test run: | - cargo check -p=cargo-playdate -q --message-format=json | cargo-action-fmt - cargo test -p=cargo-playdate --no-run -q --message-format=json | cargo-action-fmt + cargo test -p=cargo-playdate -- --nocapture --test-threads=1 + rm -rf ./target/tmp - - name: Test + - name: Test (+usb) run: | - cargo test -p=cargo-playdate -- --nocapture + cargo test -p=cargo-playdate --features=usb -- --nocapture --test-threads=1 + rm -rf ./target/tmp - name: Install - run: | - cargo install -p=cargo-playdate --debug + run: cargo install --path=./cargo --debug - name: Examples run: | @@ -158,6 +159,8 @@ jobs: cargo playdate package --simulator --device -p=playdate-controls --examples cargo playdate package --simulator --device -p=playdate-color --examples cargo playdate package --simulator --device -p=playdate-sound --examples + cargo playdate package --simulator --device -p=playdate-menu --examples + cargo playdate package --simulator --device -p=playdate-graphics --examples # TODO: build crankstart with examples for compatibility test diff --git a/Cargo.toml b/Cargo.toml index 3efc1f2b..23051a3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,5 @@ members = [ "support/*", # "components/*" ] -default-members = [ - "cargo", - "support/tool", - "api/playdate", - "api/sys", - "support/build", -] +default-members = ["cargo", "support/tool"] exclude = ["cargo/tests/crates/**/*"] diff --git a/README.md b/README.md index 3ad5481a..2f36bdbc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,22 @@ # 🦀 Playdate API and build tools in Rust +* [Modular build system][support-dir] + - build-support libraries + - pre-configured bindgen with extra codegen + - [cargo-playdate][cargo-dir] - one-button solution to build, package and run programs +* [Modular low- & high- level API][api-dir] + - with [examples][ctrl-examples-dir] + +Welcome to [discussions][] and [issues][] for any questions and suggestions. + + +## Prerequisites + +Follow the instructions for: +1. [SDK](https://sdk.play.date/Inside%20Playdate%20with%20C.html#_prerequisites) +1. [cargo-playdate](https://github.com/boozook/playdate/blob/main/cargo/README.md#prerequisites) +1. [playdate-sys](https://github.com/boozook/playdate/tree/main/api/sys#prerequisites) -TODO: 🚨 proper documentation - short description for all crates with links. ## Usage @@ -19,6 +34,52 @@ cargo playdate run -p=playdate-sound --example=sp-simple --device ``` +## Modularity + +Thanks to the modular structure of the system, you can use all or only the parts of the system you need. + +### Create a Game + +1. Add [API-components][api-dir] as dependencies to your project +1. Install [cargo-playdate][] to build your project + +### Create an API-extension + +1. Add [playdate-sys][] to dependencies +1. Write neat code +1. Build & test using cargo, [cargo-playdate][] or anything else. + +Here is [example][color-dir] of simple API-extension. + +Please follow [the instructions of playdate-sys](https://github.com/boozook/playdate/tree/main/api/sys#extension-development). + +### Create your bindings + +1. Use [playdate-bindgen][] in your build-script + +### Create your build-system + +1. Use [build-support crates][support-dir] + +There is all needed to find SDK and arm-gnu toolchain on user's system, build flags, configurations, formats including pdxinfo, etc. + + + +[playdate-sys]: https://crates.io/crates/playdate-sys +[cargo-playdate]: https://crates.io/crates/cargo-playdate +[playdate-bindgen]: https://crates.io/crates/playdate-bindgen + +[support-dir]: https://github.com/boozook/playdate/tree/main/support +[cargo-dir]: https://github.com/boozook/playdate/tree/main/cargo +[api-dir]: https://github.com/boozook/playdate/tree/main/api +[ctrl-examples-dir]: https://github.com/boozook/playdate/tree/main/api/ctrl/examples +[color-dir]: https://github.com/boozook/playdate/tree/main/api/color + +[issues]: https://github.com/boozook/playdate/issues +[discussions]: https://github.com/boozook/playdate/discussions + + + - - - diff --git a/api/color/Cargo.toml b/api/color/Cargo.toml index fd830d8a..8d6b953d 100644 --- a/api/color/Cargo.toml +++ b/api/color/Cargo.toml @@ -11,28 +11,28 @@ repository = "https://github.com/boozook/playdate.git" [features] -default = ["pd/default"] -lang-items = ["pd/lang-items"] -allocator = ["pd/allocator"] -panic-handler = ["pd/panic-handler"] -eh-personality = ["pd/eh-personality"] -error-ctx = ["pd/error-ctx"] -bindgen-runtime = ["pd/bindgen-runtime"] -bindings-pre-built = ["pd/bindings-pre-built"] -bindings-generate = ["pd/bindings-generate"] -bindings-derive-default = ["pd/bindings-derive-default"] -bindings-derive-eq = ["pd/bindings-derive-eq"] -bindings-derive-copy = ["pd/bindings-derive-copy"] -bindings-derive-debug = ["pd/bindings-derive-debug"] -bindings-derive-hash = ["pd/bindings-derive-hash"] -bindings-derive-ord = ["pd/bindings-derive-ord"] -bindings-derive-partialeq = ["pd/bindings-derive-partialeq"] -bindings-derive-partialord = ["pd/bindings-derive-partialord"] -bindings-derive-constparamty = ["pd/bindings-derive-constparamty"] -bindings-documentation = ["pd/bindings-documentation"] +default = ["sys/default"] +lang-items = ["sys/lang-items"] +allocator = ["sys/allocator"] +panic-handler = ["sys/panic-handler"] +eh-personality = ["sys/eh-personality"] +error-ctx = ["sys/error-ctx"] +bindgen-runtime = ["sys/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static"] +bindings-derive-default = ["sys/bindings-derive-default"] +bindings-derive-eq = ["sys/bindings-derive-eq"] +bindings-derive-copy = ["sys/bindings-derive-copy"] +bindings-derive-debug = ["sys/bindings-derive-debug"] +bindings-derive-hash = ["sys/bindings-derive-hash"] +bindings-derive-ord = ["sys/bindings-derive-ord"] +bindings-derive-partialeq = ["sys/bindings-derive-partialeq"] +bindings-derive-partialord = ["sys/bindings-derive-partialord"] +bindings-derive-constparamty = ["sys/bindings-derive-constparamty"] +bindings-documentation = ["sys/bindings-documentation"] -[dependencies.pd] +[dependencies.sys] path = "../sys" +version = "0.1" package = "playdate-sys" default-features = false diff --git a/api/color/src/lib.rs b/api/color/src/lib.rs index ca1165ed..4b41aba6 100644 --- a/api/color/src/lib.rs +++ b/api/color/src/lib.rs @@ -2,17 +2,18 @@ #![feature(const_trait_impl)] #![feature(impl_trait_in_assoc_type)] -pub extern crate pd as sys; -use pd::ffi::LCDColor; -use pd::ffi::LCDPattern; -use pd::ffi::LCDSolidColor; +pub extern crate sys; +use sys::ffi::LCDColor; +use sys::ffi::LCDPattern; +use sys::ffi::LCDSolidColor; #[derive(PartialEq, Clone)] #[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] pub enum Color<'t> { Solid(LCDSolidColor), - Pattern(&'t LCDPattern), + PatternRef(&'t LCDPattern), + Pattern(LCDPattern), } impl Color<'static> { @@ -26,7 +27,8 @@ impl From> for LCDColor { fn from(color: Color) -> Self { match color { Color::Solid(color) => color as LCDColor, - Color::Pattern(pattern) => (pattern as *const u8) as LCDColor, + Color::Pattern(ref pattern) => (pattern as *const u8) as LCDColor, + Color::PatternRef(pattern) => (pattern as *const u8) as LCDColor, } } } diff --git a/api/ctrl/Cargo.toml b/api/ctrl/Cargo.toml index 12aeda8b..751dd8d3 100644 --- a/api/ctrl/Cargo.toml +++ b/api/ctrl/Cargo.toml @@ -13,30 +13,30 @@ repository = "https://github.com/boozook/playdate.git" [features] -default = ["pd/default"] -lang-items = ["pd/lang-items"] -allocator = ["pd/allocator"] -panic-handler = ["pd/panic-handler"] -eh-personality = ["pd/eh-personality"] -entry-point = ["pd/entry-point"] -error-ctx = ["pd/error-ctx"] -bindgen-runtime = ["pd/bindgen-runtime"] -bindings-pre-built = ["pd/bindings-pre-built"] -bindings-generate = ["pd/bindings-generate"] -bindings-derive-default = ["pd/bindings-derive-default"] -bindings-derive-eq = ["pd/bindings-derive-eq"] -bindings-derive-copy = ["pd/bindings-derive-copy"] -bindings-derive-debug = ["pd/bindings-derive-debug"] -bindings-derive-hash = ["pd/bindings-derive-hash"] -bindings-derive-ord = ["pd/bindings-derive-ord"] -bindings-derive-partialeq = ["pd/bindings-derive-partialeq"] -bindings-derive-partialord = ["pd/bindings-derive-partialord"] -bindings-derive-constparamty = ["pd/bindings-derive-constparamty"] -bindings-documentation = ["pd/bindings-documentation"] - - -[dependencies.pd] +default = ["sys/default"] +lang-items = ["sys/lang-items"] +allocator = ["sys/allocator"] +panic-handler = ["sys/panic-handler"] +eh-personality = ["sys/eh-personality"] +entry-point = ["sys/entry-point"] +error-ctx = ["sys/error-ctx"] +bindgen-runtime = ["sys/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static"] +bindings-derive-default = ["sys/bindings-derive-default"] +bindings-derive-eq = ["sys/bindings-derive-eq"] +bindings-derive-copy = ["sys/bindings-derive-copy"] +bindings-derive-debug = ["sys/bindings-derive-debug"] +bindings-derive-hash = ["sys/bindings-derive-hash"] +bindings-derive-ord = ["sys/bindings-derive-ord"] +bindings-derive-partialeq = ["sys/bindings-derive-partialeq"] +bindings-derive-partialord = ["sys/bindings-derive-partialord"] +bindings-derive-constparamty = ["sys/bindings-derive-constparamty"] +bindings-documentation = ["sys/bindings-documentation"] + + +[dependencies.sys] path = "../sys" +version = "0.1" package = "playdate-sys" default-features = false diff --git a/api/ctrl/examples/README.md b/api/ctrl/examples/README.md new file mode 100644 index 00000000..0ad415ce --- /dev/null +++ b/api/ctrl/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level, except for the parts that directly demonstrate the functionality of this package. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-controls --example=buttons --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-controls --example=accelerometer --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/ctrl/examples/accelerometer.rs b/api/ctrl/examples/accelerometer.rs index cffde7fd..1b024f4b 100644 --- a/api/ctrl/examples/accelerometer.rs +++ b/api/ctrl/examples/accelerometer.rs @@ -5,13 +5,13 @@ use alloc::boxed::Box; use core::ffi::*; #[macro_use] -extern crate pd; +extern crate sys; extern crate playdate_controls as controls; -use pd::ffi::*; +use sys::ffi::*; const INITIAL_X: u32 = LCD_COLUMNS / 2; -const INITIAL_Y: u32 = (pd::ffi::LCD_ROWS - TEXT_HEIGHT) / 2; +const INITIAL_Y: u32 = (sys::ffi::LCD_ROWS - TEXT_HEIGHT) / 2; const TEXT_HEIGHT: u32 = 16; @@ -41,7 +41,7 @@ impl State { let (x, y, z) = controls::peripherals::Accelerometer::get()?; unsafe { - let graphics = (*pd::API).graphics; + let graphics = (*sys::API).graphics; (*graphics).clear?(LCDSolidColor::kColorWhite as LCDColor); // render state to string @@ -80,7 +80,7 @@ impl State { self.pos.y = LCD_ROWS as i32 - TEXT_HEIGHT as i32 } - (*(*pd::API).system).drawFPS?(0, 0); + (*(*sys::API).system).drawFPS?(0, 0); Some(()) } } @@ -91,7 +91,7 @@ impl State { match event { // initial setup PDSystemEvent::kEventInit => { - unsafe { (*(*pd::API).display).setRefreshRate?(20.0) } + unsafe { (*(*sys::API).display).setRefreshRate?(20.0) } // turn on the accelerometer controls::peripherals::Accelerometer::enable()?; }, @@ -114,7 +114,7 @@ pub extern "C" fn eventHandlerShim(api: *const PlaydateAPI, event: PDSystemEvent match event { PDSystemEvent::kEventInit => unsafe { // register the API entry point - pd::API = api; + sys::API = api; // create game state if STATE.is_none() { diff --git a/api/ctrl/examples/buttons.rs b/api/ctrl/examples/buttons.rs index e905c13f..b45cc964 100644 --- a/api/ctrl/examples/buttons.rs +++ b/api/ctrl/examples/buttons.rs @@ -6,18 +6,18 @@ use alloc::borrow::Cow; use alloc::boxed::Box; #[macro_use] -extern crate pd; +extern crate sys; extern crate playdate_controls as controls; use alloc::format; use alloc::vec::Vec; use controls::buttons::PDButtonsExt; use controls::buttons::PDButtonsIter; use crate::controls::buttons::IterSingleButtons; -use pd::ffi::*; +use sys::ffi::*; const INITIAL_X: u32 = LCD_COLUMNS / 2; -const INITIAL_Y: u32 = (pd::ffi::LCD_ROWS - TEXT_HEIGHT) / 2; +const INITIAL_Y: u32 = (sys::ffi::LCD_ROWS - TEXT_HEIGHT) / 2; const TEXT_HEIGHT: u32 = 16; @@ -47,7 +47,7 @@ impl State { let buttons = controls::peripherals::Buttons::get()?; unsafe { - let graphics = (*pd::API).graphics; + let graphics = (*sys::API).graphics; (*graphics).clear?(LCDSolidColor::kColorWhite as LCDColor); // render buttons state to string @@ -102,7 +102,7 @@ impl State { self.pos.y = LCD_ROWS as i32 - TEXT_HEIGHT as i32 } - (*(*pd::API).system).drawFPS?(0, 0); + (*(*sys::API).system).drawFPS?(0, 0); Some(()) } } @@ -113,7 +113,7 @@ impl State { match event { // initial setup PDSystemEvent::kEventInit => unsafe { - (*(*pd::API).display).setRefreshRate?(20.0); + (*(*sys::API).display).setRefreshRate?(20.0); }, _ => {}, } @@ -130,7 +130,7 @@ pub extern "C" fn eventHandlerShim(api: *const PlaydateAPI, event: PDSystemEvent match event { PDSystemEvent::kEventInit => unsafe { // register the API entry point - pd::API = api; + sys::API = api; // create game state if STATE.is_none() { diff --git a/api/ctrl/src/buttons.rs b/api/ctrl/src/buttons.rs index 4734d162..89b26cda 100644 --- a/api/ctrl/src/buttons.rs +++ b/api/ctrl/src/buttons.rs @@ -1,7 +1,7 @@ use core::fmt::Debug; use core::fmt::Display; use core::ops::BitAnd; -use pd::ffi::PDButtons; +use sys::ffi::PDButtons; #[const_trait] diff --git a/api/ctrl/src/lib.rs b/api/ctrl/src/lib.rs index 9a9c9881..7619a0bd 100644 --- a/api/ctrl/src/lib.rs +++ b/api/ctrl/src/lib.rs @@ -4,6 +4,6 @@ #[macro_use] extern crate alloc; -pub extern crate pd as sys; +pub extern crate sys; pub mod buttons; pub mod peripherals; diff --git a/api/ctrl/src/peripherals.rs b/api/ctrl/src/peripherals.rs index 6206be66..6da884da 100644 --- a/api/ctrl/src/peripherals.rs +++ b/api/ctrl/src/peripherals.rs @@ -1,7 +1,7 @@ use core::ffi::c_float; -use pd::ffi::PDButtons; -use pd::ffi::PDPeripherals; -use pd::api; +use sys::ffi::PDButtons; +use sys::ffi::PDPeripherals; +use sys::api; macro_rules! sysfn { diff --git a/api/fs/Cargo.toml b/api/fs/Cargo.toml index 21782fe7..2f013a05 100644 --- a/api/fs/Cargo.toml +++ b/api/fs/Cargo.toml @@ -19,8 +19,7 @@ panic-handler = ["sys/panic-handler"] eh-personality = ["sys/eh-personality"] error-ctx = ["sys/error-ctx"] bindgen-runtime = ["sys/bindgen-runtime"] -bindings-pre-built = ["sys/bindings-pre-built"] -bindings-generate = ["sys/bindings-generate"] +bindgen-static = ["sys/bindgen-static"] bindings-derive-default = ["sys/bindings-derive-default"] bindings-derive-eq = ["sys/bindings-derive-eq"] bindings-derive-copy = ["sys/bindings-derive-copy"] @@ -35,6 +34,7 @@ bindings-documentation = ["sys/bindings-documentation"] [dependencies.sys] path = "../sys" +version = "0.1" package = "playdate-sys" default-features = false diff --git a/api/fs/examples/README.md b/api/fs/examples/README.md new file mode 100644 index 00000000..8c2ec971 --- /dev/null +++ b/api/fs/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level, except for the parts that directly demonstrate the functionality of this package. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-fs --example=simple --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-fs --example=simple --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/fs/src/error.rs b/api/fs/src/error.rs index 172c5c5f..ecb07e6b 100644 --- a/api/fs/src/error.rs +++ b/api/fs/src/error.rs @@ -49,19 +49,16 @@ impl Error { pub fn latest() -> Result, ApiError> { let f = sys::api_ok!(file.geterr)?; let ptr = unsafe { f() }; - if ptr.is_null() { - Ok(None) - } else { - unsafe { CStr::from_ptr(ptr as _) }.to_str() - .map_err(Into::into) - .map(Self::from) - .map(Into::into) - } + Self::from_ptr(ptr) } pub fn latest_using(fs: &Fs) -> Result, ApiError> { let f = fs.0.geterr.ok_or_null()?; let ptr = unsafe { f() }; + Self::from_ptr(ptr) + } + + pub fn from_ptr(ptr: *const core::ffi::c_char) -> Result, ApiError> { if ptr.is_null() { Ok(None) } else { diff --git a/api/gfx/Cargo.toml b/api/gfx/Cargo.toml new file mode 100644 index 00000000..b74b9049 --- /dev/null +++ b/api/gfx/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "playdate-graphics" +version = "0.1.0" +edition = "2021" + +readme = "README.md" +authors = ["Alex Koz "] +description = "High-level graphics API built on-top of Playdate API" +homepage = "https://github.com/boozook/playdate" +repository = "https://github.com/boozook/playdate.git" + + +[features] +default = ["sys/default", "color/default", "fs/default"] +# sys- features: +lang-items = ["sys/lang-items", "color/lang-items", "fs/lang-items"] +allocator = ["sys/allocator", "color/allocator", "fs/allocator"] +panic-handler = ["sys/panic-handler", "color/panic-handler", "fs/panic-handler"] +eh-personality = ["sys/eh-personality", "color/eh-personality", "fs/eh-personality"] +error-ctx = ["sys/error-ctx", "color/error-ctx", "fs/error-ctx"] +bindgen-runtime = ["sys/bindgen-runtime", "color/bindgen-runtime", "fs/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static", "color/bindgen-static", "fs/bindgen-static"] +bindings-derive-default = ["sys/bindings-derive-default", "color/bindings-derive-default", "fs/bindings-derive-default"] +bindings-derive-eq = ["sys/bindings-derive-eq", "color/bindings-derive-eq", "fs/bindings-derive-eq"] +bindings-derive-copy = ["sys/bindings-derive-copy", "color/bindings-derive-copy", "fs/bindings-derive-copy"] +bindings-derive-debug = ["sys/bindings-derive-debug", "color/bindings-derive-debug", "fs/bindings-derive-debug"] +bindings-derive-hash = ["sys/bindings-derive-hash", "color/bindings-derive-hash", "fs/bindings-derive-hash"] +bindings-derive-ord = ["sys/bindings-derive-ord", "color/bindings-derive-ord", "fs/bindings-derive-ord"] +bindings-derive-partialeq = ["sys/bindings-derive-partialeq", "color/bindings-derive-partialeq", "fs/bindings-derive-partialeq"] +bindings-derive-partialord = ["sys/bindings-derive-partialord", "color/bindings-derive-partialord", "fs/bindings-derive-partialord"] +bindings-derive-constparamty = ["sys/bindings-derive-constparamty", "color/bindings-derive-constparamty", "fs/bindings-derive-constparamty"] +bindings-documentation = ["sys/bindings-documentation", "color/bindings-documentation", "fs/bindings-documentation"] + + +[dependencies.sys] +path = "../sys" +version = "0.1" +package = "playdate-sys" +default-features = false + +# used: Error and Path +[dependencies.fs] +path = "../fs" +version = "0.1" +package = "playdate-fs" +default-features = false + +# used: Color +[dependencies.color] +path = "../color" +version = "0.1" +package = "playdate-color" +default-features = false + + +[[example]] +name = "bitmap" +crate-type = ["dylib", "staticlib"] +path = "examples/bitmap.rs" + +[package.metadata.playdate] +bundle-id = "rs.playdate.menu" diff --git a/api/gfx/README.md b/api/gfx/README.md new file mode 100644 index 00000000..761e026a --- /dev/null +++ b/api/gfx/README.md @@ -0,0 +1,28 @@ +# Graphics API for PlayDate + +High-level graphics API built on-top of [playdate-sys][]. + + +## Usage + +```rust +extern crate playdate_graphics; +use playdate_graphics::{bitmap, color, text}; + +// create and draw black rect: +let image = bitmap::Bitmap::new(100, 100, color::Color::BLACK).unwrap(); +image.draw(50, 100, bitmap::BitmapFlip::kBitmapUnflipped); + +// draw simple line of text: +let str = CStr::from_bytes_with_nul(b"Simple Text\0").unwrap(); +text::draw_text_cstr(str, 40, 40); +``` + + +[playdate-sys]: https://crates.io/crates/playdate-sys + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/api/gfx/examples/README.md b/api/gfx/examples/README.md new file mode 100644 index 00000000..225d805f --- /dev/null +++ b/api/gfx/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level, except for the parts that directly demonstrate the functionality of this package. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-graphics --example=bitmap --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-graphics --example=bitmap --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/gfx/examples/bitmap.rs b/api/gfx/examples/bitmap.rs new file mode 100644 index 00000000..2963f1df --- /dev/null +++ b/api/gfx/examples/bitmap.rs @@ -0,0 +1,123 @@ +#![no_std] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_graphics as gfx; + +use core::ffi::*; +use alloc::boxed::Box; + +use sys::ffi::*; +use gfx::color::*; +use gfx::bitmap; + + +const CENTER_X: u32 = LCD_COLUMNS / 2; +const CENTER_Y: u32 = LCD_ROWS / 2; +const TEXT_HEIGHT: u32 = 16; + + +/// App state +struct State { + rotation: c_float, + image: Option, +} + +impl State { + const fn new() -> Self { + Self { rotation: 0., + image: None } + } + + + /// Updates the state + fn update(&mut self) -> Option<()> { + const LABEL_DEF: &str = "Just rotating bitmap:\0"; + + let cstr = CStr::from_bytes_with_nul(LABEL_DEF.as_bytes()).unwrap(); + + gfx::clear(Color::WHITE); + + // get width (screen-size) of text + let text_width = gfx::text::get_text_width_cstr(cstr, None, 0); + + // render text + gfx::text::draw_text_cstr( + cstr, + CENTER_X as c_int - text_width / 2, + TEXT_HEIGHT.try_into().unwrap(), + ); + + // draw bitmap + if let Some(image) = self.image.as_ref() { + image.draw_rotated(CENTER_X as _, CENTER_Y as _, self.rotation, 0.5, 0.5, 1.0, 1.0); + } + + self.rotation += 1.0; + if self.rotation > 360.0 { + self.rotation = 0.0; + } + + Some(()) + } + + + /// Event handler + fn event(&'static mut self, event: PDSystemEvent) -> Option<()> { + match event { + // initial setup + PDSystemEvent::kEventInit => { + unsafe { (*(*sys::API).display).setRefreshRate?(60.0) }; + + self.image = Some(bitmap::Bitmap::new(100, 100, color::Color::BLACK).unwrap()); + }, + _ => {}, + } + Some(()) + } +} + + +#[no_mangle] +/// Proxy event handler, calls `State::event` +pub extern "C" fn eventHandlerShim(api: *const PlaydateAPI, event: PDSystemEvent, _arg: u32) -> c_int { + static mut STATE: Option> = None; + + match event { + PDSystemEvent::kEventInit => unsafe { + // register the API entry point + sys::API = api; + + // create game state + if STATE.is_none() { + STATE = Some(Box::new(State::new())); + } + let state = STATE.as_mut().unwrap().as_mut() as *mut State; + + // get `setUpdateCallback` fn + let f = (*(*api).system).setUpdateCallback.expect("setUpdateCallback"); + // register update callback with user-data = our state + f(Some(on_update), state.cast()); + }, + _ => {}, + } + + if let Some(state) = unsafe { STATE.as_mut() } { + state.event(event).and(Some(0)).unwrap_or(1) + } else { + 1 + } +} + + +/// Proxy update callback, calls `State::update` +unsafe extern "C" fn on_update(state: *mut c_void) -> i32 { + let ptr: *mut State = state.cast(); + let state = ptr.as_mut().expect("missed state"); + state.update().and(Some(1)).unwrap_or_default() +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/gfx/src/bitmap/api.rs b/api/gfx/src/bitmap/api.rs new file mode 100644 index 00000000..b07cc049 --- /dev/null +++ b/api/gfx/src/bitmap/api.rs @@ -0,0 +1,132 @@ +use core::ffi::c_char; +use core::ffi::c_float; +use core::ffi::c_int; + +use sys::ffi::LCDColor; +use sys::ffi::LCDBitmap; +use sys::ffi::LCDBitmapFlip; +use sys::ffi::LCDRect; + + +#[derive(Debug, Clone, Copy, core::default::Default)] +pub struct Default; +impl Api for Default {} + + +pub trait Api { + fn new_bitmap(&self) + -> unsafe extern "C" fn(width: c_int, height: c_int, bgcolor: LCDColor) -> *mut LCDBitmap { + *sys::api!(graphics.newBitmap) + } + + fn free_bitmap(&self) -> unsafe extern "C" fn(bitmap: *mut LCDBitmap) { *sys::api!(graphics.freeBitmap) } + + + fn load_bitmap(&self) + -> unsafe extern "C" fn(path: *const c_char, outerr: *mut *const c_char) -> *mut LCDBitmap { + *sys::api!(graphics.loadBitmap) + } + + fn copy_bitmap(&self) -> unsafe extern "C" fn(bitmap: *mut LCDBitmap) -> *mut LCDBitmap { + *sys::api!(graphics.copyBitmap) + } + + fn load_into_bitmap( + &self) + -> unsafe extern "C" fn(path: *const c_char, bitmap: *mut LCDBitmap, out_err: *mut *const c_char) { + *sys::api!(graphics.loadIntoBitmap) + } + + + fn get_bitmap_data( + &self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, + width: *mut c_int, + height: *mut c_int, + row_bytes: *mut c_int, + mask: *mut *mut u8, + data: *mut *mut u8) { + *sys::api!(graphics.getBitmapData) + } + + + fn clear_bitmap(&self) -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, bgcolor: LCDColor) { + *sys::api!(graphics.clearBitmap) + } + + fn rotated_bitmap( + &self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, + rotation: c_float, + x_scale: c_float, + y_scale: c_float, + allocedSize: *mut c_int) -> *mut LCDBitmap { + *sys::api!(graphics.rotatedBitmap) + } + + fn set_bitmap_mask(&self) -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, mask: *mut LCDBitmap) -> c_int { + *sys::api!(graphics.setBitmapMask) + } + + fn get_bitmap_mask(&self) -> unsafe extern "C" fn(bitmap: *mut LCDBitmap) -> *mut LCDBitmap { + *sys::api!(graphics.getBitmapMask) + } + + fn draw_bitmap(&self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, x: c_int, y: c_int, flip: LCDBitmapFlip) { + *sys::api!(graphics.drawBitmap) + } + + fn tile_bitmap( + &self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, + x: c_int, + y: c_int, + width: c_int, + height: c_int, + flip: LCDBitmapFlip) { + *sys::api!(graphics.tileBitmap) + } + + fn draw_rotated_bitmap( + &self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, + x: c_int, + y: c_int, + rotation: c_float, + center_x: c_float, + center_y: c_float, + x_scale: c_float, + y_scale: c_float) { + *sys::api!(graphics.drawRotatedBitmap) + } + + fn draw_scaled_bitmap( + &self) + -> unsafe extern "C" fn(bitmap: *mut LCDBitmap, x: c_int, y: c_int, x_scale: c_float, y_scale: c_float) { + *sys::api!(graphics.drawScaledBitmap) + } + + fn check_mask_collision( + &self) + -> unsafe extern "C" fn(bitmap1: *mut LCDBitmap, + x1: c_int, + y1: c_int, + flip1: LCDBitmapFlip, + bitmap2: *mut LCDBitmap, + x2: c_int, + y2: c_int, + flip2: LCDBitmapFlip, + rect: LCDRect) -> c_int { + *sys::api!(graphics.checkMaskCollision) + } + + fn set_color_to_pattern( + &self) + -> unsafe extern "C" fn(color: *mut LCDColor, + bitmap: *mut LCDBitmap, + x: core::ffi::c_int, + y: core::ffi::c_int) { + *sys::api!(graphics.setColorToPattern) + } +} diff --git a/api/gfx/src/bitmap/bitmap.rs b/api/gfx/src/bitmap/bitmap.rs new file mode 100644 index 00000000..bf2f9055 --- /dev/null +++ b/api/gfx/src/bitmap/bitmap.rs @@ -0,0 +1,401 @@ +use core::ffi::c_char; +use core::ffi::c_float; +use core::ffi::c_int; +use core::ffi::c_void; +use alloc::boxed::Box; + +use sys::ffi::CString; +use sys::ffi::LCDColor; +use sys::ffi::LCDPattern; +use sys::ffi::LCDRect; +use sys::ffi::LCDBitmap; +pub use sys::ffi::LCDBitmapFlip as BitmapFlip; +use fs::Path; +pub use color::*; +use crate::error::ApiError; +use crate::error::Error; +use super::api; + + +#[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] +pub struct Bitmap(pub(crate) *mut LCDBitmap, + pub(crate) Api); + +impl Bitmap { + pub fn new(width: c_int, height: c_int, bg: Color) -> Result + where Api: Default { + let api = Api::default(); + Self::new_with(api, width, height, bg) + } + + pub fn new_with(api: Api, width: c_int, height: c_int, bg: Color) -> Result { + let f = api.new_bitmap(); + let ptr = unsafe { f(width, height, bg.into()) }; + if ptr.is_null() { + Err(Error::Alloc) + } else { + Ok(Self(ptr, api)) + } + } + + + pub fn load>(path: P) -> Result + where Api: Default { + let api = Api::default(); + Self::load_with(api, path) + } + + pub fn load_with>(api: Api, path: P) -> Result { + let mut err = Box::new(core::ptr::null() as *const c_char); + let out_err = Box::into_raw(err); + + let path = CString::new(path.as_ref())?; + + let f = api.load_bitmap(); + let ptr = unsafe { f(path.as_ptr() as *mut c_char, out_err as _) }; + if ptr.is_null() { + err = unsafe { Box::from_raw(out_err) }; + if let Some(err) = fs::error::Error::from_ptr(*err).map_err(ApiError::from_err)? { + Err(Error::Fs(err).into()) + } else { + Err(Error::Alloc.into()) + } + } else { + Ok(Self(ptr, api)) + } + } +} + + +impl Bitmap { + pub fn load_into>(&mut self, path: P) -> Result<(), ApiError> { + let mut err = Box::new(core::ptr::null() as *const c_char); + let out_err = Box::into_raw(err); + + let path = CString::new(path.as_ref())?; + + let f = self.1.load_into_bitmap(); + unsafe { f(path.as_ptr() as *mut c_char, self.0, out_err as _) }; + err = unsafe { Box::from_raw(out_err) }; + if let Some(err) = fs::error::Error::from_ptr(*err).map_err(ApiError::from_err)? { + Err(Error::Fs(err).into()) + } else { + Ok(()) + } + } +} + + +impl Drop for Bitmap { + fn drop(&mut self) { + if FOD && !self.0.is_null() { + let f = self.1.free_bitmap(); + unsafe { f(self.0) }; + self.0 = core::ptr::null_mut(); + } + } +} + +impl Clone for Bitmap { + fn clone(&self) -> Self { + let f = self.1.copy_bitmap(); + let ptr = unsafe { f(self.0) }; + if ptr.is_null() { + panic!("{}: bitmap clone", Error::Alloc) + } else { + Self(ptr, self.1.clone()) + } + } +} +// impl Clone for Bitmap { +// fn clone(&self) -> Self { Self(self.0, self.1.clone()) } +// } + + +impl Bitmap { + pub fn clear(&self, bg: Color) { + let f = self.1.clear_bitmap(); + unsafe { f(self.0, bg.into()) }; + } + + + pub fn get_bitmap_data<'bitmap>(&'bitmap mut self) -> Result, Error> { + let mut width: c_int = 0; + let mut height: c_int = 0; + let mut row_bytes: c_int = 0; + + + let mut boxed_data = Box::new(core::ptr::null_mut()); + let data = Box::into_raw(boxed_data); + let mut boxed_mask = Box::new(core::ptr::null_mut()); + let mask = Box::into_raw(boxed_mask); + + let f = self.1.get_bitmap_data(); + unsafe { f(self.0, &mut width, &mut height, &mut row_bytes, mask, data) }; + + let len = row_bytes * height; + + boxed_data = unsafe { Box::from_raw(data) }; + boxed_mask = unsafe { Box::from_raw(mask) }; + + // get mask: + let mask = { + if !boxed_mask.is_null() && !(*boxed_mask).is_null() { + let mask = unsafe { core::slice::from_raw_parts_mut::(*boxed_mask, len as usize) }; + Some(mask) + } else { + None + } + }; + + // get data: + let len = if mask.is_some() { + row_bytes * height + } else { + (row_bytes * height) * 2 + }; + let data = unsafe { core::slice::from_raw_parts_mut::(*boxed_data, len as usize) }; + + + Ok(BitmapData { width, + height, + row_bytes, + mask, + data }) + } + + + /// Sets a mask image for the given bitmap. The set mask must be the same size as the target bitmap. + pub fn set_mask(&self, mask: &mut Bitmap) -> Result<(), Error> { + // TODO: investigate is it correct "res == 0 => Ok" + let f = self.1.set_bitmap_mask(); + let res = unsafe { f(self.0, mask.0) }; + if res == 0 { + Ok(()) + } else { + Err(Error::InvalidMask) + } + } + + /// Gets a mask image for the given bitmap. If the image doesn’t have a mask, returns None. + /// + /// Clones inner api-access. + // XXX: investigate is it should be free-on-drop? + #[inline(always)] + pub fn get_mask(&self) -> Option> + where Api: Clone { + self.get_mask_with(self.1.clone()) + } + + /// Gets a mask image for the given bitmap. If the image doesn’t have a mask, returns None. + /// + /// Produced `Bitmap` uses passed `api` api-access. + // XXX: investigate is it should be free-on-drop? + pub fn get_mask_with(&self, api: NewApi) -> Option> { + let f = self.1.get_bitmap_mask(); + let ptr = unsafe { f(self.0) }; + if !ptr.is_null() { + Some(Bitmap(ptr, api)) + } else { + None + } + } + + /// Returns a new, rotated and scaled Bitmap based on the given bitmap. + #[inline(always)] + pub fn rotated_clone(&self, + rotation: c_float, + x_scale: c_float, + y_scale: c_float) + -> Result, Error> + where Api: Clone + { + self.rotated_clone_with(self.1.clone(), rotation, x_scale, y_scale) + } + + pub fn rotated_clone_with(&self, + api: NewApi, + rotation: c_float, + x_scale: c_float, + y_scale: c_float) + -> Result, Error> + where Api: Clone + { + let mut alloced_size: c_int = 0; + let alloced_size_ref = &mut alloced_size; + let f = self.1.rotated_bitmap(); + let ptr = unsafe { f(self.0, rotation, x_scale, y_scale, alloced_size_ref) }; + + if alloced_size == 0 || ptr.is_null() { + Err(Error::Alloc) + } else { + Ok(Bitmap(ptr, api)) + } + } + + + #[inline(always)] + pub fn draw(&self, x: c_int, y: c_int, flip: BitmapFlip) { + let f = self.1.draw_bitmap(); + unsafe { f(self.0, x, y, flip) } + } + + #[inline(always)] + pub fn draw_tiled(&self, x: c_int, y: c_int, width: c_int, height: c_int, flip: BitmapFlip) { + let f = self.1.tile_bitmap(); + unsafe { f(self.0, x, y, width, height, flip) } + } + + /// Draws the *bitmap* scaled to `x_scale` and `y_scale` + /// then rotated by `degrees` with its center as given by proportions `center_x` and `center_y` at `x`, `y`; + /// + /// that is: + /// * if `center_x` and `center_y` are both 0.5 the center of the image is at (`x`,`y`), + /// * if `center_x` and `center_y` are both 0 the top left corner of the image (before rotation) is at (`x`,`y`), etc. + /// + /// Equivalent to [`sys::ffi::playdate_graphics::drawRotatedBitmap`]. + #[inline(always)] + pub fn draw_rotated(&self, + x: c_int, + y: c_int, + degrees: c_float, + center_x: c_float, + center_y: c_float, + x_scale: c_float, + y_scale: c_float) { + let f = self.1.draw_rotated_bitmap(); + unsafe { f(self.0, x, y, degrees, center_x, center_y, x_scale, y_scale) } + } + + #[inline(always)] + pub fn draw_scaled(&self, x: c_int, y: c_int, x_scale: c_float, y_scale: c_float) { + let f = self.1.draw_scaled_bitmap(); + unsafe { f(self.0, x, y, x_scale, y_scale) } + } + + + /// Returns `true` if any of the opaque pixels in this bitmap when positioned at `x, y` with `flip` overlap any of the opaque pixels in `other` bitmap at `x_other`, `y_other` with `flip_other` within the non-empty `rect`, + /// or `false` if no pixels overlap or if one or both fall completely outside of `rect`. + #[inline(always)] + pub fn check_mask_collision(&self, + x: c_int, + y: c_int, + flip: BitmapFlip, + other: Bitmap, + x_other: c_int, + y_other: c_int, + flip_other: BitmapFlip, + rect: LCDRect) + -> bool { + let f = self.1.check_mask_collision(); + unsafe { f(self.0, x, y, flip, other.0, x_other, y_other, flip_other, rect) == 1 } + } + + + /// Sets `color` to an 8 x 8 pattern using this bitmap. + /// `x, y` indicates the top left corner of the 8 x 8 pattern. + pub fn set_color_to_pattern(&self, color: &mut LCDColor, x: c_int, y: c_int) { + let f = self.1.set_color_to_pattern(); + unsafe { f(color as _, self.0, x, y) } + } + + /// Returns 8 x 8 pattern using this bitmap. + /// `x, y` indicates the top left corner of the 8 x 8 pattern. + pub fn get_color_of_pattern<'t>(&'t self, x: c_int, y: c_int) -> &mut LCDPattern { + let mut color: LCDColor = 0; + let f = self.1.set_color_to_pattern(); + unsafe { f(&mut color, self.0, x, y) } + assert!(color.is_pattern()); + + let ptr = color as *mut c_void as *mut u8 as *mut LCDPattern; + // TODO: check if ptr.is_null() ... + unsafe { ptr.as_mut() }.unwrap() + } +} + + +/// The data is 1 bit per pixel packed format, in MSB order; in other words, the high bit of the first byte in data is the top left pixel of the image. +/// The `mask` data is in same format but means transparency. +pub struct BitmapData<'bitmap> { + pub width: c_int, + pub height: c_int, + pub row_bytes: c_int, + mask: Option<&'bitmap mut [u8]>, + data: &'bitmap mut [u8], +} + +impl<'bitmap> BitmapData<'bitmap> { + pub fn width(&self) -> c_int { self.width } + pub fn height(&self) -> c_int { self.height } + pub fn rowbytes(&self) -> c_int { self.row_bytes } + pub fn mask(&self) -> Option<&[u8]> { self.mask.as_deref() } + pub fn mask_mut(&mut self) -> Option<&mut [u8]> { self.mask.as_deref_mut() } + pub fn data(&self) -> &[u8] { self.data } + pub fn data_mut(&mut self) -> &mut [u8] { self.data } +} + + +// +// Global Bitmap-related methods +// + +pub fn get_debug_bitmap() -> Result, ApiError> { + let f = sys::api_ok!(graphics.getDebugBitmap)?; + let ptr = unsafe { f() }; + if ptr.is_null() { + Err(Error::Alloc.into()) + } else { + Ok(Bitmap(ptr, Default::default())) + } +} + +pub fn get_display_buffer_bitmap() -> Result, Error> { + let f = *sys::api!(graphics.getDisplayBufferBitmap); + let ptr = unsafe { f() }; + if ptr.is_null() { + Err(Error::Alloc) + } else { + Ok(Bitmap(ptr, Default::default())) + } +} + +pub fn copy_frame_buffer_bitmap() -> Result, Error> { + let f = *sys::api!(graphics.copyFrameBufferBitmap); + let ptr = unsafe { f() }; + if ptr.is_null() { + Err(Error::Alloc) + } else { + Ok(Bitmap(ptr, Default::default())) + } +} + + +/// Sets the stencil used for drawing. +/// If the `tile` is `true` the stencil image will be tiled. +/// Tiled stencils must have width equal to a multiple of 32 pixels. +pub fn set_stencil_tiled(image: &Bitmap, tile: bool) { + let f = *sys::api!(graphics.setStencilImage); + unsafe { f(image.0, tile as _) }; +} + +/// Sets the stencil used for drawing. +/// For a tiled stencil, use [`set_stencil_tiled`] instead. +pub fn set_stencil(image: &Bitmap) { + let f = *sys::api!(graphics.setStencil); + unsafe { f(image.0) }; +} + +pub fn set_draw_mode(mode: sys::ffi::LCDBitmapDrawMode) { + let f = *sys::api!(graphics.setDrawMode); + unsafe { f(mode) }; +} + +pub fn push_context(target: &Bitmap) { + let f = *sys::api!(graphics.pushContext); + unsafe { f(target.0) }; +} + +pub fn pop_context() { + let f = *sys::api!(graphics.popContext); + unsafe { f() }; +} diff --git a/api/gfx/src/bitmap/table.rs b/api/gfx/src/bitmap/table.rs new file mode 100644 index 00000000..85ec2b21 --- /dev/null +++ b/api/gfx/src/bitmap/table.rs @@ -0,0 +1,161 @@ +use alloc::boxed::Box; +use core::ffi::c_char; +use core::ffi::c_int; + +use sys::ffi::CString; +use sys::ffi::LCDBitmapTable; +use fs::Path; + +use crate::error::ApiError; +use crate::error::Error; +use super::Bitmap; +use super::api::Api as BitmapApi; + + +#[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] +pub struct BitmapTable(*mut LCDBitmapTable, Api); + +impl Drop for BitmapTable { + fn drop(&mut self) { + if FOD && !self.0.is_null() { + let f = self.1.free_bitmap_table(); + unsafe { f(self.0) }; + self.0 = core::ptr::null_mut(); + } + } +} + + +impl BitmapTable { + pub fn new(count: c_int, width: c_int, height: c_int) -> Result + where Api: Default { + let api = Api::default(); + Self::new_with(api, count, width, height) + } + + pub fn new_with(api: Api, count: c_int, width: c_int, height: c_int) -> Result { + let f = api.new_bitmap_table(); + let ptr = unsafe { f(count, width, height) }; + if ptr.is_null() { + Err(Error::Alloc) + } else { + Ok(Self(ptr, api)) + } + } + + + pub fn load>(path: P) -> Result + where Api: Default { + let api = Api::default(); + Self::load_with(api, path) + } + + pub fn load_with>(api: Api, path: P) -> Result { + let mut err = Box::new(core::ptr::null() as *const c_char); + let out_err = Box::into_raw(err); + + let path = CString::new(path.as_ref())?; + + let f = api.load_bitmap_table(); + let ptr = unsafe { f(path.as_ptr() as *mut c_char, out_err as _) }; + if ptr.is_null() { + err = unsafe { Box::from_raw(out_err) }; + if let Some(err) = fs::error::Error::from_ptr(*err).map_err(ApiError::from_err)? { + Err(Error::Fs(err).into()) + } else { + Err(Error::Alloc.into()) + } + } else { + Ok(Self(ptr, api)) + } + } +} + +impl BitmapTable { + pub fn load_into>(&mut self, path: P) -> Result<(), ApiError> { + let mut err = Box::new(core::ptr::null() as *const c_char); + let out_err = Box::into_raw(err); + + let path = CString::new(path.as_ref())?; + + let f = self.1.load_into_bitmap_table(); + unsafe { f(path.as_ptr() as *mut c_char, self.0, out_err as _) }; + err = unsafe { Box::from_raw(out_err) }; + if let Some(err) = fs::error::Error::from_ptr(*err).map_err(ApiError::from_err)? { + Err(Error::Fs(err).into()) + } else { + Ok(()) + } + } + + + /// Returns the `index` bitmap in this table, + /// if `index` is out of bounds, the function returns `None`. + /// + /// Creates new default `BitApi`. + pub fn get_bitmap<'table, BitApi: BitmapApi>(&'table self, index: c_int) -> Option> + where Bitmap: 'table, + BitApi: Default { + self.get_bitmap_with(BitApi::default(), index) + } + + /// Returns the `index` bitmap in this table, + /// if `index` is out of bounds, the function returns `None`. + /// + /// Produced `Bitmap` uses passed `api` api-access. + pub fn get_bitmap_with<'table, BitApi: BitmapApi>(&'table self, + api: BitApi, + index: c_int) + -> Option> + where Bitmap: 'table + { + let f = self.1.get_table_bitmap(); + let ptr = unsafe { f(self.0, index) }; + if ptr.is_null() { + None + } else { + Some(Bitmap(ptr, api)) + } + } +} + + +pub mod api { + use core::ffi::c_char; + use core::ffi::c_int; + use sys::ffi::LCDBitmap; + use sys::ffi::LCDBitmapTable; + + + #[derive(Debug, Clone, Copy, core::default::Default)] + pub struct Default; + impl Api for Default {} + + + pub trait Api { + fn new_bitmap_table( + &self) + -> unsafe extern "C" fn(count: c_int, width: c_int, height: c_int) -> *mut LCDBitmapTable { + *sys::api!(graphics.newBitmapTable) + } + + fn free_bitmap_table(&self) -> unsafe extern "C" fn(table: *mut LCDBitmapTable) { + *sys::api!(graphics.freeBitmapTable) + } + + fn load_bitmap_table( + &self) + -> unsafe extern "C" fn(path: *const c_char, out_err: *mut *const c_char) -> *mut LCDBitmapTable { + *sys::api!(graphics.loadBitmapTable) + } + fn load_into_bitmap_table( + &self) + -> unsafe extern "C" fn(path: *const c_char, table: *mut LCDBitmapTable, out_err: *mut *const c_char) { + *sys::api!(graphics.loadIntoBitmapTable) + } + fn get_table_bitmap(&self) + -> unsafe extern "C" fn(table: *mut LCDBitmapTable, idx: c_int) -> *mut LCDBitmap { + *sys::api!(graphics.getTableBitmap) + } + } +} diff --git a/api/gfx/src/error.rs b/api/gfx/src/error.rs new file mode 100644 index 00000000..baf23811 --- /dev/null +++ b/api/gfx/src/error.rs @@ -0,0 +1,39 @@ +use core::fmt; + + +pub type ApiError = sys::error::Error; + + +#[derive(Debug)] +pub enum Error { + /// Causes when loading graphics from path fails. + /// This occurs when file does not exist or invalid format. + Fs(fs::error::Error), + /// Causes when allocation failed and/or null-ptr returned. + Alloc, + + /// Mask must be the same size as the target bitmap. + InvalidMask, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + Error::Fs(err) => err.fmt(f), + Error::Alloc => write!(f, "Allocation failed"), + Error::InvalidMask => write!(f, "Mask must be the same size as the target bitmap"), + } + } +} + +impl From for Error { + fn from(err: fs::error::Error) -> Self { Error::Fs(err) } +} + + +impl Into for Error { + fn into(self) -> ApiError { ApiError::Api(self) } +} + + +impl core::error::Error for Error {} diff --git a/api/gfx/src/lib.rs b/api/gfx/src/lib.rs new file mode 100644 index 00000000..57e06e63 --- /dev/null +++ b/api/gfx/src/lib.rs @@ -0,0 +1,221 @@ +#![cfg_attr(not(test), no_std)] +#![feature(error_in_core)] + +extern crate sys; +extern crate alloc; +pub extern crate color; + +pub mod error; +pub mod text; +pub mod bitmap { + mod bitmap; + pub mod api; + pub mod table; + pub use bitmap::*; +} + +pub use bitmap::get_debug_bitmap; +pub use bitmap::get_display_buffer_bitmap; +pub use bitmap::copy_frame_buffer_bitmap; + +pub use bitmap::set_stencil; +pub use bitmap::set_stencil_tiled; +pub use bitmap::set_draw_mode; +pub use bitmap::push_context; +pub use bitmap::pop_context; +use sys::ffi::LCDPolygonFillRule; +use sys::ffi::LCDSolidColor; + + +use core::ffi::c_float; +use core::ffi::c_int; +use error::ApiError; +use sys::ffi::LCDColor; +use sys::ffi::LCD_ROWS; +use sys::ffi::LCD_ROWSIZE; + + +unsafe fn as_slice_mut(buf: *mut u8) -> Result<&'static mut [u8], ApiError> { + if !buf.is_null() { + Ok(core::slice::from_raw_parts_mut( + buf, + (LCD_ROWSIZE * LCD_ROWS) as usize, + )) + } else { + Err(sys::error::NullPtrError.into()) + } +} + + +/// Returns the current display frame buffer. +/// Rows are 32-bit aligned, so the row stride is 52 bytes, with the extra 2 bytes per row ignored. +/// Bytes are MSB-ordered; i.e., the pixel in column 0 is the 0x80 bit of the first byte of the row. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::getFrame`]. +pub fn get_frame() -> Result<&'static mut [u8], ApiError> { + let f = *sys::api!(graphics.getFrame); + unsafe { as_slice_mut(f()) } +} + + +/// Returns the raw bits in the display buffer, +/// __the last completed frame__. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::getDisplayFrame`]. +pub fn get_display_frame() -> Result<&'static mut [u8], ApiError> { + let f = *sys::api!(graphics.getDisplayFrame); + unsafe { as_slice_mut(f()) } +} + +/// After updating pixels in the buffer returned by [`get_frame`], +/// you must tell the graphics system which rows were updated. +/// +/// This function marks a contiguous range of rows as updated +/// (e.g., `markUpdatedRows(0, LCD_ROWS-1)` tells the system to update the entire display). +/// +/// Both `start` and `end` are __included__ in the range. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::markUpdatedRows`]. +pub fn mark_updated_rows(start: c_int, end: c_int) { + let f = *sys::api!(graphics.markUpdatedRows); + unsafe { f(start, end) } +} + +/// Manually flushes the current frame buffer out to the display. +/// This function is automatically called after each pass through the run loop, +/// so there shouldn’t be any need to call it yourself. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::display`]. +pub fn display() { + let f = *sys::api!(graphics.display); + unsafe { f() } +} + +/// Clears the entire display, filling it with `color`. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::clear`]. +#[inline(always)] +pub fn clear(color: color::Color) { clear_raw(color.into()) } + + +/// Clears the entire display, filling it with `color`. +/// +/// Same as [`clear`], but without conversion `Color` -> `LCDColor`. +/// That conversion is really cheap, +/// so this function is useful if you're working with `LCDColor` directly. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::clear`]. +pub fn clear_raw(color: LCDColor) { + let f = *sys::api!(graphics.clear); + unsafe { f(color) } +} + +/// Sets the current clip rect in __screen__ coordinates. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::setScreenClipRect`]. +pub fn set_screen_clip_rect(x: c_int, y: c_int, width: c_int, height: c_int) { + let f = *sys::api!(graphics.setScreenClipRect); + unsafe { f(x, y, width, height) } +} + +/// Equivalent to [`sys::ffi::playdate_graphics::setDrawOffset`]. +pub fn set_draw_offset(dx: c_int, dy: c_int) { + let f = *sys::api!(graphics.setDrawOffset); + unsafe { f(dx, dy) } +} + +/// Sets the current clip rect, using __world__ coordinates that is, +/// the given rectangle will be translated by the current drawing offset. +/// +/// The clip rect is cleared at the beginning of each update. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::setClipRect`]. +pub fn set_clip_rect(x: c_int, y: c_int, width: c_int, height: c_int) { + let f = *sys::api!(graphics.setClipRect); + unsafe { f(x, y, width, height) } +} + +/// Equivalent to [`sys::ffi::playdate_graphics::clearClipRect`]. +pub fn clear_clip_rect() { + let f = *sys::api!(graphics.clearClipRect); + unsafe { f() } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::setBackgroundColor`]. +pub fn set_background_color(color: LCDSolidColor) { + let f = *sys::api!(graphics.setBackgroundColor); + unsafe { f(color) } +} + + +// +// Geometry +// + +/// Fills the polygon with vertices at the given coordinates +/// (an array of `2 * num_points` ints containing alternating x and y values) +/// using the given `color` and fill, or winding, `rule`. +/// +/// See [https://en.wikipedia.org/wiki/Nonzero-rule](https://en.wikipedia.org/wiki/Nonzero-rule) for an explanation of the winding rule. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::fillPolygon`]. +pub fn fill_polygon(num_points: c_int, coords: &mut [c_int], color: LCDColor, rule: LCDPolygonFillRule) { + let f = *sys::api!(graphics.fillPolygon); + unsafe { f(num_points, coords.as_mut_ptr(), color, rule) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::drawLine`]. +pub fn draw_line(x1: c_int, y1: c_int, x2: c_int, y2: c_int, width: c_int, color: LCDColor) { + let f = *sys::api!(graphics.drawLine); + unsafe { f(x1, y1, x2, y2, width, color) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::fillTriangle`]. +pub fn fill_triangle(x1: c_int, y1: c_int, x2: c_int, y2: c_int, x3: c_int, y3: c_int, color: LCDColor) { + let f = *sys::api!(graphics.fillTriangle); + unsafe { f(x1, y1, x2, y2, x3, y3, color) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::drawRect`]. +pub fn draw_rect(x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) { + let f = *sys::api!(graphics.drawRect); + unsafe { f(x, y, width, height, color) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::fillRect`]. +pub fn fill_rect(x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) { + let f = *sys::api!(graphics.fillRect); + unsafe { f(x, y, width, height, color) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::drawEllipse`]. +pub fn draw_ellipse(x: c_int, + y: c_int, + width: c_int, + height: c_int, + line_width: c_int, + start_angle: c_float, + end_angle: c_float, + color: LCDColor) { + let f = *sys::api!(graphics.drawEllipse); + unsafe { f(x, y, width, height, line_width, start_angle, end_angle, color) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::fillEllipse`]. +pub fn fill_ellipse(x: c_int, + y: c_int, + width: c_int, + height: c_int, + start_angle: c_float, + end_angle: c_float, + color: LCDColor) { + let f = *sys::api!(graphics.fillEllipse); + unsafe { f(x, y, width, height, start_angle, end_angle, color) } +} diff --git a/api/gfx/src/text.rs b/api/gfx/src/text.rs new file mode 100644 index 00000000..b626572b --- /dev/null +++ b/api/gfx/src/text.rs @@ -0,0 +1,64 @@ +// TODO: text/font +// loadFont +// getFontPage +// getPageGlyph +// getGlyphKerning +// getFontHeight +// setTextLeading +// setTextTracking +// setLineCapStyle +// setFont + +use core::ffi::c_int; + +use alloc::ffi::NulError; +use sys::ffi::{CString, CStr, LCDFont}; +use sys::ffi::PDStringEncoding; + + +const UTF: PDStringEncoding = PDStringEncoding::kUTF8Encoding; + + +/// Equivalent to [`sys::ffi::playdate_graphics::drawText`]. +pub fn draw_text>(text: S, x: c_int, y: c_int) -> Result { + let s = CString::new(text.as_ref())?; + let f = *sys::api!(graphics.drawText); + let res = unsafe { f(s.as_ptr().cast(), text.as_ref().len(), UTF, x, y) }; + Ok(res) +} + +/// Same as [`draw_text`] but takes a [`sys::ffi::CStr`], +/// but little bit more efficient. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::drawText`]. +pub fn draw_text_cstr(text: &CStr, x: c_int, y: c_int) -> c_int { + let f = *sys::api!(graphics.drawText); + let len = text.to_bytes().len(); + unsafe { f(text.as_ptr().cast(), len, UTF, x, y) } +} + + +/// Equivalent to [`sys::ffi::playdate_graphics::getTextWidth`]. +pub fn get_text_width>(text: S, + font: Option<&LCDFont>, + tracking: c_int) + -> Result { + let s = CString::new(text.as_ref())?; + let f = *sys::api!(graphics.getTextWidth); + let font = font.map(|font| font as *const LCDFont as *mut LCDFont) + .unwrap_or(core::ptr::null_mut()); + let res = unsafe { f(font, s.as_ptr().cast(), text.as_ref().len(), UTF, tracking) }; + Ok(res) +} + +/// Same as [`get_text_width`] but takes a [`sys::ffi::CStr`], +/// but little bit more efficient. +/// +/// Equivalent to [`sys::ffi::playdate_graphics::getTextWidth`]. +pub fn get_text_width_cstr(text: &CStr, font: Option<&LCDFont>, tracking: c_int) -> c_int { + let f = *sys::api!(graphics.getTextWidth); + let len = text.to_bytes().len(); + let font = font.map(|font| font as *const LCDFont as *mut LCDFont) + .unwrap_or(core::ptr::null_mut()); + unsafe { f(font, text.as_ptr().cast(), len, UTF, tracking) } +} diff --git a/api/menu/Cargo.toml b/api/menu/Cargo.toml new file mode 100644 index 00000000..c78cdd59 --- /dev/null +++ b/api/menu/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "playdate-menu" +version = "0.1.0" +edition = "2021" + +readme = "README.md" +authors = ["Alex Koz "] +description = "High-level system menu API built on-top of Playdate API" +homepage = "https://github.com/boozook/playdate" +repository = "https://github.com/boozook/playdate.git" + + +[features] +default = ["sys/default"] +# sys- features: +lang-items = ["sys/lang-items"] +allocator = ["sys/allocator"] +panic-handler = ["sys/panic-handler"] +eh-personality = ["sys/eh-personality"] +error-ctx = ["sys/error-ctx"] +bindgen-runtime = ["sys/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static"] +bindings-derive-default = ["sys/bindings-derive-default"] +bindings-derive-eq = ["sys/bindings-derive-eq"] +bindings-derive-copy = ["sys/bindings-derive-copy"] +bindings-derive-debug = ["sys/bindings-derive-debug"] +bindings-derive-hash = ["sys/bindings-derive-hash"] +bindings-derive-ord = ["sys/bindings-derive-ord"] +bindings-derive-partialeq = ["sys/bindings-derive-partialeq"] +bindings-derive-partialord = ["sys/bindings-derive-partialord"] +bindings-derive-constparamty = ["sys/bindings-derive-constparamty"] +bindings-documentation = ["sys/bindings-documentation"] + + +[dependencies.sys] +version = "0.1" +path = "../sys" +package = "playdate-sys" +default-features = false + + +[[example]] +name = "menu" +crate-type = ["dylib", "staticlib"] +path = "examples/menu.rs" + +[[example]] +name = "custom" +crate-type = ["dylib", "staticlib"] +path = "examples/custom-api-access.rs" + + +[package.metadata.playdate] +bundle-id = "rs.playdate.menu" diff --git a/api/menu/README.md b/api/menu/README.md new file mode 100644 index 00000000..883db9e3 --- /dev/null +++ b/api/menu/README.md @@ -0,0 +1,26 @@ +# System Menu API for PlayDate + +High-level system menu API built on-top of [playdate-sys][]. + + +## Usage + +```rust +extern crate playdate_menu; +use playdate_menu::*; + +fn callback(userdata: &mut u32) { *userdata += 1 } + +let simple = SimpleMenuItem::new("Simple", Some(callback), 0); +let check = CheckMenuItem::new("Check", false, None, ()); +let opts = OptionsMenuItem::new("Opts", ["No", "Yes"], None, ()); +``` + + +[playdate-sys]: https://crates.io/crates/playdate-sys + + + +- - - + +This software is not sponsored or supported by Panic. diff --git a/api/menu/examples/README.md b/api/menu/examples/README.md new file mode 100644 index 00000000..ffd7cf44 --- /dev/null +++ b/api/menu/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level, except for the parts that directly demonstrate the functionality of this package. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-menu --example=menu --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-menu --example=custom --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/menu/examples/custom-api-access.rs b/api/menu/examples/custom-api-access.rs new file mode 100644 index 00000000..577f277b --- /dev/null +++ b/api/menu/examples/custom-api-access.rs @@ -0,0 +1,213 @@ +#![no_std] +#[macro_use] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_menu as menu; + +use core::ffi::*; +use alloc::boxed::Box; + +use sys::ffi::*; +use menu::*; +use menu::api::*; + + +const INITIAL_X: u32 = LCD_COLUMNS / 2; +const INITIAL_Y: u32 = (LCD_ROWS - TEXT_HEIGHT) / 2; +const TEXT_HEIGHT: u32 = 16; + + +const MAX_CLICKS: u32 = 3; + + +#[derive(Debug, Clone, Copy)] +pub struct CustomApi(&'static playdate_sys); + +impl CustomApi { + fn new() -> Self { Self(sys::api!(system)) } +} + +impl<'t> Api for CustomApi { + fn get_menu_item_value(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> c_int { + self.0.getMenuItemValue.unwrap() + } + + fn set_menu_item_value(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem, value: c_int) { + self.0.setMenuItemValue.unwrap() + } + + fn get_menu_item_title(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> *const c_char { + self.0.getMenuItemTitle.unwrap() + } + + fn set_menu_item_title(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem, title: *const c_char) { + self.0.setMenuItemTitle.unwrap() + } + + fn get_menu_item_userdata(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> *mut c_void { + self.0.getMenuItemUserdata.unwrap() + } +} + + +/// App state +struct State { + first: Option>, + second: Option, CustomApi>>, + third: Option>, +} + +impl State { + const fn new() -> Self { + Self { first: None, + second: None, + third: None } + } + + + /// Updates the state + fn update(&mut self) -> Option<()> { + // remove first menu item if limit reached + if let Some((_item, value)) = self.first + .as_ref() + .map(|item| item.get_userdata().map(|val| (item, val))) + .flatten() + { + if *value >= MAX_CLICKS { + let item = self.first.take().unwrap(); + let value = item.remove(); + println!("First item removed on click {value:?}"); + } + } + + // remove third menu item if requested + if let Some((_item, value)) = self.third.as_ref().map(|item| (item, item.selected_option())) { + if value != 0 { + self.third.take(); + } + } + + + const LABEL_DEF: &str = "Use System Menu\0"; + + let cstr = CStr::from_bytes_with_nul(LABEL_DEF.as_bytes()).unwrap(); + + unsafe { + let graphics = (*sys::API).graphics; + (*graphics).clear?(LCDSolidColor::kColorWhite as LCDColor); + + // get width (screen-size) of text + let text_width = (*graphics).getTextWidth?( + core::ptr::null_mut(), + cstr.as_ptr() as *const _, + LABEL_DEF.len(), + PDStringEncoding::kUTF8Encoding, + 0, + ); + // render text + (*graphics).drawText?( + cstr.as_ptr() as *const _, + LABEL_DEF.len(), + PDStringEncoding::kUTF8Encoding, + INITIAL_X as c_int - text_width / 2, + INITIAL_Y.try_into().unwrap(), + ); + } + Some(()) + } + + + /// Event handler + fn event(&'static mut self, event: PDSystemEvent) -> Option<()> { + match event { + // initial setup + PDSystemEvent::kEventInit => unsafe { + (*(*sys::API).display).setRefreshRate?(20.0); + + + let api = CustomApi::new(); + + + fn callback(userdata: &mut u32) { + println!("Check menu item clicked {userdata} times."); + *userdata += 1; + } + self.first = SimpleMenuItem::new_with(api.clone(), "Check Me", Some(callback), 0).ok(); + + + fn change_first(state: &mut Option<&'static State>) { + if let Some(state) = state { + if let Some(item) = state.first.as_ref() { + if let Some(value) = item.get_userdata() { + item.set_title(format!("Clicked: {value}/{MAX_CLICKS}")).unwrap(); + } else { + println!("No user-data") + } + } else { + println!("No menu item") + } + } else { + println!("No state") + } + } + self.second = + CheckMenuItem::new_with(api.clone(), "Change^", false, Some(change_first), None).unwrap() + .into(); + let second = self.second.as_ref().unwrap(); + second.set_userdata(Some(self)); + + + self.third = OptionsMenuItem::new_with(api.clone(), "Remove?", ["No", "Yes"], None, ()).unwrap() + .into(); + }, + _ => {}, + } + Some(()) + } +} + + +#[no_mangle] +/// Proxy event handler, calls `State::event` +pub extern "C" fn eventHandlerShim(api: *const PlaydateAPI, event: PDSystemEvent, _arg: u32) -> c_int { + static mut STATE: Option> = None; + + match event { + PDSystemEvent::kEventInit => unsafe { + // register the API entry point + sys::API = api; + + // create game state + if STATE.is_none() { + STATE = Some(Box::new(State::new())); + } + let state = STATE.as_mut().unwrap().as_mut() as *mut State; + + // get `setUpdateCallback` fn + let f = (*(*api).system).setUpdateCallback.expect("setUpdateCallback"); + // register update callback with user-data = our state + f(Some(on_update), state.cast()); + }, + _ => {}, + } + + if let Some(state) = unsafe { STATE.as_mut() } { + state.event(event).and(Some(0)).unwrap_or(1) + } else { + 1 + } +} + + +/// Proxy update callback, calls `State::update` +unsafe extern "C" fn on_update(state: *mut c_void) -> i32 { + let ptr: *mut State = state.cast(); + let state = ptr.as_mut().expect("missed state"); + state.update().and(Some(1)).unwrap_or_default() +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/menu/examples/menu.rs b/api/menu/examples/menu.rs new file mode 100644 index 00000000..c5378b62 --- /dev/null +++ b/api/menu/examples/menu.rs @@ -0,0 +1,176 @@ +#![no_std] +#[macro_use] +extern crate alloc; + +#[macro_use] +extern crate sys; +extern crate playdate_menu as menu; + +use core::ffi::*; +use alloc::boxed::Box; + +use sys::ffi::*; +use menu::*; + + +const INITIAL_X: u32 = LCD_COLUMNS / 2; +const INITIAL_Y: u32 = (LCD_ROWS - TEXT_HEIGHT) / 2; +const TEXT_HEIGHT: u32 = 16; + + +const MAX_CLICKS: u32 = 3; + +/// App state +struct State { + first: Option>, + second: Option>>, + third: Option, +} + +impl State { + const fn new() -> Self { + Self { first: None, + second: None, + third: None } + } + + + /// Updates the state + fn update(&mut self) -> Option<()> { + // remove first menu item if limit reached + if let Some((_item, value)) = self.first + .as_ref() + .map(|item| item.get_userdata().map(|val| (item, val))) + .flatten() + { + if *value >= MAX_CLICKS { + let item = self.first.take().unwrap(); + let value = item.remove(); + println!("First item removed on click {value:?}"); + } + } + + // remove third menu item if requested + if let Some((_item, value)) = self.third.as_ref().map(|item| (item, item.selected_option())) { + if value != 0 { + self.third.take(); + } + } + + + const LABEL_DEF: &str = "Use System Menu\0"; + + let cstr = CStr::from_bytes_with_nul(LABEL_DEF.as_bytes()).unwrap(); + + unsafe { + let graphics = (*sys::API).graphics; + (*graphics).clear?(LCDSolidColor::kColorWhite as LCDColor); + + // get width (screen-size) of text + let text_width = (*graphics).getTextWidth?( + core::ptr::null_mut(), + cstr.as_ptr() as *const _, + LABEL_DEF.len(), + PDStringEncoding::kUTF8Encoding, + 0, + ); + // render text + (*graphics).drawText?( + cstr.as_ptr() as *const _, + LABEL_DEF.len(), + PDStringEncoding::kUTF8Encoding, + INITIAL_X as c_int - text_width / 2, + INITIAL_Y.try_into().unwrap(), + ); + } + Some(()) + } + + + /// Event handler + fn event(&'static mut self, event: PDSystemEvent) -> Option<()> { + match event { + // initial setup + PDSystemEvent::kEventInit => unsafe { + (*(*sys::API).display).setRefreshRate?(20.0); + + fn callback(userdata: &mut u32) { + println!("Check menu item clicked {userdata} times."); + *userdata += 1; + } + self.first = SimpleMenuItem::new("Check Me", Some(callback), 0).unwrap().into(); + + + fn change_first(state: &mut Option<&'static State>) { + if let Some(state) = state { + if let Some(item) = state.first.as_ref() { + if let Some(value) = item.get_userdata() { + item.set_title(format!("Clicked: {value}/{MAX_CLICKS}")).unwrap(); + } else { + println!("No user-data") + } + } else { + println!("No menu item") + } + } else { + println!("No state") + } + } + self.second = CheckMenuItem::new("Change^", false, Some(change_first), None).unwrap() + .into(); + let second = self.second.as_ref().unwrap(); + second.set_userdata(Some(self)); + + + self.third = OptionsMenuItem::new("Remove?", ["No", "Yes"], None, ()).unwrap() + .into(); + }, + _ => {}, + } + Some(()) + } +} + + +#[no_mangle] +/// Proxy event handler, calls `State::event` +pub extern "C" fn eventHandlerShim(api: *const PlaydateAPI, event: PDSystemEvent, _arg: u32) -> c_int { + static mut STATE: Option> = None; + + match event { + PDSystemEvent::kEventInit => unsafe { + // register the API entry point + sys::API = api; + + // create game state + if STATE.is_none() { + STATE = Some(Box::new(State::new())); + } + let state = STATE.as_mut().unwrap().as_mut() as *mut State; + + // get `setUpdateCallback` fn + let f = (*(*api).system).setUpdateCallback.expect("setUpdateCallback"); + // register update callback with user-data = our state + f(Some(on_update), state.cast()); + }, + _ => {}, + } + + if let Some(state) = unsafe { STATE.as_mut() } { + state.event(event).and(Some(0)).unwrap_or(1) + } else { + 1 + } +} + + +/// Proxy update callback, calls `State::update` +unsafe extern "C" fn on_update(state: *mut c_void) -> i32 { + let ptr: *mut State = state.cast(); + let state = ptr.as_mut().expect("missed state"); + state.update().and(Some(1)).unwrap_or_default() +} + + +// Needed for debug build +ll_symbols!(); diff --git a/api/menu/src/error.rs b/api/menu/src/error.rs new file mode 100644 index 00000000..af8c555a --- /dev/null +++ b/api/menu/src/error.rs @@ -0,0 +1,27 @@ +use core::fmt; + + +pub type ApiError = sys::error::Error; + + +#[derive(Debug)] +pub enum Error { + /// Causes when allocation failed and/or null-ptr returned. + Alloc, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + Error::Alloc => write!(f, "Menu: Allocation failed"), + } + } +} + + +impl Into for Error { + fn into(self) -> ApiError { ApiError::Api(self) } +} + + +impl core::error::Error for Error {} diff --git a/api/menu/src/lib.rs b/api/menu/src/lib.rs new file mode 100644 index 00000000..19a63865 --- /dev/null +++ b/api/menu/src/lib.rs @@ -0,0 +1,396 @@ +#![cfg_attr(not(test), no_std)] +#![feature(error_in_core)] + +pub extern crate sys; +pub extern crate alloc; + +pub mod error; + + +use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::ffi::NulError; +use core::ffi::c_int; +use core::ffi::c_char; +use core::ffi::c_void; +use core::marker::PhantomData; +use sys::ffi::PDMenuItemCallbackFunction; +use sys::ffi::PDMenuItem; +use sys::ffi::CStr; +use sys::ffi::CString; + +use error::{Error, ApiError}; + + +pub type SimpleMenuItem = + MenuItem; + +pub type CheckMenuItem = + MenuItem; + +pub type OptionsMenuItem = + MenuItem; + + +#[cfg_attr(feature = "bindings-derive-debug", derive(Debug))] +pub struct MenuItem(*mut PDMenuItem, Api, PhantomData, PhantomData) where Kind: kind::Kind, UserData: Into>, Api: api::Api; + + +impl MenuItem { + pub fn get_title(&self) -> Cow<'_, str> { + let f = self.1.get_menu_item_title(); + unsafe { CStr::from_ptr(f(self.0) as _) }.to_string_lossy() + } + + pub fn set_title>(&self, title: S) -> Result<(), NulError> { + let f = self.1.set_menu_item_title(); + let s = CString::new(title.as_ref())?; + unsafe { f(self.0, s.as_ptr() as *mut c_char) }; + core::mem::drop(s); + Ok(()) + } + + pub fn get_userdata(&self) -> Option<&mut UD> { self.get_userdata_full().map(|(_, ud)| ud) } + + fn get_userdata_full(&self) -> Option<&mut CallbackUserData> { + let f = self.1.get_menu_item_userdata(); + let ptr = unsafe { f(self.0) }; + if ptr.is_null() + /* TODO: or miss-aligned */ + { + return None; + } + + unsafe { (ptr as *mut CallbackUserData).as_mut() } + } + + pub fn set_userdata(&self, userdata: UD) -> Option { + if let Some(existing) = self.get_userdata() { + // *ud = userdata + core::mem::replace(existing, userdata).into() + } else { + todo!() + } + } + + pub fn get_value(&self) -> c_int { + let f = self.1.get_menu_item_value(); + unsafe { f(self.0) } + } + + pub fn set_value(&self, value: c_int) { + let f = self.1.set_menu_item_value(); + unsafe { f(self.0, value) } + } + + + fn take_userdata(&mut self) -> Option { + if self.0.is_null() { + return None; + } + + let f = self.1.get_menu_item_userdata(); + let ptr = unsafe { f(self.0) }; + if ptr.is_null() { + return None; + } else if core::mem::size_of::() == 0 + // || ptr.addr() & (core::mem::align_of::>() - 1) != 0 + { + // invalid pointer, mostly means that the userdata was not set/initialized + return None; + } + + let ud: CallbackUserData = *unsafe { Box::from_raw(ptr as *mut CallbackUserData) }; + let (_, userdata) = ud; + Some(userdata) + } +} + + +impl MenuItem { + #[inline(always)] + pub fn new>(title: S, + checked: bool, + callback: Option>, + userdata: UD) + -> Result + where Api: Default + { + Self::new_with(Api::default(), title, checked, callback, userdata) + } + + pub fn new_with>(api: Api, + title: S, + checked: bool, + callback: Option>, + userdata: UD) + -> Result { + let (callback, userdata) = proxy_menu_parts::<_, _>(callback, userdata); + let title = CString::new(title.as_ref())?; + + let ctor = api.add_checkmark_menu_item(); + let ptr = unsafe { ctor(title.as_ptr() as *mut c_char, checked as _, callback, userdata) }; + + if ptr.is_null() { + Err(Error::Alloc.into()) + } else { + Ok(MenuItem(ptr, api, PhantomData, PhantomData)) + } + } +} + + +impl MenuItem { + #[inline(always)] + pub fn new>(title: S, + callback: Option>, + userdata: UD) + -> Result + where Api: Default + { + Self::new_with(Api::default(), title, callback, userdata) + } + + pub fn new_with>(api: Api, + title: S, + callback: Option>, + userdata: UD) + -> Result { + let (callback, userdata) = proxy_menu_parts::<_, _>(callback, userdata); + let title = CString::new(title.as_ref())?; + + let ctor = api.add_menu_item(); + let ptr = unsafe { ctor(title.as_ptr() as *mut c_char, callback, userdata) }; + + if ptr.is_null() { + Err(Error::Alloc.into()) + } else { + Ok(MenuItem(ptr, api, PhantomData, PhantomData::)) + } + } +} + +impl MenuItem { + #[inline(always)] + pub fn new, O: AsRef<[S]>>(title: S, + options: O, + callback: Option>, + userdata: UD) + -> Result + where Api: Default + { + Self::new_with(Api::default(), title, options, callback, userdata) + } + + pub fn new_with, O: AsRef<[S]>>(api: Api, + title: S, + options: O, + callback: Option>, + userdata: UD) + -> Result { + use alloc::vec::Vec; + + + let (callback, userdata) = proxy_menu_parts::<_, _>(callback, userdata); + let title = CString::new(title.as_ref())?; + + let options = options.as_ref(); + let mut opts = Vec::with_capacity(options.len()); + for opt in options { + let opt = CString::new(opt.as_ref())?; + opts.push(opt) + } + let mut ptrs = Vec::with_capacity(options.len()); + ptrs.extend(opts.iter().map(|s| s.as_ptr())); + + let ctor = api.add_options_menu_item(); + let ptr = unsafe { + ctor( + title.as_ptr() as *mut c_char, + ptrs.as_mut_ptr() as _, + ptrs.len() as _, + callback, + userdata, + ) + }; + + core::mem::drop(ptrs); + core::mem::drop(opts); + + if ptr.is_null() { + Err(Error::Alloc.into()) + } else { + Ok(MenuItem(ptr, api, PhantomData, PhantomData::)) + } + } +} + + +#[inline(always)] +fn proxy_menu_parts(cb: Option, + ud: UD) + -> (PDMenuItemCallbackFunction, *mut c_void) { + unsafe extern "C" fn proxy(userdata: *mut c_void) { + if let Some((callback, userdata)) = (userdata as *mut CallbackUserData).as_mut() { + callback(userdata) + } else { + panic!("user callback missed"); + } + } + + if let Some(callback) = cb { + // convert (callback, userdata) -> pointer: + let ptr = Box::into_raw(Box::from((callback, ud))); + (Some(proxy:: as _), ptr as *mut _) + } else { + // we can get user data smaller: + // convert userdata -> pointer: + // let ptr = Box::into_raw(Box::from(userdata)); + // Ok((None, ptr as _)) + + // but better to have same for consistency, + // required for get/set userdata: + fn noop(_: &mut UserData) {} + let ptr = Box::into_raw(Box::from((noop::, ud))); + (None, ptr as *mut _) + } +} + + +impl MenuItem { + #[inline(always)] + pub fn is_checked(&self) -> bool { self.get_value() == 1 } +} + + +impl MenuItem { + #[inline(always)] + /// The array index of the currently selected option. + pub fn selected_option(&self) -> i32 { self.get_value() } +} + + +impl Drop for MenuItem { + fn drop(&mut self) { + if REM && !self.0.is_null() { + // we have to drop userdata: + self.take_userdata(); + let f = self.1.remove_menu_item(); + unsafe { f(self.0) }; + } + } +} + +impl MenuItem { + #[inline(always)] + pub fn remove(mut self) -> Option { + let ud = self.take_userdata(); + + let f = self.1.remove_menu_item(); + unsafe { f(self.0) }; + self.0 = core::ptr::null_mut() as _; + + ud + } +} + + +pub fn remove_all_menu_items() { + use api::Api; + let f = api::Default::default().remove_all_menu_items(); + unsafe { f() }; +} + + +type CallbackUserData = (MenuItemCallback, UserData); +type MenuItemCallback = fn(userdata: &mut T); + + +pub mod kind { + pub trait Kind {} + + + #[derive(Debug)] + pub struct Simple; + + #[derive(Debug)] + pub struct Check; + + #[derive(Debug)] + pub struct Options; + + impl Kind for Simple {} + impl Kind for Check {} + impl Kind for Options {} +} + + +pub mod api { + use core::ffi::c_char; + use core::ffi::c_int; + use core::ffi::c_void; + use sys::ffi::PDMenuItem; + use sys::ffi::PDMenuItemCallbackFunction; + + + #[derive(Debug, Clone, Copy, core::default::Default)] + pub struct Default; + + impl Api for Default {} + + + pub trait Api { + fn add_menu_item( + &self) + -> unsafe extern "C" fn(title: *const c_char, + callback: PDMenuItemCallbackFunction, + userdata: *mut c_void) -> *mut PDMenuItem { + *sys::api!(system.addMenuItem) + } + + fn add_checkmark_menu_item( + &self) + -> unsafe extern "C" fn(title: *const c_char, + value: c_int, + callback: PDMenuItemCallbackFunction, + userdata: *mut c_void) -> *mut PDMenuItem { + *sys::api!(system.addCheckmarkMenuItem) + } + + fn add_options_menu_item( + &self) + -> unsafe extern "C" fn(title: *const c_char, + optionTitles: *mut *const c_char, + optionsCount: c_int, + f: PDMenuItemCallbackFunction, + userdata: *mut c_void) -> *mut PDMenuItem { + *sys::api!(system.addOptionsMenuItem) + } + + fn remove_menu_item(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) { + *sys::api!(system.removeMenuItem) + } + + fn get_menu_item_value(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> c_int { + *sys::api!(system.getMenuItemValue) + } + + fn set_menu_item_value(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem, value: c_int) { + *sys::api!(system.setMenuItemValue) + } + + fn get_menu_item_title(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> *const c_char { + *sys::api!(system.getMenuItemTitle) + } + + fn set_menu_item_title(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem, title: *const c_char) { + *sys::api!(system.setMenuItemTitle) + } + + fn get_menu_item_userdata(&self) -> unsafe extern "C" fn(menuItem: *mut PDMenuItem) -> *mut c_void { + *sys::api!(system.getMenuItemUserdata) + } + + fn remove_all_menu_items(&self) -> unsafe extern "C" fn() { *sys::api!(system.removeAllMenuItems) } + } +} diff --git a/api/sound/Cargo.toml b/api/sound/Cargo.toml index f3d96c5d..b38ed88f 100644 --- a/api/sound/Cargo.toml +++ b/api/sound/Cargo.toml @@ -13,33 +13,34 @@ repository = "https://github.com/boozook/playdate.git" [features] default = ["sys/default"] # sys- features: -lang-items = ["sys/lang-items"] -allocator = ["sys/allocator"] -panic-handler = ["sys/panic-handler"] -eh-personality = ["sys/eh-personality"] -error-ctx = ["sys/error-ctx"] -bindgen-runtime = ["sys/bindgen-runtime"] -bindings-pre-built = ["sys/bindings-pre-built"] -bindings-generate = ["sys/bindings-generate"] -bindings-derive-default = ["sys/bindings-derive-default"] -bindings-derive-eq = ["sys/bindings-derive-eq"] -bindings-derive-copy = ["sys/bindings-derive-copy"] -bindings-derive-debug = ["sys/bindings-derive-debug"] -bindings-derive-hash = ["sys/bindings-derive-hash"] -bindings-derive-ord = ["sys/bindings-derive-ord"] -bindings-derive-partialeq = ["sys/bindings-derive-partialeq"] -bindings-derive-partialord = ["sys/bindings-derive-partialord"] -bindings-derive-constparamty = ["sys/bindings-derive-constparamty"] -bindings-documentation = ["sys/bindings-documentation"] +lang-items = ["sys/lang-items", "fs/lang-items"] +allocator = ["sys/allocator", "fs/allocator"] +panic-handler = ["sys/panic-handler", "fs/panic-handler"] +eh-personality = ["sys/eh-personality", "fs/eh-personality"] +error-ctx = ["sys/error-ctx", "fs/error-ctx"] +bindgen-runtime = ["sys/bindgen-runtime", "fs/bindgen-runtime"] +bindgen-static = ["sys/bindgen-static", "fs/bindgen-static"] +bindings-derive-default = ["sys/bindings-derive-default", "fs/bindings-derive-default"] +bindings-derive-eq = ["sys/bindings-derive-eq", "fs/bindings-derive-eq"] +bindings-derive-copy = ["sys/bindings-derive-copy", "fs/bindings-derive-copy"] +bindings-derive-debug = ["sys/bindings-derive-debug", "fs/bindings-derive-debug"] +bindings-derive-hash = ["sys/bindings-derive-hash", "fs/bindings-derive-hash"] +bindings-derive-ord = ["sys/bindings-derive-ord", "fs/bindings-derive-ord"] +bindings-derive-partialeq = ["sys/bindings-derive-partialeq", "fs/bindings-derive-partialeq"] +bindings-derive-partialord = ["sys/bindings-derive-partialord", "fs/bindings-derive-partialord"] +bindings-derive-constparamty = ["sys/bindings-derive-constparamty", "fs/bindings-derive-constparamty"] +bindings-documentation = ["sys/bindings-documentation", "fs/bindings-documentation"] [dependencies.sys] path = "../sys" +version = "0.1" package = "playdate-sys" default-features = false [dependencies.fs] path = "../fs" +version = "0.1" package = "playdate-fs" default-features = false diff --git a/api/sound/examples/README.md b/api/sound/examples/README.md new file mode 100644 index 00000000..0f17bce3 --- /dev/null +++ b/api/sound/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level, except for the parts that directly demonstrate the functionality of this package. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-sound --example=fp-simple --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-sound --example=sp-simple --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/sys/Cargo.toml b/api/sys/Cargo.toml index 1ac59b26..a372f2b9 100644 --- a/api/sys/Cargo.toml +++ b/api/sys/Cargo.toml @@ -16,7 +16,12 @@ documentation = "https://docs.rs/playdate-sys" [features] -default = ["lang-items", "bindgen-runtime", "bindings-derive-debug"] +default = [ + "lang-items", + "bindgen-runtime", + "bindings-derive-debug", + # "bindings-documentation", # this should not be default +] lang-items = ["allocator", "panic-handler", "eh-personality"] # lang-items: allocator = [] # global allocator @@ -56,6 +61,8 @@ heapless = "0.7" [build-dependencies] semver = "1.0" +# TODO: make bindgen optional: +# if no bindgen => use pre-built or find installed bindgen and call [build-dependencies.bindgen] package = "playdate-bindgen" path = "../../support/bindgen" diff --git a/api/sys/README.md b/api/sys/README.md index f655aa4e..0428330a 100644 --- a/api/sys/README.md +++ b/api/sys/README.md @@ -241,7 +241,7 @@ It is possible to add extra things there only if that is: - feature-gated, in case the thing has dependencies or isn't so small. -### Development Extension +### Extension Development You can add functionality that based on this package. Just create a new your oun package on-top this and re-export all features. @@ -260,8 +260,8 @@ _This makes kind of paramount importance if a user is using several of these ext default = ["playdate-sys/default"] lang-items = ["playdate-sys/lang-items"] allocator = ["playdate-sys/allocator"] - bindings-derive-default = ["playdate-sys/bindings-derive-default"] bindings-derive-debug = ["playdate-sys/bindings-derive-debug"] + bindings-derive-default = ["playdate-sys/bindings-derive-default"] ... ``` diff --git a/api/sys/examples/README.md b/api/sys/examples/README.md new file mode 100644 index 00000000..8df7d071 --- /dev/null +++ b/api/sys/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +⚠️ All of the examples here are very low-level. + + +# How to run + +```bash +# Simulator: +cargo playdate run -p=playdate-sys --example=hello-world --features=bindgen-runtime,bindings-derive-debug +# Device: +cargo playdate run -p=playdate-sys --example=hello-world --features=bindgen-runtime,bindings-derive-debug --device +``` + +More information how to use [cargo-playdate][] in help: `cargo playdate --help`. + + + +[cargo-playdate]: https://crates.io/crates/cargo-playdate diff --git a/api/sys/src/build.rs b/api/sys/src/build.rs index 40325c84..9f6772c0 100644 --- a/api/sys/src/build.rs +++ b/api/sys/src/build.rs @@ -1,5 +1,6 @@ extern crate bindgen; +use std::borrow::Cow; use std::env; use std::error::Error; use std::path::{PathBuf, Path}; @@ -108,13 +109,21 @@ fn main() { // open_bindings(&out_path); } + +/// Needed for dev purposes. +/// Opens bindings in a `$EDITOR` or `code` if first doesn't set. +/// This is useful for reading, validating & debugging codegen results. #[allow(dead_code)] fn open_bindings(path: &Path) { - std::process::Command::new("code").arg(path) - .envs(env::vars()) - .current_dir(env::current_dir().expect("PWD")) - .spawn() - .ok(); + let editor = std::env::var("EDITOR").map(Cow::from) + .unwrap_or_else(|_| "code".into()); + let mut editor = editor.split(" "); + std::process::Command::new(editor.next().unwrap()).args(editor) + .arg(path) + .envs(env::vars()) + .current_dir(env::current_dir().expect("PWD")) + .spawn() + .ok(); } diff --git a/api/sys/src/lib.rs b/api/sys/src/lib.rs index 79d1d080..944a5371 100644 --- a/api/sys/src/lib.rs +++ b/api/sys/src/lib.rs @@ -120,6 +120,12 @@ pub mod misc { #[doc(hidden)] #[no_mangle] pub extern "C" fn _getpid() -> core::ffi::c_int { 0 } + + #[doc(hidden)] + #[no_mangle] + #[cfg(target_os = "windows")] + // TODO: Somehow link with proper impl: https://stackoverflow.com/q/76439798/829264 + pub extern "C" fn _sbrk() {} }; } } diff --git a/api/sys/src/sys/error/mod.rs b/api/sys/src/sys/error/mod.rs index fd7a38d8..3e406ec2 100644 --- a/api/sys/src/sys/error/mod.rs +++ b/api/sys/src/sys/error/mod.rs @@ -36,6 +36,21 @@ impl fmt::Display for Error { } } +impl Error { + pub fn from_err(error: Error) -> Self + where T: From { + match error { + Error::Api(err) => Error::Api(err.into()), + Error::Utf8(err) => Error::Utf8(err), + Error::FromUtf8(err) => Error::FromUtf8(err), + Error::CStr(err) => Error::CStr(err), + Error::NullPtr(err) => Error::NullPtr(err), + #[cfg(feature = "error-ctx")] + Error::NullPtrCtx(err) => Error::NullPtrCtx(err), + } + } +} + impl From for Error { fn from(error: Utf8Error) -> Self { Self::Utf8(error) } diff --git a/cargo-playdate.json b/cargo-playdate.json index a076632d..5cb9e19a 100644 --- a/cargo-playdate.json +++ b/cargo-playdate.json @@ -1,206 +1,288 @@ { - "$comment": "https://github.com/TODO/cargo-playdate-or-metabuild/#configuration", "$id": "https://json.schemastore.org/cargo-playdate.json", "$schema": "http://json-schema.org/draft-07/schema#", - "allOf": [ { "$ref": "http://json.schemastore.org/cargo.json" } ], + "$comment": "https://github.com/boozook/playdate", + "allOf": [ + { + "$ref": "https://json.schemastore.org/cargo.json" + } + ], "definitions": { - "Package": { - "allOf": [{ "$ref": "http://json.schemastore.org/cargo.json#/definitions/Package" }], - "type": "object", - "required": [ - "metadata" - ], - "additionalProperties": true, - "properties": { - "metadata": { - "allOf": [{ "$ref": "http://json.schemastore.org/cargo.json#/definitions/Package/properties/metadata" }], - "type": "object", - "properties": { - "playdate": { - "$ref": "#/definitions/PlaydateMetadata" - } - }, - "required": [ - "playdate" - ], - "additionalProperties": true, - "x-taplo": { - "links": { - "key": "https://doc.rust-lang.org/cargo/reference/manifest.html#the-metadata-table" - } - } - } + "Package": { + "allOf": [ + { + "$ref": "https://json.schemastore.org/cargo.json#/definitions/Package" + } + ], + "type": "object", + "required": [ + "metadata" + ], + "additionalProperties": true, + "properties": { + "metadata": { + "allOf": [ + { + "$ref": "https://json.schemastore.org/cargo.json#/definitions/Package/properties/metadata" + } + ], + "type": "object", + "properties": { + "playdate": { + "$ref": "#/definitions/PlaydateMetadata" + } + }, + "required": [ + "playdate" + ], + "additionalProperties": true, + "x-taplo": { + "links": { + "key": "https://doc.rust-lang.org/cargo/reference/manifest.html#the-metadata-table" + } + } + } + }, + "x-taplo": { + "links": { + "key": "https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section" + } + } + }, + "PlaydateMetadata": { + "title": "Playdate Package Metadata", + "description": "Metadata and build configuration.", + "type": "object", + "required": [ + "bundle-id" + ], + "properties": { + "bundle-id": { + "type": "string", + "description": "A unique identifier for your game, in reverse DNS notation.", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } }, - "x-taplo": { - "links": { - "key": "https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section" - } - } - }, - "PlaydateMetadata": { - "title": "Playdate Package Metadata", - "description": "Metadata and build configuration.", - "type": "object", - "required": [ - "bundle-id" - ], - "properties": { - "bundle-id": { - "type": "string", - "description": "A unique identifier for your game, in reverse DNS notation.", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } } - }, - "name": { - "type": "string", - "description": "A game version number, formatted any way you wish, that is displayed to players. It is not used to compute when updates should occur.", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } } - }, - "author": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } } - }, - "description": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } } - }, - "version": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } } - }, - "build-number": { - "type": "integer", - "exclusiveMinimum": 0, - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } }, - "description": "A monotonically-increasing integer value used to indicate a unique version of your game. This can be set using an automated build process like Continuous Integration to avoid having to set the value by hand.\n\nFor sideloaded games, buildNumber is required and is used to determine when a newer version is available to download." - }, - "image-path": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } }, - "description": "A directory of images that will be used by the launcher.\n\nMore in [official documentation](https://sdk.play.date/#pdxinfo)." - }, - "launch-sound-path": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } }, - "description": "Should point to the path of a short audio file to be played as the game launch animation is taking place.\n\nMore in [official documentation](https://sdk.play.date/#pdxinfo)." - }, - "content-warning": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } }, - "description": "Optional. A content warning that displays when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose." - }, - "content-warning2": { - "type": "string", - "x-taplo": { "links": { "key": "https://sdk.play.date/#pdxinfo" } }, - "description": "Optional. A second content warning that displays on a second screen when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose.\n\nNote: `content-warning2` will only display if a `content-warning` attribute is also specified.\n\nThe string displayed on the content warning screen can only be so long before it will be truncated with an \"…\" character. Be sure to keep this in mind when designing your `content-warning` and `content-warning2` text." - }, - "assets": { - "anyOf": [ - { "$ref": "#/definitions/PlaydateMetadataAssetsMap" }, - { "$ref": "#/definitions/PlaydateMetadataAssetsArray" } - ] - }, - "options": { - "$ref": "#/definitions/PlaydateMetadataOptions" - }, - "support": { - "type": "object" - } + "name": { + "type": "string", + "description": "A game version number, formatted any way you wish, that is displayed to players. It is not used to compute when updates should occur.", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } }, - "additionalProperties": false, - "x-taplo": { - "initKeys": ["bundle-id", "name", "description", "author", "image-path", "launch-sound-path"], - "links": { "key": "https://sdk.play.date/#pdxinfo" } - } - }, - "PlaydateMetadataAssetsArray": { - "type": "array", - "title": "Assets list", - "description": "List of paths to include.", - "uniqueItems": true, - "items": { - "title": "Path", - "description": "Path to include.", - "type": "string" + "author": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } }, - "x-taplo": { - "links": { - "key": "https://TODO:doc-url" - } - } - }, - "PlaydateMetadataAssetsMap": { - "type": "object", - "title": "Assets rules", - "description": "Rules used to resolve paths to include.", - "properties": { - "options": { "$ref": "#/definitions/PlaydateMetadataAssetsOptions" } + "description": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } }, - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "title": "Path", - "description": "Path of files to include. Can be absolute, relative to the crate root, or/and glob.\n\nLeft hand is where to put files, path in the resulting package.\n\nRight hand is a path or pattern to match files to include." - }, - { - "type": "boolean", - "title": "Include", - "description": "Include or exclude the file or glob-pattern." - } - ] + "version": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } }, - "x-taplo": { - "links": { - "key": "https://TODO:doc-url" - } - } - }, - - "PlaydateMetadataAssetsOptions": { - "type": "object", - "title": "Assets Configuration", - "description": "Options for assets paths resolution and how to build assets collection", - "additionalProperties": false, - "properties": { - "overwrite": {"type": "boolean", "description": "Allow overwriting existing files."}, - - "follow-symlinks": {"type": "boolean"}, - "method": { - "type": "string", - "enum": [ "copy", "link" ] - }, - "dependencies": { - "type": "boolean", - "description": "Allow build assets for dependencies." - } + "build-number": { + "type": "integer", + "exclusiveMinimum": 0, + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + }, + "description": "A monotonically-increasing integer value used to indicate a unique version of your game. This can be set using an automated build process like Continuous Integration to avoid having to set the value by hand.\n\nFor sideloaded games, buildNumber is required and is used to determine when a newer version is available to download." + }, + "image-path": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + }, + "description": "A directory of images that will be used by the launcher.\n\nMore in [official documentation](https://sdk.play.date/#pdxinfo)." + }, + "launch-sound-path": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + }, + "description": "Should point to the path of a short audio file to be played as the game launch animation is taking place.\n\nMore in [official documentation](https://sdk.play.date/#pdxinfo)." + }, + "content-warning": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + }, + "description": "Optional. A content warning that displays when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose." + }, + "content-warning2": { + "type": "string", + "x-taplo": { + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + }, + "description": "Optional. A second content warning that displays on a second screen when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose.\n\nNote: `content-warning2` will only display if a `content-warning` attribute is also specified.\n\nThe string displayed on the content warning screen can only be so long before it will be truncated with an \"…\" character. Be sure to keep this in mind when designing your `content-warning` and `content-warning2` text." + }, + "assets": { + "anyOf": [ + { + "$ref": "#/definitions/PlaydateMetadataAssetsMap" + }, + { + "$ref": "#/definitions/PlaydateMetadataAssetsArray" + } + ] }, - "x-taplo": { - "links": { - "key": "https://TODO:doc-url" - } + "options": { + "$ref": "#/definitions/PlaydateMetadataOptions" + }, + "support": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "additionalProperties": false, + "x-taplo": { + "initKeys": [ + "bundle-id", + "name", + "description", + "author", + "image-path", + "launch-sound-path" + ], + "links": { + "key": "https://sdk.play.date/#pdxinfo" + } + } + }, + "PlaydateMetadataAssetsArray": { + "type": "array", + "title": "Assets list", + "description": "List of paths to include.", + "uniqueItems": true, + "items": { + "title": "Path", + "description": "Path to include.", + "type": "string" + }, + "x-taplo": { + "links": { + "key": "https://github.com/boozook/playdate/blob/main/support/build/README.md#assets-list" + } + } + }, + "PlaydateMetadataAssetsMap": { + "type": "object", + "title": "Assets rules", + "description": "Rules used to resolve paths to include.", + "properties": { + "options": { + "$ref": "#/definitions/PlaydateMetadataAssetsOptions" + } + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "title": "Path", + "description": "Path of files to include. Can be absolute, relative to the crate root, or/and glob.\n\nLeft hand is where to put files, path in the resulting package.\n\nRight hand is a path or pattern to match files to include." + }, + { + "type": "boolean", + "title": "Include", + "description": "Include or exclude the file or glob-pattern." + } + ] + }, + "x-taplo": { + "links": { + "key": "https://github.com/boozook/playdate/blob/main/support/build/README.md#assets-table" } - }, - - "PlaydateMetadataOptions": { - "type": "object", - "title": "Configuration", - "description": "Package build options.", - "additionalProperties": false, - - "properties": { - "assets": { - "$ref": "#/definitions/PlaydateMetadataAssetsOptions" - } + } + }, + "PlaydateMetadataAssetsOptions": { + "type": "object", + "title": "Assets Configuration", + "description": "Options for assets paths resolution and how to build assets collection", + "additionalProperties": false, + "properties": { + "overwrite": { + "type": "boolean", + "description": "Allow overwriting existing files." }, - - "x-taplo": { - "links": { - "key": "https://TODO:doc-url" - } + "follow-symlinks": { + "type": "boolean" + }, + "method": { + "type": "string", + "enum": [ + "copy", + "link" + ] + }, + "dependencies": { + "type": "boolean", + "description": "Allow build assets for dependencies." + } + }, + "x-taplo": { + "links": { + "key": "https://github.com/boozook/playdate/blob/main/support/build/README.md#assets-options" } - } + } + }, + "PlaydateMetadataOptions": { + "type": "object", + "title": "Configuration", + "description": "Package build options.", + "additionalProperties": true, + "properties": { + "assets": { + "$ref": "#/definitions/PlaydateMetadataAssetsOptions" + } + }, + "x-taplo": { + "links": { + "key": "https://github.com/boozook/playdate/blob/main/support/build/README.md#options" + } + } + } }, "properties": { - "package": { "$ref": "#/definitions/Package" } + "package": { + "$ref": "#/definitions/Package" + } + }, + "x-taplo-info": { + "authors": [ + "Alex Koz. (https://github.com/boozook)" + ], + "patterns": [ + "^(.*(/|\\\\)Cargo\\.toml|Cargo\\.toml)$" + ] } -} + } diff --git a/cargo/Cargo.toml b/cargo/Cargo.toml index 3580c62d..92c2407b 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -67,10 +67,10 @@ features = ["assets-report", "toml"] default-features = false [dependencies.tool] -version = "=0.1.0" # TODO: relax semver when tool's api stabilized +version = "=0.1.0" # TODO: relax semver when tool's api stabilized path = "../support/tool" package = "playdate-tool" -features = ["usb", "clap", "usb", "cli"] +features = ["clap", "cli"] [dependencies.clap] @@ -94,3 +94,9 @@ features = [ [dev-dependencies] target = "2.0.0" +rand = "0.8" + + +[features] +default = [] +usb = ["tool/usb"] diff --git a/cargo/README.md b/cargo/README.md index 375639fd..af24b842 100644 --- a/cargo/README.md +++ b/cargo/README.md @@ -20,7 +20,7 @@ Currently tested and works good on following platforms: - Windows (x86-64 and aarch64) - build 👍 - package 👍 - - install & run ⚠️ - issues, work in progress. + - install & run ⚠️ - issues, work in progress, see [troubleshooting](#troubleshooting). ## Prerequisites @@ -55,8 +55,8 @@ Generate new project using `new` or `init` command. ```bash mkdir hello-world && cd $_ -crank init --lib --full-metadata --deps="sys:git, controls:git" -crank run +cargo playdate init --lib --full-metadata --deps="sys:git, controls:git" +cargo playdate run ``` @@ -69,7 +69,7 @@ TODO There is no configuration other then inherited by cargo and some special environment variables. -- `CRANK_LOG` working same way as `CARGO_LOG` or default `RUST_LOG`. Also `CRANK_LOG_STYLE` +- `CARGO_PLAYDATE_LOG` working same way as `CARGO_LOG` or default `RUST_LOG`. Also `CARGO_PLAYDATE_LOG_STYLE` - `PLAYDATE_SDK_PATH` path to the SDK root - `ARM_GCC_PATH` path to the `arm-none-eabi-gcc` executable. @@ -98,6 +98,12 @@ path = "examples/demo.rs" +## Troubleshooting + +* Is some cases (see [status](#status)) hardware cannot be detected. Try to build cargo-playdate with or without feature `usb`. + +* Welcome to [discussions](https://github.com/boozook/playdate/discussions) and [issues](https://github.com/boozook/playdate/issues). + - - - This software is not sponsored or supported by Panic. diff --git a/cargo/src/cli/mod.rs b/cargo/src/cli/mod.rs index 61a3c301..6e70d042 100644 --- a/cargo/src/cli/mod.rs +++ b/cargo/src/cli/mod.rs @@ -102,10 +102,10 @@ pub fn initialize_from(args: impl IntoIterator + AsRe .flatten() .is_some() { - // skip "crank" command + // skip root command matches.subcommand() } else { - // get "crank", then subcommand + // get root, then subcommand matches.subcommand_matches(CMD_NAME) .map(|m| m.subcommand()) .flatten() @@ -389,9 +389,9 @@ fn compile_options(cmd: &Cmd, matches: &ArgMatches, ws: &Workspace<'_>) -> Cargo /// - Find `cmd` and replace to `Cmd::Build` /// - Replace all `--target(=)PlaydateTarget::*` to normalized `CompileKind` -/// - Strip first args such as `cargo crank cmd`. +/// - Strip first args such as `cargo playdate cmd`. /// -/// Note: this doesn't support flag-series with values like `-abc="c-value"`. +/// Note: this doesn't supports flag-series with values like `-abc="c-value"`. fn adapt_args_for_underlying_cargo(cmd: &Cmd, aliases: Option<&HashMap>, args: I, diff --git a/cargo/src/init/full-metadata.toml b/cargo/src/init/full-metadata.toml index e705f75d..0c11997d 100644 --- a/cargo/src/init/full-metadata.toml +++ b/cargo/src/init/full-metadata.toml @@ -1,5 +1,5 @@ # Playdate Package Info -# doc: // TODO: https://github.com/boozook/playdate/#configuration +# doc: https://github.com/boozook/playdate/blob/main/support/build/README.md#metadata # official doc: https://sdk.play.date/#pdxinfo [package.metadata.playdate] name = "{name}" @@ -16,7 +16,7 @@ launch-sound-path = "sfx/jump" # Assets Rules -# TODO: link to documentation +# doc: https://github.com/boozook/playdate/blob/main/support/build/README.md#assets [package.metadata.playdate.assets] "img/system/" = "${{PLAYDATE_SDK_PATH}}/Examples/Game Template/Source/SystemAssets/*.png" "sfx/jump.wav" = "${{PLAYDATE_SDK_PATH}}/Examples/Level 1-1/Source/sfx/jump.wav" diff --git a/cargo/src/layout/mod.rs b/cargo/src/layout/mod.rs index 1970e6ee..eb1c3290 100644 --- a/cargo/src/layout/mod.rs +++ b/cargo/src/layout/mod.rs @@ -36,7 +36,7 @@ impl Layout for &mut T { pub trait LayoutLockable: Layout { - /// The lockfile filename for a build (e.g: `.crank-lock`). + /// The lockfile filename for a build (e.g: `.name-lock`). fn lockfilename(&self) -> Cow; /// Lock the destination directory of the layout. diff --git a/cargo/tests/build/simple.rs b/cargo/tests/build/simple.rs index 1d18968c..09d7ef88 100644 --- a/cargo/tests/build/simple.rs +++ b/cargo/tests/build/simple.rs @@ -11,7 +11,7 @@ use crate::common::*; fn run_build(crate_path: &Path, args: impl IntoIterator>) - -> Result<(Output, &'static Path)> { + -> Result<(Output, PathBuf)> { println!("crate: {}", crate_path.display()); let target_dir = target_dir(); @@ -20,7 +20,11 @@ fn run_build(crate_path: &Path, .map(Into::into) .chain([OsString::from(target_dir_arg)]); let output = Tool::build(&crate_path, args)?; - assert!(output.status.success()); + assert!( + output.status.success(), + "Tool failed with stderr:\n{}", + std::str::from_utf8(&output.stderr).unwrap() + ); Ok((output, target_dir)) } @@ -262,7 +266,7 @@ mod examples { } #[test] - /// target: playdate hardware + /// target: playdate hardware+simulator(host) fn sim_dev_release_examples() -> Result<()> { let dev_target = DEVICE_TARGET; let host_target = target_triple(); diff --git a/cargo/tests/common.rs b/cargo/tests/common.rs index 7a78d1cc..8284f08f 100644 --- a/cargo/tests/common.rs +++ b/cargo/tests/common.rs @@ -106,12 +106,23 @@ pub fn to_dyn_lib_name(crate_name: S) -> String { } -pub fn target_dir() -> &'static Path { +pub fn target_dir() -> PathBuf { + use rand::RngCore; + let tmp = Path::new(env!("CARGO_TARGET_TMPDIR")); if !tmp.exists() { std::fs::create_dir_all(tmp).expect("can't create tmp dir"); } - tmp + + // add random: + let mut values = [0u8; 4]; + rand::thread_rng().fill_bytes(&mut values); + let rand = values.into_iter() + .map(|v| v.to_string()) + .collect::>() + .join(""); + + tmp.join(rand) } pub fn target_triple() -> String { diff --git a/cargo/tests/crates/metadata/Cargo.toml b/cargo/tests/crates/metadata/Cargo.toml index a786aaf7..f4046a7f 100644 --- a/cargo/tests/crates/metadata/Cargo.toml +++ b/cargo/tests/crates/metadata/Cargo.toml @@ -18,7 +18,6 @@ bundle-id = "test.workspace.main.crate" [package.metadata.playdate.assets] "main/" = "Cargo.toml" -"*" = false # options.method = "link" # options.overwrite = true # options.dependencies = true diff --git a/cargo/tests/init/init.rs b/cargo/tests/init/init.rs index f640cf23..da14d69d 100644 --- a/cargo/tests/init/init.rs +++ b/cargo/tests/init/init.rs @@ -13,7 +13,7 @@ fn run(crate_name: &str, -> Result<(Output, PathBuf)> { println!("crate: {}", crate_name); - let crate_path = target_dir().join("create-init-tests").join(&crate_name); + let crate_path = target_dir().join(format!("create-init--{crate_name}")); if crate_path.try_exists()? { std::fs::remove_dir_all(&crate_path)?; diff --git a/cargo/tests/init/new.rs b/cargo/tests/init/new.rs index bc1d7cfa..490dea04 100644 --- a/cargo/tests/init/new.rs +++ b/cargo/tests/init/new.rs @@ -13,7 +13,7 @@ fn run(crate_name: &str, -> Result<(Output, PathBuf)> { println!("crate: {}", crate_name); - let crate_path = target_dir().join("create-new-tests").join(&crate_name); + let crate_path = target_dir().join(format!("create-new--{crate_name}")); if crate_path.try_exists()? { std::fs::remove_dir_all(&crate_path)?; @@ -21,7 +21,7 @@ fn run(crate_name: &str, let crate_parent = crate_path.parent().expect("parent"); if !crate_parent.try_exists()? { - std::fs::create_dir_all(&crate_path)?; + std::fs::create_dir_all(&crate_parent)?; } let mut extra = vec![OsString::from(&crate_path)]; diff --git a/support/build/README.md b/support/build/README.md index 5e72fe5b..a02d6b12 100644 --- a/support/build/README.md +++ b/support/build/README.md @@ -4,6 +4,127 @@ Contains manifest format and "build assets by metadata" utils. +- - - + +## Metadata + +Here is the metadata format explanation in examples + +### Package Info + +```toml +# Playdate Package Info +# official doc: https://sdk.play.date/#pdxinfo +[package.metadata.playdate] +name = "{name}" # optional, default is package.name +author = "{author}" # optional, default is package.authors +version = "{version}" # optional, default is package.version +description = "{description}" # optional, default is package.description +bundle-id = "com.yourcompany.{bundle_id}" + +image-path = "img/system" # optional +launch-sound-path = "sfx/jump" # optional + +content-warning = "This game contains mild realistic, violence and bloodshed." # optional +content-warning2 = "Really scary game." # optional +``` + + +### Assets + +Instructions for Playdate Package Build System such as cargo-playdate. + +Describes where assets are stored, how and where they should be in the package. + +```toml +[package.metadata.playdate.assets] +``` + +> #### Dev-Assets +> `[package.metadata.playdate.dev-assets]` +> Assets that for examples or tests only, inherited by main assets. +> Not implemented yet, WiP. + + +There is two options how to set assets - list or table: + +#### Assets List + +Simplest way to declare assets is just list of paths. + +- Path can contain glob-patterns like `/**/*o*e.png` +- Path can contain env-vars like `${MY_VARIABLE}` +- Path can be absolute or relative to crate root + +So all matched files will be included. + +```toml +[package.metadata.playdate] +assets = ["assets/**/*.wav", "assets/**/*.png"] +``` + +If glob in path, resulting path of file starts with matched part of path, e.g.: +- for `assets/**/*.wav` it will be `foo/some.wav`, if `assets` contains `foo` dir + + +#### Assets Table + +This is a complex way of specifying what assets should be included. +- Left hand is a path where asset should be in the package, +- Right hand is the path where source(s) should be found. + +- Both hands can contain globs. +- Both hands can contain env-var queries like `${MY_VARIABLE}` + +- Left hand path is relative to building playdate-package root +- Right hand path can be absolute or relative to crate root + +```toml +[package.metadata.playdate.assets] +# Next line means that all png-files in SystemAssets dir wil be included and placed in img/system directory +"img/system/" = "${PLAYDATE_SDK_PATH}/Examples/Game Template/Source/SystemAssets/*.png" +# Next line means that jump.wav will be included and placed in package as sfx/jump.wav +"sfx/jump.wav" = "${PLAYDATE_SDK_PATH}/Examples/Level 1-1/Source/sfx/jump.wav" +# Next line means that img.png will be included in root of package +"/" = "assets/img.png" # path is relative to crate root +``` + +Also this way supports simple include and exclude instructions: +```toml +"rel-to-crate-root/file-to-include" = true # left hand is a local path, relative to crate-root, +"file-to-exclude" = false # OR resulting path that where asset will be in the resulting package. +``` + + +#### Assets Options + +There is some options where to set asset options: +- `[package.metadata.playdate.assets.options]` +- `[package.metadata.playdate.options.assets]` + +Both are equal but should not be both in one crate. + +```toml +[package.metadata.playdate.assets.options] +dependencies = true # allow to build assets for dependencies (default is `true`) +overwrite = true # overwrite existing assets in build dir (default is `true`) +method = "link" # "copy" or "link" (default is `link`) - how assets should be collected, make symlinks or copy files +follow-symlinks = true # follow symlinks (default is `true`) +``` + + +### Options + +Package build options, instruction for Playdate Package Build System such as cargo-playdate. + +```toml +[package.metadata.playdate.options] +``` + +Available options is `assets`, see [Assets Options](#assets-options). + +Currently there is no more options, it's just reserved for future use. + diff --git a/support/tool/README.md b/support/tool/README.md index 583ca1c7..8ec6c9d7 100644 --- a/support/tool/README.md +++ b/support/tool/README.md @@ -2,14 +2,12 @@ CLI-tool and lib for interaction with Playdate device and sim. -This software is not sponsored or supported by Panic. - ### Status This is earlier version, that means "alpha" or "MVP". API can be changed in future versions. -Global refactoring is planned with main reason of properly work with usb on all platforms. +Global __refactoring is planned__ with main reason of properly work with usb on all platforms. Currently tested and works good on following platforms: - Unix (x86-64 and aarch64) diff --git a/support/utils/src/toolchain/gcc.rs b/support/utils/src/toolchain/gcc.rs index 899cedf5..d87e6621 100644 --- a/support/utils/src/toolchain/gcc.rs +++ b/support/utils/src/toolchain/gcc.rs @@ -352,10 +352,12 @@ mod tests { fn gcc_from_env_path() { Gcc::try_from_env_path().unwrap(); } #[test] + #[cfg(unix)] fn gcc_from_default_path() { Gcc::try_from_default_path().unwrap(); } #[test] + #[cfg(unix)] fn gcc_sysroot_fallback() { let gcc = Gcc::try_new().unwrap(); let res = gcc.sysroot_fallback().unwrap(); @@ -363,6 +365,7 @@ mod tests { } #[test] + #[ignore = "sysroot can be empty"] fn gcc_sysroot_by_output() { let gcc = Gcc::try_new().unwrap(); let res = gcc.sysroot_by_output().unwrap(); diff --git a/support/utils/src/toolchain/sdk.rs b/support/utils/src/toolchain/sdk.rs index 8c7b5a4b..0e95ec81 100644 --- a/support/utils/src/toolchain/sdk.rs +++ b/support/utils/src/toolchain/sdk.rs @@ -198,8 +198,5 @@ mod tests { #[test] - fn sdk_from_env_path() { Sdk::try_from_default_env().unwrap(); } - - #[test] - fn sdk_from_default_config() { Sdk::try_from_default_config().unwrap(); } + fn sdk() { Sdk::try_new().unwrap(); } }