commit cad84029fd9a76ff0ae715499f7ae5b5e660725c Author: Christophe Vila Date: Sat Jul 4 09:58:02 2026 +0200 chore: initial padel scaffold + design docs Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722a728 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +*.prg +*.prg.debug.xml +developer_key +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33e5b46 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A Garmin Connect IQ watch-app for padel, written in **Monkey C**. It is currently the default scaffold generated by the VS Code "Monkey C: New Project" command — a single view with a menu — and has not yet been customized beyond the template. + +- App entry: `garmin_padelApp` (set in `manifest.xml` via `entry="garmin_padelApp"`). +- Build target: a single product, `venu445mm` (Venu 4, 45mm), with `minApiLevel="6.0.2"`. + +## Build, run, and test + +There is no shell-script build harness; this project is built and run through the **Monkey C VS Code extension** (Connect IQ SDK), driven from the command palette: + +- **Build / run in simulator**: `Monkey C: Build for Device` then `Monkey C: Run App`, or just press `F5` (the simulator launches `connectiq` + `monkeydo`). +- **Edit build targets / products**: `Monkey C: Edit Products` or `Monkey C: Set Products by Product Category`. +- **Edit app attributes, permissions, languages**: the corresponding `Monkey C: Edit ...` palette commands. These regenerate `manifest.xml` — do not hand-edit it (it is marked generated). +- **Package for the store**: `Monkey C: Export Project` (produces a `.iq` file). + +CLI equivalents (if the SDK `bin` is on PATH) use `monkeyc` to compile against a device + SDK, `connectiq`/`monkeydo` to run in the simulator, and `monkeyc --unit-test` (with a test runner view) for unit tests. There are currently no tests in this repo. + +## Code architecture + +Connect IQ apps follow a fixed MVC-ish lifecycle. The four `source/*.mc` files map to the standard roles: + +- **`garmin-padelApp.mc`** — `AppBase` subclass. `getInitialView()` returns the initial `[View, InputDelegate]` pair. App-wide start/stop hooks live here. `getApp()` is the global accessor. +- **`garmin-padelView.mc`** — `WatchUi.View`. `onLayout` binds `Rez.Layouts.MainLayout`; `onShow`/`onUpdate`/`onHide` are the render/visibility lifecycle. +- **`garmin-padelDelegate.mc`** — `BehaviorDelegate` for the main view. `onMenu()` pushes the menu (`Rez.Menus.MainMenu`) with its delegate. +- **`garmin-padelMenuDelegate.mc`** — `MenuInputDelegate`. `onMenuItem(item as Symbol)` dispatches on menu-item symbols (`:item_1`, `:item_2`). + +Key conventions: +- **Resources are referenced via the generated `Rez` namespace** (`Rez.Layouts.*`, `Rez.Menus.*`, `Rez.Strings.*`, `Rez.Drawables.*`). These symbols are generated at build time from the XML in `resources/` — you do not write them by hand. +- **`resources/`** holds the declarative UI and assets: `layouts/layout.xml`, `menus/menu.xml`, `strings/strings.xml`, `drawables/`. Menu item ids in `menu.xml` must match the `:symbol` names handled in the menu delegate, and string ids must match `@Strings.*` references. +- **`monkey.jungle`** is the build config; it currently only points at `manifest.xml`. Per-device resource overrides and source paths would be added here. +- **`bin/`** is generated build output (`.mir`, `.mbc`, `Rez.mcgen`) — never edit; safe to delete and regenerate. diff --git a/docs/superpowers/plans/2026-07-02-padel-scoring.md b/docs/superpowers/plans/2026-07-02-padel-scoring.md new file mode 100644 index 0000000..2b171f5 --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-padel-scoring.md @@ -0,0 +1,1016 @@ +# Padel Scoring App Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn the Connect IQ scaffold into a padel score tracker: a top-down court with a yellow serve ball, sets/games on the left, points on the right, heart rate on top, time on bottom, driven by swipe/tap gestures. + +**Architecture:** A UI-free scoring engine (`PadelMatch`) holds all match state and rules and is fully unit-tested. UI classes (`CourtRenderer`, `HeartRateProvider`, the `View`, the `Delegate`) read from the engine and are verified in the simulator. Rules live in a `RulesConfig` object so a future settings screen can drive them. + +**Tech Stack:** Monkey C, Connect IQ SDK 9.2.0, product `venu445mm` (Venu 4, 45mm), Toybox `WatchUi`/`Graphics`/`Sensor`/`UserProfile`/`Test`. + +--- + +## Reference: full spec + +Design spec lives at `docs/superpowers/specs/2026-07-02-padel-scoring-design.md`. Read it before starting. + +## File structure + +- `source/RulesConfig.mc` — rules struct + v1 defaults (golden point, best-of-3, tiebreak at 6-6). +- `source/PadelMatch.mc` — the scoring engine (no UI imports). Module-level `US`/`THEM` and quadrant enum live here. +- `source/PadelMatchTest.mc` — unit tests + plain helper functions for the engine. +- `source/CourtRenderer.mc` — draws the court + yellow serve ball against a `Dc`. +- `source/HeartRateProvider.mc` — wraps the HR sensor + zone lookup. +- `source/garmin-padelView.mc` — draws the whole screen in `onUpdate`; 1s timer for the clock. +- `source/garmin-padelDelegate.mc` — maps swipes/taps to the engine. +- `source/garmin-padelApp.mc` — owns the engine/rules/HR, wires view+delegate, enables sensors. +- `source/garmin-padelMenuDelegate.mc` — **deleted** (scaffold menu not used). +- `resources/strings/strings.xml` — team labels + match-end text. +- `manifest.xml` — regenerated to add `Sensor` + `UserProfile` permissions. + +## Conventions used throughout + +- Teams: module constants `US = 0`, `THEM = 1`. +- Quadrants: module enum `QUAD_BOTTOM_RIGHT, QUAD_BOTTOM_LEFT, QUAD_UPPER_LEFT, QUAD_UPPER_RIGHT` (values 0..3). +- Your team is the bottom half of the court; opponents the top. + +--- + +## Task 0: Prerequisites — env vars, git, gitignore + +**Files:** +- Create: `.gitignore` + +- [ ] **Step 1: Set SDK env vars (run in every shell you use for this plan)** + +```bash +export SDK="$HOME/Library/Application Support/Garmin/ConnectIQ/Sdks/connectiq-sdk-mac-9.2.0-2026-06-09-92a1605b2" +export KEY="$HOME/Downloads/developer_key" +export DEVICE=venu445mm +``` + +Verify: `"$SDK/bin/monkeyc" --version` prints a version string. + +- [ ] **Step 2: Create `.gitignore`** + +``` +bin/ +*.prg +``` + +- [ ] **Step 3: Initialize git and commit the current scaffold** + +```bash +cd /Users/kriss/Dev/scm/scm.vilanet.fr/kriss/garmin-padel +git init +git add -A +git commit -m "chore: initial padel scaffold + design docs" +``` + +Expected: a commit is created (the `bin/` output is excluded). + +--- + +## Task 1: RulesConfig + +**Files:** +- Create: `source/RulesConfig.mc` + +- [ ] **Step 1: Write `RulesConfig.mc`** + +```monkeyc +import Toybox.Lang; + +// Configurable padel rules. v1 uses the defaults below; a future settings +// screen will mutate an instance of this class — the engine reads it as-is. +class RulesConfig { + public var goldenPoint as Boolean; // true = sudden death at 40-40 + public var gamesPerSet as Number; // games needed to take a set (before win-by-2) + public var setsToWin as Number; // sets needed to win the match + public var tiebreakEnabled as Boolean; // play a tiebreak at gamesPerSet-all + public var tiebreakTarget as Number; // points needed to win a tiebreak (before win-by-2) + + public function initialize() { + goldenPoint = true; + gamesPerSet = 6; + setsToWin = 2; + tiebreakEnabled = true; + tiebreakTarget = 7; + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add source/RulesConfig.mc +git commit -m "feat: add RulesConfig with v1 defaults" +``` + +--- + +## Task 2: Engine test suite (test-first) + +Write the complete unit-test suite for `PadelMatch` before the engine exists. It will fail to compile (no `PadelMatch`) — that is the expected failing state. + +**Files:** +- Create: `source/PadelMatchTest.mc` + +- [ ] **Step 1: Write `PadelMatchTest.mc`** + +```monkeyc +import Toybox.Lang; +import Toybox.Test; + +// ---- plain helpers (not annotated :test, so not run as cases) ---- + +function newMatch() as PadelMatch { + return new PadelMatch(new RulesConfig()); +} + +function newAdvantageMatch() as PadelMatch { + var r = new RulesConfig(); + r.goldenPoint = false; + return new PadelMatch(r); +} + +// Win one game for `team` under golden-point rules (4 straight points). +function winGameFor(m as PadelMatch, team as Number) as Void { + for (var i = 0; i < 4; i += 1) { m.pointTo(team); } +} + +// ---- test cases ---- + +(:test) function testInitialState(logger as Logger) as Boolean { + var m = newMatch(); + Test.assertEqualMessage(m.sets(US), 0, "sets US"); + Test.assertEqualMessage(m.games(US), 0, "games US"); + Test.assertEqualMessage(m.pointLabel(US), "0", "points US"); + Test.assertEqualMessage(m.isMatchOver(), false, "not over"); + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "US serves deuce"); + return true; +} + +(:test) function testPointLabelsProgress(logger as Logger) as Boolean { + var m = newMatch(); + m.pointTo(US); + Test.assertEqualMessage(m.pointLabel(US), "15", "15"); + m.pointTo(US); + Test.assertEqualMessage(m.pointLabel(US), "30", "30"); + m.pointTo(US); + Test.assertEqualMessage(m.pointLabel(US), "40", "40"); + return true; +} + +(:test) function testGoldenGameWin(logger as Logger) as Boolean { + var m = newMatch(); + winGameFor(m, US); + Test.assertEqualMessage(m.games(US), 1, "US won a game"); + Test.assertEqualMessage(m.pointLabel(US), "0", "points reset"); + return true; +} + +(:test) function testAdvantageDeuceCycle(logger as Logger) as Boolean { + var m = newAdvantageMatch(); + // 3-3 deuce + for (var i = 0; i < 3; i += 1) { m.pointTo(US); m.pointTo(THEM); } + Test.assertEqualMessage(m.pointLabel(US), "40", "deuce US 40"); + m.pointTo(US); + Test.assertEqualMessage(m.pointLabel(US), "AD", "US advantage"); + m.pointTo(THEM); + Test.assertEqualMessage(m.pointLabel(US), "40", "back to deuce"); + m.pointTo(US); m.pointTo(US); + Test.assertEqualMessage(m.games(US), 1, "US won from advantage"); + return true; +} + +(:test) function testWinSetSixLove(logger as Logger) as Boolean { + var m = newMatch(); + for (var g = 0; g < 6; g += 1) { winGameFor(m, US); } + Test.assertEqualMessage(m.sets(US), 1, "US won the set"); + Test.assertEqualMessage(m.games(US), 0, "games reset"); + return true; +} + +(:test) function testTiebreakEntryAndWin(logger as Logger) as Boolean { + var m = newMatch(); + for (var i = 0; i < 6; i += 1) { winGameFor(m, US); winGameFor(m, THEM); } + Test.assertEqualMessage(m.isInTiebreak(), true, "6-6 tiebreak"); + Test.assertEqualMessage(m.games(US), 6, "games 6-6"); + for (var p = 0; p < 7; p += 1) { m.pointTo(US); } + Test.assertEqualMessage(m.isInTiebreak(), false, "tiebreak done"); + Test.assertEqualMessage(m.sets(US), 1, "US won the set via tiebreak"); + return true; +} + +(:test) function testServeSideParity(logger as Logger) as Boolean { + var m = newMatch(); // US serving + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "0 pts deuce"); + m.pointTo(THEM); // 1 point played + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_LEFT, "1 pt ad"); + m.pointTo(US); // 2 points played + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "2 pts deuce"); + return true; +} + +(:test) function testServerAlternation(logger as Logger) as Boolean { + var m = newMatch(); + winGameFor(m, US); // serve passes to THEM + Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "THEM serves deuce"); + return true; +} + +(:test) function testTiebreakServing(logger as Logger) as Boolean { + var m = newMatch(); + for (var i = 0; i < 6; i += 1) { winGameFor(m, US); winGameFor(m, THEM); } + // 12 games played, start server US -> US serves first in tiebreak + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_RIGHT, "P0 US deuce"); + m.pointTo(US); + Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_RIGHT, "P1 THEM ad"); + m.pointTo(US); + Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "P2 THEM deuce"); + m.pointTo(US); + Test.assertEqualMessage(m.servingQuadrant(), QUAD_BOTTOM_LEFT, "P3 US ad"); + return true; +} + +(:test) function testUndoAcrossGame(logger as Logger) as Boolean { + var m = newMatch(); + winGameFor(m, US); // US 1 game, points 0-0, THEM to serve + m.pointTo(THEM); // THEM has 15 + m.undo(); // back to just-after-game + Test.assertEqualMessage(m.pointLabel(THEM), "0", "point undone"); + m.undo(); // undo the game-winning point + Test.assertEqualMessage(m.games(US), 0, "game undone"); + Test.assertEqualMessage(m.pointLabel(US), "40", "back to 40"); + return true; +} + +(:test) function testUndoAtStartIsNoop(logger as Logger) as Boolean { + var m = newMatch(); + m.undo(); + Test.assertEqualMessage(m.games(US), 0, "still zero"); + return true; +} + +(:test) function testStartingServerToggle(logger as Logger) as Boolean { + var m = newMatch(); + m.toggleStartingServer(); + Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_LEFT, "THEM serves first"); + m.pointTo(US); // match started; toggle now locked + m.toggleStartingServer(); + Test.assertEqualMessage(m.canSetServer(), false, "server locked after first point"); + return true; +} + +(:test) function testMatchOver(logger as Logger) as Boolean { + var m = newMatch(); + for (var s = 0; s < 2; s += 1) { + for (var g = 0; g < 6; g += 1) { winGameFor(m, US); } + } + Test.assertEqualMessage(m.isMatchOver(), true, "match over"); + Test.assertEqualMessage(m.winner(), US, "US wins"); + var setsBefore = m.sets(US); + m.pointTo(US); // ignored after match over + Test.assertEqualMessage(m.sets(US), setsBefore, "no scoring after match"); + return true; +} +``` + +- [ ] **Step 2: Build the test binary and confirm it FAILS to compile** + +```bash +"$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test +``` + +Expected: FAIL — errors like `Undefined symbol :PadelMatch` / `US` / `QUAD_BOTTOM_RIGHT`. This confirms the tests reference the not-yet-written engine. + +- [ ] **Step 3: Commit the tests** + +```bash +git add source/PadelMatchTest.mc +git commit -m "test: add PadelMatch engine test suite (failing)" +``` + +--- + +## Task 3: PadelMatch engine + +Implement the engine so every test in Task 2 passes. + +**Files:** +- Create: `source/PadelMatch.mc` + +- [ ] **Step 1: Write `PadelMatch.mc`** + +```monkeyc +import Toybox.Lang; + +// Teams +const US = 0; +const THEM = 1; + +// Court quadrants (your team = bottom half) +enum { + QUAD_BOTTOM_RIGHT, // US, deuce (right) + QUAD_BOTTOM_LEFT, // US, ad (left) + QUAD_UPPER_LEFT, // THEM, deuce (their right -> screen left) + QUAD_UPPER_RIGHT // THEM, ad (their left -> screen right) +} + +class PadelMatch { + private var _rules as RulesConfig; + + private var _points as Array; // raw point count per team in the current game + private var _games as Array; // games won in the current set + private var _sets as Array; // sets won in the match + private var _server as Number; // team serving the current game + private var _inTiebreak as Boolean; + private var _tbPoints as Array; + private var _tbFirstServer as Number; // team that served the tiebreak's first point + private var _matchOver as Boolean; + private var _winner as Number; // US / THEM / -1 + private var _history as Array; + + private const POINT_LABELS = ["0", "15", "30", "40"]; + + public function initialize(rules as RulesConfig) { + _rules = rules; + _points = [0, 0]; + _games = [0, 0]; + _sets = [0, 0]; + _server = US; + _inTiebreak = false; + _tbPoints = [0, 0]; + _tbFirstServer = US; + _matchOver = false; + _winner = -1; + _history = []; + } + + // ---- public API ---- + + public function pointTo(team as Number) as Void { + if (_matchOver) { return; } + _pushHistory(); + if (_inTiebreak) { + _tbPoints[team] += 1; + _resolveTiebreak(team); + } else { + _points[team] += 1; + _resolveGame(team); + } + } + + public function undo() as Void { + var n = _history.size(); + if (n == 0) { return; } + var s = _history[n - 1]; + _history = _history.slice(0, n - 1); + _points = s["points"] as Array; + _games = s["games"] as Array; + _sets = s["sets"] as Array; + _server = s["server"] as Number; + _inTiebreak = s["inTiebreak"] as Boolean; + _tbPoints = s["tbPoints"] as Array; + _tbFirstServer = s["tbFirstServer"] as Number; + _matchOver = s["matchOver"] as Boolean; + _winner = s["winner"] as Number; + } + + public function canSetServer() as Boolean { + return _history.size() == 0 && !_matchOver; + } + + public function toggleStartingServer() as Void { + if (canSetServer()) { _server = 1 - _server; } + } + + public function servingQuadrant() as Number { + var team; + var pointsPlayed; + if (_inTiebreak) { + pointsPlayed = _tbPoints[0] + _tbPoints[1]; + team = _tiebreakServer(pointsPlayed); + } else { + pointsPlayed = _points[0] + _points[1]; + team = _server; + } + var isDeuce = (pointsPlayed % 2) == 0; + return _quadrantFor(team, isDeuce); + } + + public function pointLabel(team as Number) as String { + if (_inTiebreak) { return _tbPoints[team].toString(); } + var a = _points[team]; + var b = _points[1 - team]; + if (!_rules.goldenPoint && a == 4 && b == 3) { return "AD"; } + if (a >= 4) { return "40"; } // safety; game normally ends before display + return POINT_LABELS[a]; + } + + public function games(team as Number) as Number { return _games[team]; } + public function sets(team as Number) as Number { return _sets[team]; } + public function server() as Number { return _server; } + public function isInTiebreak() as Boolean { return _inTiebreak; } + public function isMatchOver() as Boolean { return _matchOver; } + public function winner() as Number { return _winner; } + public function tiebreakPoints(team as Number) as Number { return _tbPoints[team]; } + + // ---- internals ---- + + private function _resolveGame(team as Number) as Void { + var other = 1 - team; + var a = _points[team]; + var b = _points[other]; + if (_rules.goldenPoint) { + if (a >= 4) { _winGame(team); } + } else { + if (a >= 4 && (a - b) >= 2) { _winGame(team); } + else if (a >= 4 && b >= 4) { _points[team] = 3; _points[other] = 3; } // both past 40 -> deuce + } + } + + private function _winGame(team as Number) as Void { + _games[team] += 1; + _points = [0, 0]; + _server = 1 - _server; + _resolveSet(team); + } + + private function _resolveSet(team as Number) as Void { + var other = 1 - team; + var a = _games[team]; + var b = _games[other]; + if (a >= _rules.gamesPerSet && (a - b) >= 2) { + _winSet(team); + } else if (_rules.tiebreakEnabled && a == _rules.gamesPerSet && b == _rules.gamesPerSet) { + _inTiebreak = true; + _tbPoints = [0, 0]; + _tbFirstServer = _server; // team due to serve after the 12th game + } + } + + private function _resolveTiebreak(team as Number) as Void { + var other = 1 - team; + var a = _tbPoints[team]; + var b = _tbPoints[other]; + if (a >= _rules.tiebreakTarget && (a - b) >= 2) { + _games[team] += 1; // e.g. 7-6 + _winSet(team); + } + } + + private function _winSet(team as Number) as Void { + _sets[team] += 1; + _games = [0, 0]; + if (_inTiebreak) { + _inTiebreak = false; + _server = 1 - _tbFirstServer; // tiebreak receiver serves first next set + } + _resolveMatch(team); + } + + private function _resolveMatch(team as Number) as Void { + if (_sets[team] >= _rules.setsToWin) { + _matchOver = true; + _winner = team; + } + } + + private function _tiebreakServer(pointsPlayed as Number) as Number { + var offset = ((pointsPlayed + 1) / 2) % 2; // integer division + return (offset == 0) ? _tbFirstServer : (1 - _tbFirstServer); + } + + private function _quadrantFor(team as Number, isDeuce as Boolean) as Number { + if (team == US) { + return isDeuce ? QUAD_BOTTOM_RIGHT : QUAD_BOTTOM_LEFT; + } + return isDeuce ? QUAD_UPPER_LEFT : QUAD_UPPER_RIGHT; + } + + private function _pushHistory() as Void { + _history.add({ + "points" => [_points[0], _points[1]], + "games" => [_games[0], _games[1]], + "sets" => [_sets[0], _sets[1]], + "server" => _server, + "inTiebreak" => _inTiebreak, + "tbPoints" => [_tbPoints[0], _tbPoints[1]], + "tbFirstServer" => _tbFirstServer, + "matchOver" => _matchOver, + "winner" => _winner + }); + } +} +``` + +- [ ] **Step 2: Build the test binary (should now compile)** + +```bash +"$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test +``` + +Expected: `BUILD SUCCESSFUL`. + +- [ ] **Step 3: Run the unit tests in the simulator** + +```bash +"$SDK/bin/connectiq" & +"$SDK/bin/monkeydo" bin/test.prg "$DEVICE" -t +``` + +Expected: the console lists each `test...` case as `PASSED` and ends with a summary reporting `PASSED` for all cases and `FAILED` count 0. If any case fails, fix the engine (not the tests) and re-run. + +- [ ] **Step 4: Commit** + +```bash +git add source/PadelMatch.mc +git commit -m "feat: implement PadelMatch scoring engine (tests pass)" +``` + +--- + +## Task 4: CourtRenderer + +Pure drawing. Verified in the simulator (Task 8), but written and compiled now. + +**Files:** +- Create: `source/CourtRenderer.mc` + +- [ ] **Step 1: Write `CourtRenderer.mc`** + +```monkeyc +import Toybox.Lang; +import Toybox.Graphics; + +// Draws a top-down padel court centered at (cx, cy) and a yellow serve ball +// in the given quadrant. Half-width HW and half-height HH keep it inside the +// central band of the round screen. +class CourtRenderer { + private const HW = 60; // half court width (px) + private const HH = 78; // half court height (px) + private const BALL_R = 6; + + public function draw(dc as Graphics.Dc, cx as Number, cy as Number, + quadrant as Number, showBall as Boolean) as Void { + var left = cx - HW; + var top = cy - HH; + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.setPenWidth(2); + dc.drawRectangle(left, top, 2 * HW, 2 * HH); // outer court + dc.drawLine(cx, top, cx, cy + HH); // center service line + + dc.setPenWidth(1); + dc.drawLine(left, cy - HH / 2, left + 2 * HW, cy - HH / 2); // upper service line + dc.drawLine(left, cy + HH / 2, left + 2 * HW, cy + HH / 2); // lower service line + + dc.setPenWidth(3); + dc.drawLine(left, cy, left + 2 * HW, cy); // net + + if (showBall) { + var pos = _ballPos(quadrant, cx, cy); + dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT); + dc.fillCircle(pos[0], pos[1], BALL_R); + } + } + + private function _ballPos(quadrant as Number, cx as Number, cy as Number) as Array { + var dx = HW / 2; + var dy = HH / 2; + if (quadrant == QUAD_BOTTOM_RIGHT) { return [cx + dx, cy + dy]; } + if (quadrant == QUAD_BOTTOM_LEFT) { return [cx - dx, cy + dy]; } + if (quadrant == QUAD_UPPER_LEFT) { return [cx - dx, cy - dy]; } + return [cx + dx, cy - dy]; // QUAD_UPPER_RIGHT + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add source/CourtRenderer.mc +git commit -m "feat: add CourtRenderer (court + yellow serve ball)" +``` + +--- + +## Task 5: HeartRateProvider + +**Files:** +- Create: `source/HeartRateProvider.mc` + +- [ ] **Step 1: Write `HeartRateProvider.mc`** + +```monkeyc +import Toybox.Lang; +import Toybox.Sensor; +import Toybox.UserProfile; + +// Wraps the heart-rate sensor and maps the current bpm to a zone (1..5). +// Returns null bpm when no reading is available (e.g. simulator without data +// simulation), so the view can show a placeholder. +class HeartRateProvider { + private var _hr as Number?; + private var _zones as Array?; + + public function initialize() { + _hr = null; + _zones = null; + } + + public function onStart() as Void { + Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]); + Sensor.enableSensorEvents(method(:onSensor)); + _zones = UserProfile.getHeartRateZones(UserProfile.HR_ZONE_SPORT_GENERIC); + } + + public function onStop() as Void { + Sensor.enableSensorEvents(null); + } + + public function onSensor(info as Sensor.Info) as Void { + _hr = info.heartRate; + } + + public function getHeartRate() as Number? { + return _hr; + } + + // Returns 1..5, or 0 if HR/zones unavailable. + public function getZone() as Number { + if (_hr == null || _zones == null) { return 0; } + var z = _zones as Array; + var hr = _hr as Number; + // _zones holds 6 boundaries: [z1lo, z2lo, z3lo, z4lo, z5lo, max] + for (var i = z.size() - 2; i >= 0; i -= 1) { + if (hr >= z[i]) { return i + 1 > 5 ? 5 : i + 1; } + } + return 1; + } +} +``` + +- [ ] **Step 2: Build a normal (non-test) binary to typecheck the new file** + +```bash +"$SDK/bin/monkeyc" -f monkey.jungle -o bin/garmin-padel.prg -y "$KEY" -d "$DEVICE" +``` + +Expected: `BUILD SUCCESSFUL` (the app still uses the old scaffold view/delegate at this point; those are replaced next). If the build complains about `HR_ZONE_SPORT_GENERIC`, confirm the constant name against the API docs for SDK 9.2.0 and adjust. + +- [ ] **Step 3: Commit** + +```bash +git add source/HeartRateProvider.mc +git commit -m "feat: add HeartRateProvider (bpm + HR zone)" +``` + +--- + +## Task 6: App wiring — own the engine, rules, HR + +Replace the scaffold app so it owns shared state and exposes it to the view/delegate. + +**Files:** +- Modify: `source/garmin-padelApp.mc` + +- [ ] **Step 1: Rewrite `garmin-padelApp.mc`** + +```monkeyc +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelApp extends Application.AppBase { + private var _rules as RulesConfig; + private var _match as PadelMatch; + private var _hr as HeartRateProvider; + + public function initialize() { + AppBase.initialize(); + _rules = new RulesConfig(); + _match = new PadelMatch(_rules); + _hr = new HeartRateProvider(); + } + + public function onStart(state as Dictionary?) as Void { + _hr.onStart(); + } + + public function onStop(state as Dictionary?) as Void { + _hr.onStop(); + } + + public function getMatch() as PadelMatch { return _match; } + public function getHeartRate() as HeartRateProvider { return _hr; } + + public function resetMatch() as Void { + _match = new PadelMatch(_rules); + } + + public function getInitialView() as [Views] or [Views, InputDelegates] { + return [ new garmin_padelView(), new garmin_padelDelegate() ]; + } +} + +function getApp() as garmin_padelApp { + return Application.getApp() as garmin_padelApp; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add source/garmin-padelApp.mc +git commit -m "feat: app owns match/rules/HR and exposes accessors" +``` + +--- + +## Task 7: The View — draw the whole screen + +**Files:** +- Modify: `source/garmin-padelView.mc` + +- [ ] **Step 1: Rewrite `garmin-padelView.mc`** + +```monkeyc +import Toybox.Lang; +import Toybox.Graphics; +import Toybox.WatchUi; +import Toybox.System; +import Toybox.Timer; + +class garmin_padelView extends WatchUi.View { + private const ACCENT = 0x00E0A0; // padel green (sets/games) + private const LABEL_COLOR = 0x808080; // muted grey (S/G/pts labels) + + private var _court as CourtRenderer; + private var _timer as Timer.Timer; + + public function initialize() { + View.initialize(); + _court = new CourtRenderer(); + _timer = new Timer.Timer(); + } + + public function onShow() as Void { + _timer.start(method(:onTick), 1000, true); // refresh clock + HR every second + } + + public function onHide() as Void { + _timer.stop(); + } + + public function onTick() as Void { + WatchUi.requestUpdate(); + } + + public function onUpdate(dc as Graphics.Dc) as Void { + var m = getApp().getMatch(); + var w = dc.getWidth(); + var h = dc.getHeight(); + var cx = w / 2; + var cy = h / 2; + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); + dc.clear(); + + _court.draw(dc, cx, cy, m.servingQuadrant(), !m.isMatchOver()); + _drawLeftColumn(dc, m); + _drawRightColumn(dc, m, w); + _drawHeartRate(dc, cx, 40); + _drawTime(dc, cx, h - 44); + + if (m.isMatchOver()) { + _drawWinner(dc, cx, cy, m); + } + } + + // Left: S and G mini-columns, your team on top, opponents below. + private function _drawLeftColumn(dc as Graphics.Dc, m as PadelMatch) as Void { + var sX = 24; + var gX = 60; + var labelY = 150; + var usY = 178; + var themY = 206; + + dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); + dc.drawText(sX, labelY, Graphics.FONT_XTINY, "S", Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(gX, labelY, Graphics.FONT_XTINY, "G", Graphics.TEXT_JUSTIFY_CENTER); + + dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT); + dc.drawText(sX, usY, Graphics.FONT_TINY, m.sets(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(sX, themY, Graphics.FONT_TINY, m.sets(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(gX, usY, Graphics.FONT_TINY, m.games(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(gX, themY, Graphics.FONT_TINY, m.games(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); + } + + // Right: pts column, your team on top, opponents below. + private function _drawRightColumn(dc as Graphics.Dc, m as PadelMatch, w as Number) as Void { + var pX = w - 42; + var labelY = 150; + var usY = 178; + var themY = 206; + + dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); + dc.drawText(pX, labelY, Graphics.FONT_XTINY, "pts", Graphics.TEXT_JUSTIFY_CENTER); + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText(pX, usY, Graphics.FONT_TINY, m.pointLabel(US), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(pX, themY, Graphics.FONT_TINY, m.pointLabel(THEM), Graphics.TEXT_JUSTIFY_CENTER); + } + + private function _drawHeartRate(dc as Graphics.Dc, cx as Number, y as Number) as Void { + var hr = getApp().getHeartRate(); + var bpm = hr.getHeartRate(); + var text = (bpm == null) ? "-- bpm" : bpm.toString() + " bpm"; + dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT); + dc.drawText(cx, y, Graphics.FONT_TINY, text, Graphics.TEXT_JUSTIFY_CENTER); + _drawZoneBar(dc, cx, y + 26, hr.getZone()); + } + + // 5-segment bar; segments up to the current zone are filled. + private function _drawZoneBar(dc as Graphics.Dc, cx as Number, y as Number, zone as Number) as Void { + var segW = 12; + var segH = 6; + var gap = 3; + var totalW = 5 * segW + 4 * gap; + var x = cx - totalW / 2; + for (var i = 1; i <= 5; i += 1) { + if (i <= zone) { + dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT); + dc.fillRectangle(x, y, segW, segH); + } else { + dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); + dc.drawRectangle(x, y, segW, segH); + } + x += segW + gap; + } + } + + private function _drawTime(dc as Graphics.Dc, cx as Number, y as Number) as Void { + var now = System.getClockTime(); + var text = now.hour.format("%02d") + ":" + now.min.format("%02d"); + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText(cx, y, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER); + } + + private function _drawWinner(dc as Graphics.Dc, cx as Number, cy as Number, m as PadelMatch) as Void { + var text = (m.winner() == US) ? "You win!" : "They win"; + dc.setColor(ACCENT, Graphics.COLOR_BLACK); + dc.fillRectangle(cx - 70, cy - 18, 140, 36); + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT); + dc.drawText(cx, cy - 14, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add source/garmin-padelView.mc +git commit -m "feat: draw full padel screen (score, court, HR, time)" +``` + +--- + +## Task 8: The Delegate — gestures + +**Files:** +- Modify: `source/garmin-padelDelegate.mc` +- Delete: `source/garmin-padelMenuDelegate.mc` + +- [ ] **Step 1: Rewrite `garmin-padelDelegate.mc`** + +```monkeyc +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelDelegate extends WatchUi.BehaviorDelegate { + + public function initialize() { + BehaviorDelegate.initialize(); + } + + public function onSwipe(swipeEvent as WatchUi.SwipeEvent) as Boolean { + var m = getApp().getMatch(); + var dir = swipeEvent.getDirection(); + if (dir == WatchUi.SWIPE_UP) { + m.pointTo(US); + } else if (dir == WatchUi.SWIPE_DOWN) { + m.pointTo(THEM); + } else if (dir == WatchUi.SWIPE_LEFT) { + m.undo(); + } else { + return false; + } + WatchUi.requestUpdate(); + return true; + } + + public function onTap(clickEvent as WatchUi.ClickEvent) as Boolean { + var m = getApp().getMatch(); + if (m.isMatchOver()) { + getApp().resetMatch(); + } else if (m.canSetServer()) { + m.toggleStartingServer(); + } else { + return false; + } + WatchUi.requestUpdate(); + return true; + } +} +``` + +- [ ] **Step 2: Delete the unused menu delegate** + +```bash +git rm source/garmin-padelMenuDelegate.mc +``` + +- [ ] **Step 3: Commit** + +```bash +git add source/garmin-padelDelegate.mc +git commit -m "feat: swipe to score/undo, tap to set server / new match" +``` + +--- + +## Task 9: Permissions, strings, full build & simulator verification + +**Files:** +- Modify: `manifest.xml` (via palette command — do not hand-edit) +- Modify: `resources/strings/strings.xml` + +- [ ] **Step 1: Add Sensor + UserProfile permissions** + +In VS Code run `Monkey C: Edit Permissions` and enable **Sensor** and **UserProfile**. This regenerates `manifest.xml`. + +Verify from the CLI: + +```bash +grep -E "Sensor|UserProfile" manifest.xml +``` + +Expected: two `` / `` lines. + +- [ ] **Step 2: Update strings (remove scaffold menu strings, keep app name)** + +```xml + + Padel + +``` + +(The old `prompt`/`menu_label_*` strings are removed; nothing references them now that the menu is gone.) + +- [ ] **Step 3: Build the release binary** + +```bash +"$SDK/bin/monkeyc" -f monkey.jungle -o bin/garmin-padel.prg -y "$KEY" -d "$DEVICE" +``` + +Expected: `BUILD SUCCESSFUL`. + +- [ ] **Step 4: Run in the simulator and verify behavior** + +```bash +"$SDK/bin/connectiq" & +"$SDK/bin/monkeydo" bin/garmin-padel.prg "$DEVICE" +``` + +Then in the simulator, verify: +- Court is centered with a yellow ball at **bottom-right** (US serves, deuce). +- **Tap** → ball jumps to **upper-left**; tap again → back to bottom-right. +- **Swipe up** → your `pts` go `0→15→30→40`; after the 4th point (golden) your `G` increments and points reset; the ball moves to the opponents' half. +- **Swipe down** scores for opponents; **swipe left** undoes the last point. +- Top shows `-- bpm` (or a value if HR data simulation is enabled via the simulator's **Settings → data-simulation / sensors**) with the zone bar; bottom shows the current time. +- Win a full match → the winner banner appears; **tap** starts a fresh match at server-positioning. + +- [ ] **Step 5: Re-run unit tests to confirm no regressions** + +```bash +"$SDK/bin/monkeyc" -f monkey.jungle -o bin/test.prg -y "$KEY" -d "$DEVICE" --unit-test +"$SDK/bin/monkeydo" bin/test.prg "$DEVICE" -t +``` + +Expected: all cases `PASSED`, 0 failures. + +- [ ] **Step 6: Commit** + +```bash +git add manifest.xml resources/strings/strings.xml +git commit -m "feat: add Sensor/UserProfile permissions, tidy strings" +``` + +--- + +## Done + +The scaffold is now a working padel score tracker. Deferred to future iterations (see spec §8): the settings screen (driving `RulesConfig` and the top/bottom info slots), match history, resume-after-close persistence, and recorded Garmin activity. diff --git a/docs/superpowers/specs/2026-07-02-padel-scoring-design.md b/docs/superpowers/specs/2026-07-02-padel-scoring-design.md new file mode 100644 index 0000000..7a8f06c --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-padel-scoring-design.md @@ -0,0 +1,166 @@ +# Padel Scoring App — Design Spec + +**Date:** 2026-07-02 +**Target:** Garmin Connect IQ watch-app (Monkey C), product `venu445mm` (Venu 4, 45mm, round AMOLED), minApiLevel 6.0.2. +**Status:** Approved design, pre-implementation. + +## 1. Purpose + +A padel score tracker for the watch. It shows the live score (sets / games / points) and a +top-down court diagram whose highlighted quadrant tells you who serves the next point. Points +are recorded by swiping. No GPS, no recorded Garmin activity, no match history in v1. + +## 2. Screen layout + +Round screen. A top band (heart rate) and a bottom band (time) frame a central row split into +three columns: **left** = sets + games, **center** = court, **right** = current-game points. + +``` + ♥ 142 ▮▮▮▯▯ ← heart rate + zone (top band) + + S G pts + 2 4 +------+------+ 15 ← your team (upper number in each column) + 1 3 | | | 40 ← opponents (lower number) + +------+------+ net + | | ● ← yellow ball = next server + +------+------+ + + 14:32 ← current time (bottom band) +``` + +- **Left column — sets & games.** Two mini-columns: **S** (sets) and **G** (games). A small + label (`S`, `G`) sits above each pair of numbers; under it, two stacked numbers — **your team + on top**, opponents below. Sets in **bold accent color**, games in **accent color**. +- **Right column — points.** A small `pts` label (same small font as `S`/`G`) above two stacked + numbers for the current game — your team on top, opponents below — in **white** + (`0 / 15 / 30 / 40`, and `AD` if advantage is enabled). +- **Center — court.** Simplified top-down padel court, your team the **bottom** half and + opponents the **top** half. A small **yellow ball** is drawn in the quadrant of the **next + server** (see §4); it moves as serve rotates. Nothing else is highlighted. +- **Top band — heart rate.** Current heart rate (bpm) with a **zone indicator** (the current HR + zone 1–5, shown as a short colored bar / zone number). Requires a heart-rate sensor reading + (see Permissions below). +- **Bottom band — time.** Current time (`HH:MM`), refreshed by a 1-second timer. + +The small `S` / `G` / `pts` labels share one small font and a muted (dim grey) color so the +numbers stay dominant. + +**Accent color.** A single constant (proposed: a vivid padel green/teal) used for the sets and +games numbers, so it can be changed in one place. The serve ball is **yellow** (its own constant). + +**Permissions.** Reading live heart rate requires the **Sensor** permission; deriving the zone +from the user's profile requires the **UserProfile** permission. Both are added via the +`Monkey C: Edit Permissions` palette command (which regenerates `manifest.xml`). + +## 3. Interaction model + +**During play (gestures):** +- **Swipe up** → point to **your team**. +- **Swipe down** → point to the **opponents**. +- **Swipe left** → **undo** the last recorded point (steps back through game/set boundaries too). + +**Before the first point (tap):** +- A **tap** toggles the starting server between **bottom-right** (your team serves first) and + **upper-left** (opponents serve first) — the yellow ball jumps between those two quadrants. + These are the only two options because the first point + of any game is served from the deuce (right) court, and the opponents' right-hand court maps to + the screen's upper-left. Tapping is only accepted while the match is at its very start (no points + recorded yet). + +**At match end:** +- The court shows a "won by *your team* / *opponents*" state and stops accepting points; **undo + still works**. A **tap** starts a fresh match, returning to server-positioning. + +## 4. Serving / court geometry + +Your team = bottom half, opponents = top half. The yellow ball is drawn in the quadrant +determined by (serving team) × (serve side): + +| Serving team | Serve side | Quadrant | +|--------------|------------|--------------| +| Your team | deuce (right) | bottom-right | +| Your team | ad (left) | bottom-left | +| Opponents | deuce (their right) | upper-left | +| Opponents | ad (their left) | upper-right | + +**Serve side by point parity.** Even number of points played so far in the current game → +deuce (right); odd → ad (left). Game start (0-0) is always deuce. + +**Serving team.** +- Normal game: one team serves the entire game; the serving team alternates after each completed game. +- The first server of the match is chosen by tap (see §3). + +**Tiebreak serving** (when tiebreak is enabled and reached, see §5): +- Serve side still follows point parity: even total tiebreak points → deuce, odd → ad. +- The first server serves 1 point, then service alternates every 2 points. (Server for the point + with `P` points already played = the team given by `floor((P+1)/2) mod 2` offset from the + team due to serve at 6-6.) + +## 5. Scoring rules + +Rules are held in a `RulesConfig` object that the engine reads as parameters, so the future +settings screen can drive them without an engine rewrite. **v1 defaults** (all overridable later): + +- **Points in a game:** `0 / 15 / 30 / 40`. +- **40-40 resolution:** **golden point** (default). Next point at 40-40 wins the game. When + `RulesConfig` is set to advantage instead, 40-40 = deuce, then advantage (`AD`), then game; + must win by two points. +- **Set:** first to **6 games, win by 2**. +- **6-6:** **7-point tiebreak** (win by 2). When tiebreak is disabled in `RulesConfig`, the set + continues until a team leads by 2 games. +- **Match:** **best of 3 sets** (first to 2 sets). + +## 6. Architecture (Monkey C) + +Keeps the scaffold's file-per-role split; adds two logic-only files. The scoring engine imports no +`WatchUi`/`Graphics`, so it is unit-testable in isolation. + +- **`source/PadelMatch.mc`** — the scoring engine. Holds points/games/sets for both teams, the + serving team, tiebreak state, and match-over/winner. Public surface: + - `pointTo(team)` — record a point, advancing game/set/match as needed. + - `undo()` — revert the last recorded point. + - `servingQuadrant()` — returns which of the four quadrants holds the serve ball. + - `setStartingServer(team)` / toggle — only valid at match start. + - accessors for the display: points (per team, formatted), games, sets, `isMatchOver()`, `winner()`. + - **Undo strategy:** push a full state snapshot onto a stack before each `pointTo`; `undo` pops + and restores. This makes swipe-left correct across point/game/set boundaries with no special-casing. +- **`source/RulesConfig.mc`** — the rules struct (golden-point flag, sets-to-win, games-per-set, + tiebreak flag/target) plus the v1 defaults. +- **`source/CourtRenderer.mc`** — draws the top-down court and the yellow serve ball in a given + quadrant, so the view stays focused on layout. Pure drawing against a `Dc`. +- **`source/HeartRateProvider.mc`** — wraps the heart-rate sensor and zone lookup. Enables + `Sensor.SENSOR_HEARTRATE`, exposes the latest bpm, and maps it to a zone 1–5 using + `UserProfile.getHeartRateZones`. Returns a "no reading" state when HR is unavailable + (simulator / no strap) so the view can render a placeholder. +- **`source/garmin-padelView.mc`** — `WatchUi.View`. `onUpdate` draws the top HR band, the left + sets/games column, the center court (via `CourtRenderer`), the right points column, and the + bottom time band. A 1-second `Timer` calls `requestUpdate` to keep the clock (and HR) live. +- **`source/garmin-padelDelegate.mc`** — `BehaviorDelegate`. Maps `onSwipe` (up/down/left) and + `onTap` (pre-match server toggle; new match after match end) to the engine, then `requestUpdate`. +- **`source/garmin-padelApp.mc`** — owns the `PadelMatch`, `RulesConfig`, and `HeartRateProvider` + instances; wires the view + delegate; enables sensors on start and disables them on stop. + +Resources (`resources/`): the whole screen is drawn programmatically in `onUpdate` rather than via +a static layout, since positions depend on state. `strings.xml` holds team labels and the +match-end text; the template menu can be removed or repurposed later. Sensor + UserProfile +permissions are declared in `manifest.xml` via the Edit Permissions command. + +## 7. Testing + +- **Engine unit tests** (`monkeyc --unit-test` with a test runner) cover `PadelMatch`: + - point → game → set → match progression (golden point and advantage modes), + - tiebreak entry at 6-6, tiebreak win-by-2, + - serving team alternation between games and serve-side parity within a game, + - tiebreak serving rotation, + - `servingQuadrant()` mapping for all four cases, + - `undo()` across point/game/set boundaries and at match start (no-op). +- Rendering (`CourtRenderer`, view, HR band) is verified in the simulator (HR via the + simulator's data-simulation, since there is no real strap). + +## 8. Deferred (not in v1) + +- Settings screen (drives `RulesConfig`; makes the top/bottom info slots configurable). +- Match history / saved matches. +- Resume-after-close persistence (engine state is in memory only for v1). +- Recorded Garmin activity, GPS. (Heart rate **is** in v1 as a read-only display, but the match is + still not recorded as a Garmin activity.) diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..b75ea23 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monkey.jungle b/monkey.jungle new file mode 100644 index 0000000..87796c7 --- /dev/null +++ b/monkey.jungle @@ -0,0 +1 @@ +project.manifest = manifest.xml diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml new file mode 100644 index 0000000..6302154 --- /dev/null +++ b/resources/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources/drawables/launcher_icon.svg b/resources/drawables/launcher_icon.svg new file mode 100644 index 0000000..4719135 --- /dev/null +++ b/resources/drawables/launcher_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/drawables/monkey.png b/resources/drawables/monkey.png new file mode 100644 index 0000000..2f172d7 Binary files /dev/null and b/resources/drawables/monkey.png differ diff --git a/resources/layouts/layout.xml b/resources/layouts/layout.xml new file mode 100644 index 0000000..95968a4 --- /dev/null +++ b/resources/layouts/layout.xml @@ -0,0 +1,4 @@ + + diff --git a/resources/menus/menu.xml b/resources/menus/menu.xml new file mode 100644 index 0000000..1a48c79 --- /dev/null +++ b/resources/menus/menu.xml @@ -0,0 +1,4 @@ + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml new file mode 100644 index 0000000..5e63980 --- /dev/null +++ b/resources/strings/strings.xml @@ -0,0 +1,8 @@ + + garmin-padel + + Click the menu button + + Item 1 + Item 2 + diff --git a/source/garmin-padelApp.mc b/source/garmin-padelApp.mc new file mode 100644 index 0000000..d5c690d --- /dev/null +++ b/source/garmin-padelApp.mc @@ -0,0 +1,28 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelApp extends Application.AppBase { + + function initialize() { + AppBase.initialize(); + } + + // onStart() is called on application start up + function onStart(state as Dictionary?) as Void { + } + + // onStop() is called when your application is exiting + function onStop(state as Dictionary?) as Void { + } + + // Return the initial view of your application here + function getInitialView() as [Views] or [Views, InputDelegates] { + return [ new garmin_padelView(), new garmin_padelDelegate() ]; + } + +} + +function getApp() as garmin_padelApp { + return Application.getApp() as garmin_padelApp; +} \ No newline at end of file diff --git a/source/garmin-padelDelegate.mc b/source/garmin-padelDelegate.mc new file mode 100644 index 0000000..68e488c --- /dev/null +++ b/source/garmin-padelDelegate.mc @@ -0,0 +1,15 @@ +import Toybox.Lang; +import Toybox.WatchUi; + +class garmin_padelDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } + + function onMenu() as Boolean { + WatchUi.pushView(new Rez.Menus.MainMenu(), new garmin_padelMenuDelegate(), WatchUi.SLIDE_UP); + return true; + } + +} \ No newline at end of file diff --git a/source/garmin-padelMenuDelegate.mc b/source/garmin-padelMenuDelegate.mc new file mode 100644 index 0000000..931d197 --- /dev/null +++ b/source/garmin-padelMenuDelegate.mc @@ -0,0 +1,19 @@ +import Toybox.Lang; +import Toybox.System; +import Toybox.WatchUi; + +class garmin_padelMenuDelegate extends WatchUi.MenuInputDelegate { + + function initialize() { + MenuInputDelegate.initialize(); + } + + function onMenuItem(item as Symbol) as Void { + if (item == :item_1) { + System.println("item 1"); + } else if (item == :item_2) { + System.println("item 2"); + } + } + +} \ No newline at end of file diff --git a/source/garmin-padelView.mc b/source/garmin-padelView.mc new file mode 100644 index 0000000..492c2e9 --- /dev/null +++ b/source/garmin-padelView.mc @@ -0,0 +1,33 @@ +import Toybox.Graphics; +import Toybox.WatchUi; + +class garmin_padelView extends WatchUi.View { + + function initialize() { + View.initialize(); + } + + // Load your resources here + function onLayout(dc as Dc) as Void { + setLayout(Rez.Layouts.MainLayout(dc)); + } + + // Called when this View is brought to the foreground. Restore + // the state of this View and prepare it to be shown. This includes + // loading resources into memory. + function onShow() as Void { + } + + // Update the view + function onUpdate(dc as Dc) as Void { + // Call the parent onUpdate function to redraw the layout + View.onUpdate(dc); + } + + // Called when this View is removed from the screen. Save the + // state of this View here. This includes freeing resources from + // memory. + function onHide() as Void { + } + +}