Skip to content

Commit a93c6a2

Browse files
sebgodclaude
andcommitted
Update docs: SignalBus architecture, filter wheel config, equipment tab
- Update GuiEventHandlers XML doc to reference SignalBus pattern - CLAUDE.md: rewrite widget system section with signal bus pattern, add Filter Wheel Configuration section documenting driver fallbacks, Filter type with DisplayName/CustomName, equipment tab editing flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 544831c commit a93c6a2

File tree

2 files changed

+66
-17
lines changed

2 files changed

+66
-17
lines changed

CLAUDE.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,39 +332,54 @@ RgbaImageRenderer).
332332
- `BackgroundTaskTracker` — tracks async operations, checks completions per frame, logs errors
333333
- `TextInputState` — single-line text input with `OnCommit` (async), `OnCancel`,
334334
`OnTextChanged`, `OnKeyOverride` callbacks
335+
- `SignalBus` — thread-safe typed deferred signal bus; widgets `PostSignal<T>()` during
336+
event handling, hosts `Subscribe<T>()` at startup, `ProcessPending()` delivers once per frame
337+
- `DropdownMenuState` — generic popup dropdown menu with keyboard navigation, custom entry
338+
support, and full-screen backdrop dismiss
339+
340+
**Signal bus pattern** (replaces callback properties):
341+
1. Widget calls `PostSignal(new SomeSignal(...))` during click/key handling — just enqueues
342+
2. `OnPostFrame` calls `bus.ProcessPending(tracker)` — delivers all pending signals
343+
3. Sync handlers run inline; async handlers submitted to `BackgroundTaskTracker`
344+
4. Signals are `readonly record struct` value types defined in `GuiSignals.cs`
345+
5. Built-in signals in DIR.Lib: `ActivateTextInputSignal`, `DeactivateTextInputSignal`,
346+
`RequestExitSignal`, `RequestRedrawSignal`
347+
6. App signals in Abstractions: `DiscoverDevicesSignal`, `AddOtaSignal`, `EditSiteSignal`,
348+
`CreateProfileSignal`, `AssignDeviceSignal`, `UpdateProfileSignal`, `BuildScheduleSignal`,
349+
`ToggleFullscreenSignal`, `PlateSolveSignal`
335350

336351
**Click handling pattern**:
337352
1. Widgets call `RegisterClickable(x, y, w, h, hitResult, onClick)` during render
338-
2. Self-contained actions (filter cycling, list selection) pass an `OnClick` delegate
353+
2. Self-contained actions (offset stepping, list selection) pass an `OnClick` delegate
339354
3. `HitTestAndDispatch(px, py)` invokes `OnClick` and returns the `HitResult`
340-
4. SDL-dependent actions (text input focus) are handled by `GuiEventHandlers`
355+
4. Cross-component actions use `PostSignal` (text input focus, profile save, discovery)
341356

342357
**Keyboard handling pattern**:
343358
1. `Program.cs` maps SDL scancodes to `InputKey` via `SdlInputMapping` (in `TianWen.UI.Shared`)
344359
2. `GuiEventHandlers.HandleKeyDown` routes to active text input first
345360
3. If not consumed, `Program.cs` routes to `ActiveTab.HandleKeyDown(inputKey, modifiers)`
346361
4. Each tab overrides `HandleKeyDown` for tab-specific shortcuts (pure state mutations)
347-
5. DI-dependent actions use `Action`/`Func<Task>` callbacks set by the host at wiring time
362+
5. Cross-component actions use `PostSignal` (delivered in `OnPostFrame`)
348363

349364
**Mouse wheel pattern**:
350365
- `GuiEventHandlers.HandleMouseWheel` delegates to `ActiveTab.HandleMouseWheel(scrollY, x, y)`
351366
- Each tab overrides to handle its own scroll zones (target list, config panel, etc.)
352367

353368
**Async operations**:
354369
- All background work goes through `BackgroundTaskTracker.Run(Func<Task>, description)`
355-
- `Program.cs` calls `tracker.ProcessCompletions(logger)` each frame — logs errors, triggers redraw
370+
- `Program.cs` calls `bus.ProcessPending(tracker)` then `tracker.ProcessCompletions(logger)`
371+
each frame — async signal handlers are submitted to tracker automatically
356372
- `TextInputState.OnCommit` is `Func<string, Task>?` — submitted to tracker on Enter
357-
- Tab callbacks (`OnDiscover`, `OnAddOta`, `OnAssignDevice`) are `Func<Task>` — submitted via tracker
358373
- Shutdown: `tracker.DrainAsync()` awaits all pending tasks
359374
- Zero fire-and-forget `_ = Task.Run(...)` in the codebase
360375

361376
**Inheritance hierarchy**:
362377
```
363-
PixelWidgetBase<TSurface> (Abstractions — renderer-agnostic)
378+
PixelWidgetBase<TSurface> (DIR.Lib — renderer-agnostic, has Bus + PostSignal)
364379
└─ VkTabBase (Gui — pins TSurface = VulkanContext)
365380
├─ VkGuiRenderer (sidebar, status bar, content dispatch)
366381
├─ VkPlannerTab (planner with autocomplete, scheduling viz)
367-
├─ VkEquipmentTab (profile/device management)
382+
├─ VkEquipmentTab (profile/device management, filter editing)
368383
└─ VkSessionTab (session config + observation list)
369384
└─ VkImageRenderer (Shared — FITS viewer toolbar, file list, histogram)
370385
```
@@ -376,10 +391,10 @@ then falls through to the active tab.
376391

377392
**Event handling** (`GuiEventHandlers`):
378393
- Generic event routing only — zero tab-specific logic in the routing methods
379-
- Constructor wires all callbacks (OnCommit, OnDiscover, etc.) with DI-dependent closures
380-
- SDL bridge: `StartTextInput`/`StopTextInput` for IME lifecycle
394+
- Constructor subscribes DI-dependent signal handlers on the `SignalBus`
395+
- Per-field `TextInputState` callbacks wired for planner search, site editing, profile creation
381396
- `Program.cs` is a thin event loop + composition root
382-
- Text input focus tracked via `GuiAppState.ActiveTextInput` (single source of truth)
397+
- Text input focus managed via `ActivateTextInputSignal` / `DeactivateTextInputSignal`
383398

384399
**SDL input mapping** (`TianWen.UI.Shared/SdlInputMapping.cs`):
385400
- C# 14 extension blocks on `Scancode` and `Keymod`
@@ -409,6 +424,42 @@ camera settings + observation list with frame estimates.
409424
- Default exposure from f-ratio: `5 × f²` seconds, clamped [10, 600]
410425
- Estimated frame count per target: `windowSeconds / (subExposure + 10s overhead)`
411426

427+
### Filter Wheel Configuration
428+
429+
**Design principle**: Seed on discovery, TianWen owns the data, sync back on connect.
430+
Filter names and focus offsets are stored as URI query params (`filter1`, `offset1`, ...)
431+
on the filter wheel device URI in the profile. All drivers read from URI params first,
432+
with driver-specific fallbacks per slot.
433+
434+
| Driver | Slot count source | Name/offset fallback |
435+
|--------|------------------|---------------------|
436+
| ASCOM | `Names.Length` (COM) | COM `Names[i]` / `FocusOffsets[i]` |
437+
| ZWO | `NumberOfSlots` (hardware) | `"Filter {N}"` / `0` (seeded at discovery) |
438+
| Alpaca | REST API `names` array | API values |
439+
| Fake | preset count (LRGB/Narrowband/Simple by device ID, max 8) | preset values |
440+
| Manual | always 1 | `InstalledFilter.Name` |
441+
442+
**`Filter` type** (`TianWen.Lib/Imaging/Filter.cs`):
443+
- `readonly record struct Filter(string Name, string ShortName, string DisplayName, Bandpass)`
444+
- `Name`: code name (`HydrogenAlpha`), `ShortName`: abbreviation (``), `DisplayName`: user-friendly (`H-Alpha`)
445+
- `FromName()` uses `[GeneratedRegex]` patterns, supports unicode α/β, handles dual-band filters
446+
- `InstalledFilter.CustomName` preserves unknown filter names (e.g. "Optolong L-Ultimate")
447+
448+
**Equipment tab filter editing** (`VkEquipmentTab`):
449+
- Filter table below filter wheel slot (expand/collapse via `[+]`/`[-]` header)
450+
- Filter name: click opens `DropdownMenuState` with `CommonFilterNames` + "Custom..." entry
451+
- Custom entry shows inline `TextInputState` for arbitrary names
452+
- Focus offset: `[-]` / `[+]` stepper buttons
453+
- All edits are in-memory (`EquipmentTabState.EditingFilters`); Save/Cancel buttons appear when dirty
454+
- Save commits via `UpdateProfileSignal` through the signal bus
455+
456+
**`EquipmentActions`** (`TianWen.UI.Abstractions/EquipmentActions.cs`):
457+
- `GetFilterConfig(ProfileData, otaIndex)` — reads filters from URI params
458+
- `SetFilterConfig(ProfileData, otaIndex, filters)` — writes filters to URI params
459+
- `UpdateOTA(ProfileData, otaIndex, ...)` — updates OTA name/focal length/aperture/optical design
460+
- `FilterDisplayName(InstalledFilter)` — returns `DisplayName` (custom name for unknowns)
461+
- `CommonFilterNames` — shared list used by dropdown and CLI
462+
412463
### Fake Camera Sensor Presets
413464

414465
`FakeCameraDriver` selects sensor specs by device ID (1-based, mod 9), alternating

src/TianWen.UI.Gui/GuiEventHandlers.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
using DIR.Lib;
2-
using System;
3-
using System.Linq;
4-
using System.Threading;
5-
using System.Threading.Tasks;
62
using Microsoft.Extensions.DependencyInjection;
73
using Microsoft.Extensions.Logging;
84
using TianWen.Lib.Devices;
@@ -14,9 +10,11 @@ namespace TianWen.UI.Gui
1410
{
1511
/// <summary>
1612
/// Centralized event handling for the GUI. Bridges SDL input events to the
17-
/// widget/tab system. Tab-specific logic lives in the tabs themselves via
18-
/// callbacks (<see cref="TextInputState.OnCommit"/>, <see cref="TextInputState.OnKeyOverride"/>,
19-
/// <see cref="IPixelWidget.HandleKeyDown"/>, <see cref="IPixelWidget.HandleMouseWheel"/>).
13+
/// widget/tab system. Action signals are subscribed on the <see cref="SignalBus"/>
14+
/// (via <see cref="PixelWidgetBase{TSurface}.PostSignal{T}"/>); per-field callbacks
15+
/// remain on <see cref="TextInputState"/> (<see cref="TextInputState.OnCommit"/>,
16+
/// <see cref="TextInputState.OnKeyOverride"/>). Tab-specific keyboard/mouse handling
17+
/// uses <see cref="IWidget.HandleKeyDown"/> and <see cref="IWidget.HandleMouseWheel"/>.
2018
/// </summary>
2119
public sealed class GuiEventHandlers
2220
{

0 commit comments

Comments
 (0)