Phase 2: 10-icon menu ring
Rotating ring modeled after the watch Controls menu: UP/DOWN spin,
START/STOP selects. Selection point sits at −30° (≈ 2 o'clock) so it
lines up with the physical enter button on 5-button round Garmins.
Icons are rasterized at 80×80 with automaticPalette="false" and
scaled via drawBitmap2 to stay crisp at any display resolution. Long
German compounds ("Einsatzbeginn", "Beweismittel", "Letzten löschen")
wrap to two lines via a Config array so the center label never
overlaps the surrounding icons. Selected index is persisted in
Application.Storage and restored on next launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
43f970d764
commit
d3494acc0d
18 changed files with 203 additions and 75 deletions
109
source/MenuView.mc
Normal file
109
source/MenuView.mc
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import Toybox.Application;
|
||||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
import Toybox.Math;
|
||||
import Toybox.WatchUi;
|
||||
|
||||
// Rotating 10-icon ring modelled after the watch Controls menu.
|
||||
// Selection point is fixed at the 3 o'clock position (START/STOP
|
||||
// button height). UP/DOWN input rotates the ring.
|
||||
class MenuView extends WatchUi.View {
|
||||
|
||||
const STORAGE_KEY = "menu_idx";
|
||||
|
||||
private var _items as Array<Dictionary>;
|
||||
private var _selectedIndex as Number = 0;
|
||||
private var _bitmaps as Dictionary = {};
|
||||
|
||||
function initialize() {
|
||||
View.initialize();
|
||||
_items = Config.menuItems();
|
||||
var stored = Application.Storage.getValue(STORAGE_KEY);
|
||||
if (stored instanceof Number && stored >= 0 && stored < _items.size()) {
|
||||
_selectedIndex = stored;
|
||||
}
|
||||
}
|
||||
|
||||
function onLayout(dc as Dc) as Void {
|
||||
for (var i = 0; i < _items.size(); i++) {
|
||||
var iconId = _items[i][:icon];
|
||||
_bitmaps[iconId] = WatchUi.loadResource(iconId) as BitmapResource;
|
||||
}
|
||||
}
|
||||
|
||||
function rotateNext() as Void {
|
||||
_selectedIndex = (_selectedIndex + 1) % _items.size();
|
||||
Application.Storage.setValue(STORAGE_KEY, _selectedIndex);
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function rotatePrev() as Void {
|
||||
_selectedIndex = (_selectedIndex - 1 + _items.size()) % _items.size();
|
||||
Application.Storage.setValue(STORAGE_KEY, _selectedIndex);
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function selectedItem() as Dictionary {
|
||||
return _items[_selectedIndex];
|
||||
}
|
||||
|
||||
function onUpdate(dc as Dc) as Void {
|
||||
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
|
||||
dc.clear();
|
||||
|
||||
var cx = LayoutMetrics.centerX(dc);
|
||||
var cy = LayoutMetrics.centerY(dc);
|
||||
var radius = LayoutMetrics.ringRadius(dc);
|
||||
var baseSize = LayoutMetrics.iconSize(dc);
|
||||
var selSize = LayoutMetrics.selectedIconSize(dc);
|
||||
var n = _items.size();
|
||||
|
||||
var selectionAngle = Config.SELECTION_ANGLE_DEG * Math.PI / 180.0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var offset = i - _selectedIndex;
|
||||
var angle = selectionAngle + (2.0 * Math.PI * offset) / n;
|
||||
var x = (cx + radius * Math.cos(angle)).toNumber();
|
||||
var y = (cy + radius * Math.sin(angle)).toNumber();
|
||||
var isSelected = (i == _selectedIndex);
|
||||
var targetSize = isSelected ? selSize : baseSize;
|
||||
|
||||
var iconId = _items[i][:icon];
|
||||
var bmp = _bitmaps[iconId] as BitmapResource;
|
||||
if (bmp != null) {
|
||||
_drawScaledIcon(dc, bmp, x, y, targetSize);
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
dc.setPenWidth(LayoutMetrics.accentPenWidth(dc));
|
||||
dc.setColor(Config.COLOR_ACCENT, Config.COLOR_BG);
|
||||
dc.drawCircle(x, y, targetSize / 2 + 5);
|
||||
}
|
||||
}
|
||||
|
||||
var lines = _items[_selectedIndex][:lines] as Array<String>;
|
||||
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
|
||||
var font = Graphics.FONT_TINY;
|
||||
var lineHeight = dc.getFontHeight(font);
|
||||
var totalHeight = lines.size() * lineHeight;
|
||||
var startY = cy - totalHeight / 2 + lineHeight / 2;
|
||||
for (var j = 0; j < lines.size(); j++) {
|
||||
dc.drawText(cx, startY + j * lineHeight, font, lines[j],
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// Scales a bitmap to the requested pixel size and draws it centered
|
||||
// at (cx, cy). Uses drawBitmap2 so icons stay crisp on any resolution.
|
||||
private function _drawScaledIcon(dc as Dc, bmp as BitmapResource,
|
||||
cx as Number, cy as Number,
|
||||
targetSize as Number) as Void {
|
||||
var bmpW = bmp.getWidth();
|
||||
var scale = targetSize.toFloat() / bmpW.toFloat();
|
||||
dc.drawBitmap2(
|
||||
cx - targetSize / 2,
|
||||
cy - targetSize / 2,
|
||||
bmp,
|
||||
{ :scaleX => scale, :scaleY => scale }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue