Compare commits

..

11 Commits

Author SHA1 Message Date
20a3ee40a1 docs: add NEXT-STEPS note (activity recording + resume context)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:50:27 +02:00
926f876a6e feat: refine watch UI — arc HR gauge, proper padel court, spacing
- HR shown as a top arc gauge in Garmin zone colors (grey/blue/green/
  orange/red) with a marker on the current zone and the bpm number below
- Redraw court to a proper padel layout: net centered, service lines at
  the 3m-from-back-wall proportion, center line only between service lines
- Wider court with a configurable blue surface (bgColor param on
  CourtRenderer, ready for the future settings screen)
- Serve ball marks the back zone (farthest from the net) of the server
- Roomier score spacing; games column white, sets accent green; "Pts"
- HR + time on a matched FONT_MEDIUM

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:42:33 +02:00
33d3f4f08e chore: tidy EOF newlines, drop dead monkey.png, vcenter winner banner text
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:51:25 +02:00
ffb8149c45 chore: remove orphaned scaffold menu/layout resources, tidy strings to Padel
- Delete resources/menus/menu.xml (scaffold menu, replaced by custom UI in Task 8)
- Delete resources/layouts/layout.xml (scaffold layout, view now draws in onUpdate)
- Trim resources/strings/strings.xml to AppName="Padel" only; remove prompt,
  menu_label_1, menu_label_2 which were only referenced by the deleted files
- Release build: BUILD SUCCESSFUL
- Unit tests: PASSED (passed=16, failed=0, errors=0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:30:27 +02:00
ae101d1b88 feat: swipe to score/undo, tap to set server / new match
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:24:29 +02:00
8dab8183d7 feat: draw full padel screen (score, court, HR, time)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:11:18 +02:00
48f7d5c7dc feat: app owns match/rules/HR and exposes accessors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:59:31 +02:00
693bcf9a58 feat: add HeartRateProvider (bpm + HR zone)
Add Sensor and UserProfile permissions to manifest.xml (required for
Toybox.Sensor and Toybox.UserProfile API access; build fails without them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:53:30 +02:00
faf9eafe45 feat: add CourtRenderer (court + yellow serve ball)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:44:07 +02:00
1d9d878c3d feat: implement PadelMatch scoring engine (tests pass)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:39:21 +02:00
4ae32743d7 test: add win-by-2 coverage (golden point 40-40, set 7-5, tiebreak 8-6)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:32:37 +02:00
14 changed files with 588 additions and 63 deletions

View File

@@ -0,0 +1,63 @@
# Next steps
Scratch continuity note for resuming work (e.g. on another machine). The
authoritative design lives in `specs/2026-07-02-padel-scoring-design.md` and
`plans/2026-07-02-padel-scoring.md`.
## Current status (2026-07-05)
- Scoring app is **implemented and working**: UI-free `PadelMatch` engine
(17 unit tests pass in the sim), `CourtRenderer`, `HeartRateProvider`, and
the View/Delegate/App. Built and verified in the Connect IQ simulator and
sideloaded to a real Venu 4 (venu445mm).
- UI has been refined beyond the plan: top **HR arc gauge** in Garmin zone
colors (grey/blue/green/orange/red) with a marker on the current zone and
the bpm number below; **proper padel court** (net centered, service lines at
the 3m-from-back-wall proportion, center line only between service lines),
wider with a configurable blue surface (`bgColor` on `CourtRenderer`); serve
ball marks the back zone; games column white, sets accent green, "Pts"; HR +
time on a matched `FONT_MEDIUM`.
- All committed on `main` and pushed to https://scm.vilanet.fr/kriss/garmin-padel.
## Build / run reminder
- `developer_key` is git-ignored (private signing key) — provide it out of band.
- SDK: connectiq-sdk-mac-9.2.0. Device: `venu445mm`.
- Build: `"$SDK/bin/monkeyc" -f monkey.jungle -o bin/garmin-padel.prg -y ./developer_key -d venu445mm`
- Run: `"$SDK/bin/connectiq" &` then `"$SDK/bin/monkeydo" bin/garmin-padel.prg venu445mm`
- Unit tests: build with `--unit-test` then `monkeydo bin/test.prg venu445mm -t`
(monkeydo's exit code 1 is a known simulator artifact; trust the in-app
`passed/failed` summary).
## Input gotcha (important)
The **simulator merges the back-swipe and the physical back button** into one
`onBack`, so swipe-left appears to exit the app there. On **real hardware** they
are separate: swipe-left reaches `onSwipe(SWIPE_LEFT)` (→ undo, consumed) and
the physical button triggers the default back (→ exit). The delegate relies on
this — test back/undo behavior **on the watch, not the sim**.
## Next feature: record a padel activity
The app currently only *displays* the score + a live HR readout; it does **not**
record a Garmin activity (spec §8 deferred). Add session recording via
`Toybox.ActivityRecording`:
- create/start a session (find the best sport type — native padel/racquet if
the Venu 4 exposes one, else a generic training/sport type),
- pause/resume, and save-or-discard at match end (appears in Garmin Connect
with duration, HR zones, calories),
- likely show an elapsed activity timer on screen.
**Open design questions to settle first (do a brief brainstorm/spec pass):**
- Auto-start the session on launch, on the first point, or via an explicit start?
- Which sport type is available on venu445mm?
- Save prompt at match end (save / discard / resume)?
- Where to surface the elapsed activity timer in the layout?
## Other deferred items (spec §8)
- Settings screen driving `RulesConfig` (golden point, sets, tiebreak) and the
top/bottom info slots (incl. the court color already wired as `bgColor`).
- Match history.
- Resume-after-close persistence.

View File

@@ -20,7 +20,10 @@
Use "Monkey C: Edit Permissions" from the Visual Studio Code command Use "Monkey C: Edit Permissions" from the Visual Studio Code command
palette to update permissions. palette to update permissions.
--> -->
<iq:permissions/> <iq:permissions>
<iq:uses-permission id="Sensor"/>
<iq:uses-permission id="UserProfile"/>
</iq:permissions>
<!-- <!--
Use "Monkey C: Edit Languages" from the Visual Studio Code command Use "Monkey C: Edit Languages" from the Visual Studio Code command
palette to edit your compatible language list. palette to edit your compatible language list.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,4 +0,0 @@
<layout id="MainLayout" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://developer.garmin.com/downloads/connect-iq/resources.xsd">
<label x="center" y="5" text="@Strings.prompt" color="Graphics.COLOR_WHITE" justification="Graphics.TEXT_JUSTIFY_CENTER" />
<bitmap id="id_monkey" x="center" y="30" filename="../drawables/monkey.png" />
</layout>

View File

@@ -1,4 +0,0 @@
<menu id="MainMenu" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://developer.garmin.com/downloads/connect-iq/resources.xsd">
<menu-item id="item_1" label="@Strings.menu_label_1" />
<menu-item id="item_2" label="@Strings.menu_label_2" />
</menu>

View File

@@ -1,8 +1,3 @@
<strings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://developer.garmin.com/downloads/connect-iq/resources.xsd"> <strings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://developer.garmin.com/downloads/connect-iq/resources.xsd">
<string id="AppName">garmin-padel</string> <string id="AppName">Padel</string>
<string id="prompt">Click the menu button</string>
<string id="menu_label_1">Item 1</string>
<string id="menu_label_2">Item 2</string>
</strings> </strings>

52
source/CourtRenderer.mc Normal file
View File

@@ -0,0 +1,52 @@
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 = 92; // half court width (px)
private const HH = 95; // half court height (px)
private const BALL_R = 7;
// bgColor fills the court surface (blue for now; a future settings screen
// will drive it via RulesConfig).
public function draw(dc as Graphics.Dc, cx as Number, cy as Number,
quadrant as Number, showBall as Boolean,
bgColor as Number) as Void {
var left = cx - HW;
var top = cy - HH;
// Service line sits 3m from the back wall (7m from the net) on a 10m
// half, i.e. 0.7 of the way from the net to the back boundary.
var sl = (7 * HH) / 10;
dc.setColor(bgColor, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(left, top, 2 * HW, 2 * HH); // court surface
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.setPenWidth(2);
dc.drawRectangle(left, top, 2 * HW, 2 * HH); // outer boundary
dc.drawLine(left, cy - sl, left + 2 * HW, cy - sl); // upper service line
dc.drawLine(left, cy + sl, left + 2 * HW, cy + sl); // lower service line
dc.drawLine(cx, cy - sl, cx, cy + sl); // center line (between service lines only)
dc.setPenWidth(4);
dc.drawLine(left, cy, left + 2 * HW, cy); // net (boldest)
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<Number> {
var dx = HW / 2;
var dy = (17 * HH) / 20; // centered in the back zone, farthest from the net
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
}
}

View File

@@ -0,0 +1,46 @@
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<Number>?;
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<Number>;
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;
}
}

200
source/PadelMatch.mc Normal file
View File

@@ -0,0 +1,200 @@
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<Number>; // raw point count per team in the current game
private var _games as Array<Number>; // games won in the current set
private var _sets as Array<Number>; // 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<Number>;
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<Dictionary>;
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<Number>;
_games = s["games"] as Array<Number>;
_sets = s["sets"] as Array<Number>;
_server = s["server"] as Number;
_inTiebreak = s["inTiebreak"] as Boolean;
_tbPoints = s["tbPoints"] as Array<Number>;
_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
});
}
}

View File

@@ -139,6 +139,46 @@ function winGameFor(m as PadelMatch, team as Number) as Void {
m.pointTo(US); // match started; toggle now locked m.pointTo(US); // match started; toggle now locked
m.toggleStartingServer(); m.toggleStartingServer();
Test.assertEqualMessage(m.canSetServer(), false, "server locked after first point"); Test.assertEqualMessage(m.canSetServer(), false, "server locked after first point");
Test.assertEqualMessage(m.servingQuadrant(), QUAD_UPPER_RIGHT, "server unchanged after locked toggle");
return true;
}
(:test) function testGoldenPointSuddenDeath(logger as Logger) as Boolean {
var m = newMatch(); // golden point default
for (var i = 0; i < 3; i += 1) { m.pointTo(US); m.pointTo(THEM); }
Test.assertEqualMessage(m.pointLabel(US), "40", "US 40 at deuce");
Test.assertEqualMessage(m.pointLabel(THEM), "40", "THEM 40 at deuce");
m.pointTo(US); // sudden death: next point wins
Test.assertEqualMessage(m.games(US), 1, "US wins game on golden point");
Test.assertEqualMessage(m.pointLabel(US), "0", "points reset after game");
return true;
}
(:test) function testSetWinBySevenFive(logger as Logger) as Boolean {
var m = newMatch();
for (var g = 0; g < 5; g += 1) { winGameFor(m, US); winGameFor(m, THEM); }
Test.assertEqualMessage(m.games(US), 5, "5-5 games");
winGameFor(m, US); // 6-5: not a set yet (win by 2)
Test.assertEqualMessage(m.sets(US), 0, "no set at 6-5");
Test.assertEqualMessage(m.isInTiebreak(), false, "no tiebreak at 6-5");
Test.assertEqualMessage(m.games(US), 6, "US leads 6-5");
winGameFor(m, US); // 7-5: set won
Test.assertEqualMessage(m.sets(US), 1, "US wins set 7-5");
return true;
}
(:test) function testTiebreakWinByTwo(logger as Logger) as Boolean {
var m = newMatch();
for (var g = 0; g < 6; g += 1) { winGameFor(m, US); winGameFor(m, THEM); }
Test.assertEqualMessage(m.isInTiebreak(), true, "tiebreak at 6-6");
for (var p = 0; p < 6; p += 1) { m.pointTo(US); m.pointTo(THEM); } // 6-6 in tiebreak
Test.assertEqualMessage(m.isInTiebreak(), true, "still in tiebreak at 6-6");
m.pointTo(US); // 7-6: not won (win by 2)
Test.assertEqualMessage(m.isInTiebreak(), true, "7-6 not enough");
Test.assertEqualMessage(m.sets(US), 0, "no set at 7-6");
m.pointTo(US); // 8-6: set won
Test.assertEqualMessage(m.isInTiebreak(), false, "tiebreak done at 8-6");
Test.assertEqualMessage(m.sets(US), 1, "US wins set via 8-6 tiebreak");
return true; return true;
} }

View File

@@ -3,24 +3,35 @@ import Toybox.Lang;
import Toybox.WatchUi; import Toybox.WatchUi;
class garmin_padelApp extends Application.AppBase { class garmin_padelApp extends Application.AppBase {
private var _rules as RulesConfig;
private var _match as PadelMatch;
private var _hr as HeartRateProvider;
function initialize() { public function initialize() {
AppBase.initialize(); AppBase.initialize();
_rules = new RulesConfig();
_match = new PadelMatch(_rules);
_hr = new HeartRateProvider();
} }
// onStart() is called on application start up public function onStart(state as Dictionary?) as Void {
function onStart(state as Dictionary?) as Void { _hr.onStart();
} }
// onStop() is called when your application is exiting public function onStop(state as Dictionary?) as Void {
function onStop(state as Dictionary?) as Void { _hr.onStop();
} }
// Return the initial view of your application here public function getMatch() as PadelMatch { return _match; }
function getInitialView() as [Views] or [Views, InputDelegates] { 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() ]; return [ new garmin_padelView(), new garmin_padelDelegate() ];
} }
} }
function getApp() as garmin_padelApp { function getApp() as garmin_padelApp {

View File

@@ -3,13 +3,36 @@ import Toybox.WatchUi;
class garmin_padelDelegate extends WatchUi.BehaviorDelegate { class garmin_padelDelegate extends WatchUi.BehaviorDelegate {
function initialize() { public function initialize() {
BehaviorDelegate.initialize(); BehaviorDelegate.initialize();
} }
function onMenu() as Boolean { public function onSwipe(swipeEvent as WatchUi.SwipeEvent) as Boolean {
WatchUi.pushView(new Rez.Menus.MainMenu(), new garmin_padelMenuDelegate(), WatchUi.SLIDE_UP); 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; 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;
}
} }

View File

@@ -1,19 +0,0 @@
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");
}
}
}

View File

@@ -1,33 +1,152 @@
import Toybox.Lang;
import Toybox.Graphics; import Toybox.Graphics;
import Toybox.WatchUi; import Toybox.WatchUi;
import Toybox.System;
import Toybox.Timer;
import Toybox.Math;
class garmin_padelView extends WatchUi.View { 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 const COURT_BLUE = 0x1560BD; // court surface fill (configurable later)
function initialize() { private var _court as CourtRenderer;
private var _timer as Timer.Timer;
public function initialize() {
View.initialize(); View.initialize();
_court = new CourtRenderer();
_timer = new Timer.Timer();
} }
// Load your resources here public function onShow() as Void {
function onLayout(dc as Dc) as Void { _timer.start(method(:onTick), 1000, true); // refresh clock + HR every second
setLayout(Rez.Layouts.MainLayout(dc));
} }
// Called when this View is brought to the foreground. Restore public function onHide() as Void {
// the state of this View and prepare it to be shown. This includes _timer.stop();
// loading resources into memory.
function onShow() as Void {
} }
// Update the view public function onTick() as Void {
function onUpdate(dc as Dc) as Void { WatchUi.requestUpdate();
// Call the parent onUpdate function to redraw the layout
View.onUpdate(dc);
} }
// Called when this View is removed from the screen. Save the public function onUpdate(dc as Graphics.Dc) as Void {
// state of this View here. This includes freeing resources from var m = getApp().getMatch();
// memory. var w = dc.getWidth();
function onHide() as Void { 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(), COURT_BLUE);
_drawLeftColumn(dc, m);
_drawRightColumn(dc, m, w);
_drawHeartRate(dc, cx, cy);
_drawTime(dc, cx, h - 78);
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 = 30;
var gX = 76;
var labelY = 148;
var usY = 184;
var themY = 240;
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.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
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 - 46;
var labelY = 148;
var usY = 184;
var themY = 240;
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);
}
// Heart rate as a top arc gauge: 5 zone-colored segments (grey/blue/green/
// orange/red), a white marker on the current zone, and the bpm number just
// below — mirroring the standard Garmin run-activity HR view.
private function _drawHeartRate(dc as Graphics.Dc, cx as Number, cy as Number) as Void {
var hr = getApp().getHeartRate();
var bpm = hr.getHeartRate();
var zone = hr.getZone();
var r = cx - 9; // arc radius, hugging the top bezel
// 5 zone segments across the top (150deg .. 30deg, clockwise)
dc.setPenWidth(14);
for (var k = 0; k < 5; k += 1) {
var segStart = 150 - k * 24;
dc.setColor(_zoneColor(k + 1), Graphics.COLOR_TRANSPARENT);
dc.drawArc(cx, cy, r, Graphics.ARC_CLOCKWISE, segStart - 2, segStart - 22);
}
dc.setPenWidth(1);
// marker on the current zone
if (zone >= 1 && zone <= 5) {
var mid = 150 - (zone - 1) * 24 - 12;
var rad = Math.toRadians(mid);
var mx = (cx + r * Math.cos(rad)).toNumber();
var my = (cy - r * Math.sin(rad)).toNumber();
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.fillCircle(mx, my, 8);
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
dc.drawCircle(mx, my, 8);
}
// exact rate, tucked just under the arc (dim placeholder when no reading,
// so it never reads as a court line)
var text = (bpm == null) ? "--" : bpm.toString();
var col = (bpm == null) ? LABEL_COLOR : Graphics.COLOR_WHITE;
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, cy - 178, Graphics.FONT_MEDIUM, text, Graphics.TEXT_JUSTIFY_CENTER);
}
// Garmin HR zone colors: 1 grey, 2 blue, 3 green, 4 orange, 5 red.
private function _zoneColor(z as Number) as Number {
if (z == 1) { return Graphics.COLOR_LT_GRAY; }
if (z == 2) { return Graphics.COLOR_BLUE; }
if (z == 3) { return Graphics.COLOR_GREEN; }
if (z == 4) { return Graphics.COLOR_ORANGE; }
return Graphics.COLOR_RED;
}
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_MEDIUM, 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, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
}
} }