Skip to content

Commit f5b7964

Browse files
sebgodclaude
andcommitted
Add site configuration UI to Equipment tab
When a mount is assigned, show site info (lat/lon/elevation) with clickable [>] button to edit. Clicking opens three text input fields (Lat, Lon, Elev) with Tab to cycle between them, Enter to save, Escape to cancel. "Set Site" button shown when no site is configured. Pre-fills existing values when editing. Saves directly to the mount URI query params via EquipmentActions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fc086a3 commit f5b7964

File tree

3 files changed

+158
-17
lines changed

3 files changed

+158
-17
lines changed

src/TianWen.UI.Gui/EquipmentTabState.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ public class EquipmentTabState
1313
public bool IsCreatingProfile { get; set; }
1414
public TextInputState ProfileNameInput { get; } = new() { Placeholder = "Enter profile name..." };
1515

16+
// Site configuration
17+
public bool IsEditingSite { get; set; }
18+
public TextInputState LatitudeInput { get; } = new() { Placeholder = "Latitude (e.g. 48.2)" };
19+
public TextInputState LongitudeInput { get; } = new() { Placeholder = "Longitude (e.g. 16.3)" };
20+
public TextInputState ElevationInput { get; } = new() { Placeholder = "Elevation m (e.g. 200)" };
21+
public TextInputState? ActiveTextInput { get; set; } // which field is currently focused
22+
1623
// Device discovery
1724
public IReadOnlyList<DeviceBase> DiscoveredDevices { get; set; } = [];
1825
public bool IsDiscovering { get; set; }

src/TianWen.UI.Gui/Program.cs

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ await PlannerActions.ComputeTonightsBestAsync(
178178
var textStr = Marshal.PtrToStringUTF8(evt.Text.Text);
179179
if (textStr is not null)
180180
{
181-
guiRenderer.EquipmentTab.State.ProfileNameInput.InsertText(textStr);
181+
var activeInput = guiRenderer.EquipmentTab.State.ActiveTextInput
182+
?? guiRenderer.EquipmentTab.State.ProfileNameInput;
183+
activeInput.InsertText(textStr);
182184
}
183185
break;
184186
}
@@ -239,8 +241,9 @@ await PlannerActions.ComputeTonightsBestAsync(
239241
void HandleKeyDown(Scancode scancode, Keymod keymod)
240242
{
241243
// Route to text input if active
242-
var eqInput = guiRenderer.EquipmentTab.State.ProfileNameInput;
243-
if (eqInput.IsActive)
244+
var eqState = guiRenderer.EquipmentTab.State;
245+
var eqInput = eqState.ActiveTextInput ?? (eqState.ProfileNameInput.IsActive ? eqState.ProfileNameInput : null);
246+
if (eqInput is { IsActive: true })
244247
{
245248
var inputKey = scancode switch
246249
{
@@ -255,18 +258,47 @@ void HandleKeyDown(Scancode scancode, Keymod keymod)
255258
_ => (TextInputKey?)null
256259
};
257260

261+
// Tab cycles between site fields
262+
if (scancode == Scancode.Tab && eqState.IsEditingSite)
263+
{
264+
eqState.ActiveTextInput = eqState.ActiveTextInput == eqState.LatitudeInput ? eqState.LongitudeInput
265+
: eqState.ActiveTextInput == eqState.LongitudeInput ? eqState.ElevationInput
266+
: eqState.LatitudeInput;
267+
appState.NeedsRedraw = true;
268+
return;
269+
}
270+
258271
if (inputKey.HasValue && eqInput.HandleKey(inputKey.Value))
259272
{
260-
if (eqInput.IsCommitted && eqInput.Text.Length > 0)
273+
if (eqInput.IsCommitted)
261274
{
262-
// Trigger profile creation
263-
HandleEquipmentClick(new EquipmentHitResult(EquipmentHitType.CreateButton));
275+
if (eqState.IsEditingSite)
276+
{
277+
// Enter in site field → save site
278+
HandleEquipmentClick(new EquipmentHitResult(EquipmentHitType.SaveSiteButton));
279+
}
280+
else if (eqInput.Text.Length > 0)
281+
{
282+
// Trigger profile creation
283+
HandleEquipmentClick(new EquipmentHitResult(EquipmentHitType.CreateButton));
284+
}
264285
}
265286
else if (eqInput.IsCancelled)
266287
{
288+
if (eqState.IsEditingSite)
289+
{
290+
eqState.IsEditingSite = false;
291+
eqState.LatitudeInput.Deactivate();
292+
eqState.LongitudeInput.Deactivate();
293+
eqState.ElevationInput.Deactivate();
294+
eqState.ActiveTextInput = null;
295+
}
296+
else
297+
{
298+
eqState.IsCreatingProfile = false;
299+
eqState.ProfileNameInput.Clear();
300+
}
267301
eqInput.Deactivate();
268-
eqInput.Clear();
269-
guiRenderer.EquipmentTab.State.IsCreatingProfile = false;
270302
StopTextInput(sdlWindow.Handle);
271303
}
272304
appState.NeedsRedraw = true;
@@ -430,9 +462,19 @@ void HandleEquipmentClick(EquipmentHitResult hit)
430462
break;
431463

432464
case EquipmentHitType.TextInput:
433-
if (!eqState.ProfileNameInput.IsActive)
465+
if (eqState.IsEditingSite)
466+
{
467+
// Cycle focus: if no active input, activate latitude
468+
if (eqState.ActiveTextInput is null)
469+
{
470+
eqState.ActiveTextInput = eqState.LatitudeInput;
471+
}
472+
StartTextInput(sdlWindow.Handle);
473+
}
474+
else if (!eqState.ProfileNameInput.IsActive)
434475
{
435476
eqState.ProfileNameInput.Activate();
477+
eqState.ActiveTextInput = eqState.ProfileNameInput;
436478
StartTextInput(sdlWindow.Handle);
437479
}
438480
break;
@@ -522,6 +564,57 @@ void HandleEquipmentClick(EquipmentHitResult hit)
522564
appState.NeedsRedraw = true;
523565
});
524566
break;
567+
568+
case EquipmentHitType.EditSiteButton:
569+
var eqSt = guiRenderer.EquipmentTab.State;
570+
eqSt.IsEditingSite = true;
571+
// Pre-fill with existing site values if available
572+
if (appState.ActiveProfile?.Data is { } pd2)
573+
{
574+
var existingSite = EquipmentActions.GetSiteFromMount(pd2.Mount);
575+
if (existingSite.HasValue)
576+
{
577+
eqSt.LatitudeInput.Activate(existingSite.Value.Lat.ToString(System.Globalization.CultureInfo.InvariantCulture));
578+
eqSt.LongitudeInput.Activate(existingSite.Value.Lon.ToString(System.Globalization.CultureInfo.InvariantCulture));
579+
eqSt.ElevationInput.Activate(existingSite.Value.Elev?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "");
580+
}
581+
else
582+
{
583+
eqSt.LatitudeInput.Activate();
584+
eqSt.LongitudeInput.Activate();
585+
eqSt.ElevationInput.Activate();
586+
}
587+
}
588+
StartTextInput(sdlWindow.Handle);
589+
break;
590+
591+
case EquipmentHitType.SaveSiteButton when appState.ActiveProfile is { } siteProfile:
592+
var eqSt2 = guiRenderer.EquipmentTab.State;
593+
if (double.TryParse(eqSt2.LatitudeInput.Text, System.Globalization.CultureInfo.InvariantCulture, out var sLat) &&
594+
double.TryParse(eqSt2.LongitudeInput.Text, System.Globalization.CultureInfo.InvariantCulture, out var sLon))
595+
{
596+
double? sElev = double.TryParse(eqSt2.ElevationInput.Text, System.Globalization.CultureInfo.InvariantCulture, out var e) ? e : null;
597+
var sData = siteProfile.Data ?? ProfileData.Empty;
598+
var newSiteData = EquipmentActions.SetSite(sData, sLat, sLon, sElev);
599+
var updatedSite = siteProfile.WithData(newSiteData);
600+
_ = Task.Run(async () =>
601+
{
602+
await updatedSite.SaveAsync(external, cts.Token);
603+
appState.ActiveProfile = updatedSite;
604+
eqSt2.IsEditingSite = false;
605+
eqSt2.LatitudeInput.Deactivate();
606+
eqSt2.LongitudeInput.Deactivate();
607+
eqSt2.ElevationInput.Deactivate();
608+
eqSt2.ActiveTextInput = null;
609+
StopTextInput(sdlWindow.Handle);
610+
appState.NeedsRedraw = true;
611+
});
612+
}
613+
else
614+
{
615+
appState.StatusMessage = "Invalid latitude or longitude";
616+
}
617+
break;
525618
}
526619

527620
appState.NeedsRedraw = true;

src/TianWen.UI.Gui/VkEquipmentTab.cs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ public enum EquipmentHitType
1818
CreateButton,
1919
AddOtaButton,
2020
DiscoverButton,
21-
TextInput
21+
TextInput,
22+
EditSiteButton,
23+
SaveSiteButton
2224
}
2325

2426
/// <summary>
@@ -289,22 +291,61 @@ private void RenderProfilePanel(
289291
"Mount", pd.Mount, new AssignTarget.ProfileLevel("Mount"),
290292
x, cursor, w, itemH, dpiScale, fontPath, fontSize, padding, arrowW);
291293

292-
// Site info (derived from mount URI, non-clickable)
294+
// Site info — clickable to edit
293295
var site = EquipmentActions.GetSiteFromMount(pd.Mount ?? NoneDevice.Instance.DeviceUri);
294-
if (site.HasValue)
296+
if (State.IsEditingSite)
297+
{
298+
// Show editable lat/lon/elevation fields
299+
var fieldH = (int)(itemH * 1.2f);
300+
var fieldW = (int)(w - padding * 2f);
301+
var fieldX = (int)(x + padding);
302+
303+
DrawText(" Lat:".AsSpan(), fontPath, x + padding, cursor, 50f * dpiScale, itemH, fontSize * 0.85f, DimText, TextAlign.Near, TextAlign.Center);
304+
TextInputRenderer.Render(_renderer, State.LatitudeInput, fieldX + (int)(50f * dpiScale), (int)cursor, fieldW - (int)(50f * dpiScale), fieldH, fontPath, fontSize * 0.9f, FrameCount);
305+
RegisterClickable(fieldX + 50f * dpiScale, cursor, fieldW - 50f * dpiScale, fieldH, new EquipmentHitResult(EquipmentHitType.TextInput));
306+
cursor += fieldH + 2;
307+
308+
DrawText(" Lon:".AsSpan(), fontPath, x + padding, cursor, 50f * dpiScale, itemH, fontSize * 0.85f, DimText, TextAlign.Near, TextAlign.Center);
309+
TextInputRenderer.Render(_renderer, State.LongitudeInput, fieldX + (int)(50f * dpiScale), (int)cursor, fieldW - (int)(50f * dpiScale), fieldH, fontPath, fontSize * 0.9f, FrameCount);
310+
RegisterClickable(fieldX + 50f * dpiScale, cursor, fieldW - 50f * dpiScale, fieldH, new EquipmentHitResult(EquipmentHitType.TextInput));
311+
cursor += fieldH + 2;
312+
313+
DrawText(" Elev:".AsSpan(), fontPath, x + padding, cursor, 50f * dpiScale, itemH, fontSize * 0.85f, DimText, TextAlign.Near, TextAlign.Center);
314+
TextInputRenderer.Render(_renderer, State.ElevationInput, fieldX + (int)(50f * dpiScale), (int)cursor, fieldW - (int)(50f * dpiScale), fieldH, fontPath, fontSize * 0.9f, FrameCount);
315+
RegisterClickable(fieldX + 50f * dpiScale, cursor, fieldW - 50f * dpiScale, fieldH, new EquipmentHitResult(EquipmentHitType.TextInput));
316+
cursor += fieldH + 2;
317+
318+
// Save button
319+
var saveBtnW = _renderer.MeasureText("Save Site".AsSpan(), fontPath, fontSize).Width + padding * 4f;
320+
FillRect(x + padding, cursor, saveBtnW, buttonH, CreateButton);
321+
RegisterClickable(x + padding, cursor, saveBtnW, buttonH, new EquipmentHitResult(EquipmentHitType.SaveSiteButton));
322+
DrawText("Save Site".AsSpan(), fontPath, x + padding, cursor, saveBtnW, buttonH, fontSize, BodyText, TextAlign.Center, TextAlign.Center);
323+
cursor += buttonH + padding;
324+
}
325+
else if (site.HasValue)
295326
{
296327
var (lat, lon, elev) = site.Value;
297328
var latStr = lat >= 0 ? $"{lat:F1}°N" : $"{-lat:F1}°S";
298329
var lonStr = lon >= 0 ? $"{lon:F1}°E" : $"{-lon:F1}°W";
299330
var elevStr = elev.HasValue ? $", {elev.Value:F0}m" : "";
300331
var siteStr = $" Site: {latStr}, {lonStr}{elevStr}";
301-
DrawText(
302-
siteStr.AsSpan(),
303-
fontPath,
304-
x + padding, cursor, w - padding * 2f, itemH,
305-
fontSize * 0.9f, SiteText, TextAlign.Near, TextAlign.Center);
332+
333+
var siteBtnW = w - padding * 2f;
334+
FillRect(x + padding, cursor, siteBtnW, itemH, SlotNormal);
335+
RegisterClickable(x + padding, cursor, siteBtnW, itemH, new EquipmentHitResult(EquipmentHitType.EditSiteButton));
336+
DrawText(siteStr.AsSpan(), fontPath, x + padding, cursor, siteBtnW - arrowW, itemH, fontSize * 0.9f, SiteText, TextAlign.Near, TextAlign.Center);
337+
DrawText("[>]".AsSpan(), fontPath, x + w - padding - arrowW, cursor, arrowW, itemH, fontSize * 0.85f, DimText, TextAlign.Center, TextAlign.Center);
306338
cursor += itemH;
307339
}
340+
else
341+
{
342+
// No site configured — show "Set Site" button
343+
var setSiteBtnW = _renderer.MeasureText("Set Site".AsSpan(), fontPath, fontSize).Width + padding * 4f;
344+
FillRect(x + padding, cursor, setSiteBtnW, buttonH, CreateButton);
345+
RegisterClickable(x + padding, cursor, setSiteBtnW, buttonH, new EquipmentHitResult(EquipmentHitType.EditSiteButton));
346+
DrawText("Set Site".AsSpan(), fontPath, x + padding, cursor, setSiteBtnW, buttonH, fontSize, BodyText, TextAlign.Center, TextAlign.Center);
347+
cursor += buttonH;
348+
}
308349

309350
cursor += padding / 2f;
310351

0 commit comments

Comments
 (0)