Tap-to-rotate: tap icon to select, tap selected to open

Tapping an unselected icon in the menu ring rotates to it with
animation. Tapping the already-selected icon opens it. Icon
positions are tracked from last render for hit detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-13 19:35:35 +02:00
parent bcac9cbaa4
commit a45f1b5215
2 changed files with 61 additions and 3 deletions

View file

@ -1,9 +1,9 @@
import Toybox.Lang; import Toybox.Lang;
import Toybox.WatchUi; import Toybox.WatchUi;
// Routes menu input. UP/DOWN rotate the ring. START/STOP dispatches: // Routes menu input. UP/DOWN rotate the ring. START/STOP opens
// - event types (arrest, start, ) push the GPS LoadingView // the selected item. Tap on an icon rotates to it; tap on the
// - history / delete are stubbed until later phases // already-selected icon opens it.
class MenuDelegate extends WatchUi.BehaviorDelegate { class MenuDelegate extends WatchUi.BehaviorDelegate {
private var _view as MenuView; private var _view as MenuView;
@ -23,7 +23,27 @@ class MenuDelegate extends WatchUi.BehaviorDelegate {
return true; return true;
} }
function onTap(evt as WatchUi.ClickEvent) as Boolean {
var coords = evt.getCoordinates();
var tapX = coords[0] as Number;
var tapY = coords[1] as Number;
var idx = _view.itemIndexAt(tapX, tapY);
if (idx < 0) { return false; }
if (idx == _view.selectedIndex()) {
// Tap on already-selected item open it.
return _openSelected();
}
// Tap on another item rotate to it.
_view.rotateTo(idx);
return true;
}
function onSelect() as Boolean { function onSelect() as Boolean {
return _openSelected();
}
private function _openSelected() as Boolean {
var item = _view.selectedItem(); var item = _view.selectedItem();
var key = item[:key] as String; var key = item[:key] as String;

View file

@ -76,6 +76,41 @@ class MenuView extends WatchUi.View {
return _items[_selectedIndex]; return _items[_selectedIndex];
} }
function selectedIndex() as Number {
return _selectedIndex;
}
private var _iconPositions as Array = [];
// Returns the index of the icon closest to (tapX, tapY), or -1.
function itemIndexAt(tapX as Number, tapY as Number) as Number {
var bestIdx = -1;
var bestDist = 999999;
for (var i = 0; i < _iconPositions.size(); i++) {
var pos = _iconPositions[i] as Array;
var ix = pos[0] as Number;
var iy = pos[1] as Number;
var size = pos[2] as Number;
var dx = tapX - ix;
var dy = tapY - iy;
var dist = dx * dx + dy * dy;
var maxDist = (size / 2) * (size / 2);
if (dist < maxDist && dist < bestDist) {
bestDist = dist;
bestIdx = i;
}
}
return bestIdx;
}
// Rotate to a specific index with animation.
function rotateTo(index as Number) as Void {
if (index == _selectedIndex) { return; }
_startAnim(_selectedIndex);
_selectedIndex = index;
Application.Storage.setValue(STORAGE_KEY, _selectedIndex);
}
function onUpdate(dc as Dc) as Void { function onUpdate(dc as Dc) as Void {
dc.setColor(Config.COLOR_FG, Config.COLOR_BG); dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
dc.clear(); dc.clear();
@ -102,6 +137,7 @@ class MenuView extends WatchUi.View {
if (diff < -n / 2.0) { diff = diff + n; } if (diff < -n / 2.0) { diff = diff + n; }
var currentOffset = fromOffset + diff * ease; var currentOffset = fromOffset + diff * ease;
_iconPositions = new [n];
for (var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
var angle = selectionAngle + (i.toFloat() - currentOffset) * step; var angle = selectionAngle + (i.toFloat() - currentOffset) * step;
var x = (cx + radius * Math.cos(angle)).toNumber(); var x = (cx + radius * Math.cos(angle)).toNumber();
@ -119,6 +155,8 @@ class MenuView extends WatchUi.View {
targetSize = (selSize - (selSize - baseSize) * ease).toNumber(); targetSize = (selSize - (selSize - baseSize) * ease).toNumber();
} }
_iconPositions[i] = [x, y, targetSize];
// Colored circle behind icon. // Colored circle behind icon.
var color = _items[i][:color] as Number; var color = _items[i][:color] as Number;
dc.setColor(color, Config.COLOR_BG); dc.setColor(color, Config.COLOR_BG);