Skip to content

Commit d42f80b

Browse files
Zangesclaude
andauthored
Plan A: Windows app cutover from WPF to Avalonia (#19)
* Plan A Phase 3 WIP: Avalonia foundation (build broken, foundation only) First chunk of the WPF -> Avalonia main-UI cutover. Lands the toolkit-neutral foundation so subsequent sessions can iterate per-window without re-litigating settled decisions. See ai-docs/plans/windows-avalonia/PHASE3_WIP_STATUS.md for the full status, decisions, and remaining work. Locked in this session: - Avalonia 11.2.3 + Fluent theme (system variant) + Inter font. - In-place retarget of Mouse2Joy.App (no new host project). - Fluent Tooltip.Build().Typical(...).Description(...).Advice(...) API + {tt:Tooltip ...} markup extension. Visible rendering preserved. - Panic hotkey rehosted on a self-owned Win32 message-only window + dedicated message-pump thread in Platform.Windows (no HwndSource). - UiThread helper in Mouse2Joy.UI wrapping Dispatcher.UIThread. No new IDispatcher port in Platform.Abstractions. - Toolkit-agnostic Interop (WindowStyles takes raw HWND, MonitorInfo uses a PixelRect struct, no System.Windows dependency). Build is intentionally red on this branch: the WPF views/controls were deleted to make room for the Avalonia rewrites in chunks A-F. The first follow-up session ("chunk A") brings the build green by porting the custom controls + overlay widgets + stubbing the 5 windows so the AXAML re-author can land per-window from there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3 Chunk A: green build with Avalonia stubs Restores `dotnet build Mouse2Joy.sln` (0 warnings, 0 errors) and `dotnet test Mouse2Joy.sln` (348/348 passing) on the Avalonia branch. UI is still placeholder text in each window; chunks B–F land the real AXAML. Ports: - Custom controls: KeyCaptureBox, CurveEditorCanvas, ChainPreviewControl to Avalonia primitives (StyledProperty, Pointer events, FormattedText new ctor, AffectsRender, StreamGeometryContext.LineTo/EndFigure). - PlaceholderText deleted — Avalonia's built-in TextBox.Watermark covers the case; AXAML call sites use it directly during chunks B–D. - 8 overlay widgets to Avalonia Control + Render(DrawingContext): Background, Button, ButtonGrid, Axis, TwoAxis, MouseActivity, EngineStatusIndicator, Status. Status preserves the 644-line per-glyph layout; switched WPF RotateTransform(angle, cx, cy) to composed Matrix.CreateTranslation*CreateRotation*CreateTranslation and fixed non-mutating Rect.Union by reassigning each step. - OverlayWidget base: IBrush/IPen, Color.TryParse, DrawRectangle(..., radiusX, radiusY) for rounded rects. - OverlayCoordinator + OverlayWidgetHost on Avalonia Canvas; dispatcher marshaling via UiThread; window placement delegated to OverlayWindow.ApplyMonitor(MonitorInfo) so the PixelPoint-vs-DIP split lives in one place. - MonitorInfo: dropped our PixelRect struct in favour of Avalonia.PixelRect (same shape, avoids name collision in view code); re-added BoundsDip as a tuple for DIP-space sizing. Wider WPF removal: - WindowsGlobalHotkey rewritten with the same self-owned Win32 message- only window pattern as PanicHotkey. Cross-thread RegisterHotKey is ferried to the pump thread via PostMessageW(WM_APP_REGISTER) because the call is tied to the registering thread's queue. The whole Mouse2Joy.Platform.Windows assembly is now WPF-free. - Platform.Windows.csproj drops <UseWPF> and the Hardcodet WPF tray package; tray is now Avalonia in Mouse2Joy.App. - Mouse2Joy.UI.Tests drops <UseWPF>. - ModifierParamProxies.OpenEditor uses IClassicDesktopStyleApplicationLifetime.Windows to find the owner and calls ShowDialog(owner) instead of WPF's Window.Owner. Packages: - Added Microsoft.Win32.SystemEvents 8.0.0 (used to come in via WPF; OverlayCoordinator hooks DisplaySettingsChanged for monitor hotplug). Stub windows authored so the composition root compiles: - MainWindow, BindingEditorWindow, WidgetEditorWindow, CurveEditorWindow (single TextBlock each), and OverlayWindow with full click-through wiring via WindowStyles.MakeOverlay on Opened + 60 Hz DispatcherTimer sampling InputEngine.Current. Real layouts land in chunks B–F. PHASE3_WIP_STATUS.md updated with the chunk-A completion record and the remaining chunk-B-through-F roadmap. Verification: dotnet build succeeds (0/0); dotnet test passes 348/348 across Contracts (61), Platform.Abstractions (5), UI (9), Persistence (73), Engine (200). The app has not been launched yet — there is no real UI to validate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3: lock in 9 carry-over + Chunk-A-surfaced decisions Replaces the "Open questions for next session" list with a "Decisions locked in" block so chunks B-F don't re-litigate settled choices: - Compiled bindings everywhere (no `{Binding}` fallback; refactor or use explicit `{CompiledBinding}` with `x:DataType` for dynamic DataContexts like BindingEditorViewModel.SelectedProxy). - Theme-aware brushes (`DynamicResource SystemControlForegroundBaseMediumLowBrush` or closest equivalent) for dim/hint text; drop hard-coded DimGray. - DPI manifest stays PerMonitorV2 (confirmed, no action). - Tray icon: ship a minimal .ico embedded as a resource (asset work in chunk B). - Overlay tick rate stays 60 Hz DispatcherTimer per S2 spike; documented semantic change from WPF v1's InputEngine.Tick subscription. - OverlayWindow parameterless ctor gates on Design.IsDesignMode so the Avalonia previewer doesn't run EnumDisplayMonitors against the host. - Trust SystemEvents.DisplaySettingsChanged for display hotplug; smoke-test during chunk D, fall back to WM_DISPLAYCHANGE subclass per OverlayWindow only if it doesn't fire under Avalonia. - BindingEditorWindow ctors stay () and (Binding?); window owns its VM internally. - StatusWidget per-glyph rotation correctness validated by chunk E manual walkthrough; regression test added only if it visibly breaks. - Application.ShutdownMode = OnExplicitShutdown confirmed; closing the main window minimizes-to-tray. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3 Chunk B: MainWindow + tray icon Re-author MainWindow.axaml as the real 5-tab UI (Profiles, Hotkeys, Overlay, Settings, Setup), replacing the Chunk A green-build stub. Compiled bindings, theme-aware brushes, and fluent tooltips throughout. Avalonia idiom adaptations in the code-behind: - MouseLeftButtonUp -> PointerReleased with MouseButton.Left guard - CheckBox.Click -> IsCheckedChanged, with load-time suppression flags so settings don't get re-saved on window-open - ListView -> ListBox; per-item style via <ListBox.Styles> - WPF DataTrigger italic+dim cue -> Avalonia class selector (Classes.auto-label="{Binding IsAutoLabel}") - ShowDialog awaited; dialog-opening handlers are async void - MessageBox.Show -> WindowsMessageBox.Warn / AskYesNoCancel - Application.Current.Shutdown() -> IClassicDesktopStyleApplicationLifetime.Shutdown() Moved WindowsMessageBox from Mouse2Joy.App to Mouse2Joy.Platform.Windows so UI code-behind can call it without UI depending on App. Added AskYesNoCancel for the destructive remove-widget-with-children prompt. Stub editor windows updated for the new dialog flow: - BindingEditorWindow.Result (Binding?) -- left null in Chunk B - WidgetEditorWindow.Result (WidgetConfig?) plus the (existing, siblings, monitors) ctor matching the WPF call shape Branded 32x32 joypad-glyph Mouse2Joy.ico generated and wired: - Packaged as <AvaloniaResource> in Mouse2Joy.App.csproj - Tray icon loads via AssetLoader.Open("avares://Mouse2Joy/Assets/...") - MainWindow consumes via Icon="avares://..." in AXAML - Set as the exe's ApplicationIcon Verification: dotnet build green (0/0); dotnet test 348/348 passing. The app still has not been launched -- overlay window and the binding / widget editors are stubs; full manual walkthrough is Chunk E. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3 Chunk C: BindingEditor + CurveEditor Re-author the modifier-chain binding editor and the curve-editor popout as full Avalonia windows. Build remains green; 348/348 tests still pass. BindingEditorWindow - 6-section layout matching the WPF original: Label, Source/Target + Suppress, modifier chain (list + selected-card param pane), preview + auto-insert notice, validation banner, OK/Cancel. - All 20 modifier-proxy DataTemplates inlined in Window.DataTemplates. ResourceDictionary cannot host unkeyed DataTemplate children (AVLN3000); a side ResourceInclude is not an option in Avalonia. - Compiled bindings on every binding; x:DataType per template. SelectedValueBinding on the 4 enum combos uses ReflectionBinding for the item-level Tag binding (Tag lives on ComboBoxItem, not the proxy). - Modifier-card selected affordance via class selector (Classes.selected="{Binding Selected}") instead of WPF DataTrigger. - ListBoxItem chrome scoped via Style Selector="ListBox.chain-list ListBoxItem" so the system selection highlight doesn't fight the card's dark background. - KeyCaptureBox commit subscribes to CapturedKeyProperty observable so the VM sees a fresh KeySource the moment the user presses a key. - Auto-label preview surfaced via the TextBox's built-in Watermark property, mirrored from the VM's AutoLabel on Source/Target changes. - Unbound KeySource guard surfaces through WindowsMessageBox.Warn (same Win32 helper used elsewhere); validation banner uses InvBoolConverter. CurveEditorWindow popout - 4-row layout (canvas / Symmetric + Points / hint / Close) hosting CurveEditorCanvas from Chunk A. - CurveEditorWindowViewModel promoted to public so AXAML compiled bindings can resolve x:DataType against it. Linear resampling on PointCount change preserved from the WPF original. MainWindow needed no edits; its add/edit/duplicate flows already check dlg.Result, and the editor now populates it on OK. Verification - dotnet build Mouse2Joy.sln — 0 warnings, 0 errors. - dotnet test Mouse2Joy.sln — 348/348 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3 Chunk D: WidgetEditorWindow Port the WPF widget Add/Edit dialog to Avalonia with the 17-row scaffold preserved (Type / Label / Visible / Position / Monitor / Parent / Anchor + Self anchor / Offset X+reset / Offset Y+reset / Size / Width / Lock+ Swap / Height / Font (Status-only) / Options / Cancel-Save) and the dynamic Options + Font panels rebuilt against Avalonia controls. - Use the built-in Avalonia NumericUpDown (decimal-based) in place of the WPF custom one; convert at the staging-record boundary. - Lock-aspect + B/I/U "checked" tint via a scoped Style Selector="ToggleButton.toggle-highlight:checked /template/ ContentPresenter" replacing WPF's triggered-style override. - ToolTip.SetTip for dynamic tooltip swaps (lock chip, monitor combo). - TextBox.Watermark-based auto-label preview, matching the BindingEditor pattern from Chunk C. - ItemsControl ItemsSource re-poke idiom for the dynamic Options + Font panels (Avalonia doesn't observe in-place List mutations). - FontManager.Current.SystemFonts replaces WPF SystemFontFamilies. - Color.TryParse replaces ColorConverter.ConvertFromString; brushes no longer Frozen. - (IBrush?) cast on Brushes.Transparent for well-typed ?? coalesce. OverlayWindow needed no config-mode chrome — it was permanently click- through in WPF too — so the Chunk A stub is already the production shape. PHASE3_WIP_STATUS.md updated with the Session 3 Chunk D log and the remaining-work list reduced to Chunks E + F. Verification: - dotnet build Mouse2Joy.sln: 0 warnings, 0 errors. - dotnet test Mouse2Joy.sln: 348/348 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3 Chunk E: tooltip call-site sweep Reviewed every tooltip across the 5 windows against the WPF originals and the TOOLTIP_AUTO_WRAP.md convention. Chunks B-D preserved tooltip shape 1:1 with WPF, so the sweep is a minimal-delta outcome: - WidgetEditor Label TextBox: upgraded the auto-label hint from a plain string to {tt:Tooltip Typical='Leave blank to auto-label by Type (and #N when there are siblings)', Description='...'} so the Typical line surfaces the "what happens if I leave this empty" answer where it's most useful (parallels BindingEditor's LabelTooltip resource). - Added the tt: xmlns to WidgetEditorWindow root for the markup extension. Remaining ~13 plain-string tooltips correctly stay plain (single-thought content with no Typical/Advice line) and route through the app-wide ToolTip MaxWidth=320 auto-wrap style. DimGray sweep clean: 0 matches across src/Mouse2Joy.UI (Chunk-A theme-aware-brushes decision fully landed). PHASE3_WIP_STATUS.md updated with the Chunk E session block and a full user-driven manual walkthrough checklist (per-window, cross-cutting, regression scan) covering the Phase 3 exit gate that requires elevated shell + hardware. Verification: - dotnet build Mouse2Joy.sln: 0 warnings, 0 errors - dotnet test Mouse2Joy.sln: 348/348 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Plan A Phase 3+4 Chunk F: implementation write-down Adds the Plan A summary write-down per repo convention. Captures the WPF → Avalonia cutover as a single Context / What-changed / Key-decisions / Files-touched / Follow-ups record so future work doesn't have to reconstruct the rationale from the per-chunk PHASE3_WIP_STATUS diary. Phase 4 WPF-removal confirmation: - grep across src/ + tests/ for PresentationCore, PresentationFramework, WindowsBase, System.Xaml, System.Windows.{Controls,Media,Shapes,...}, <UseWPF>, Hardcodet.NotifyIcon.Wpf returns 0 hits. - System.Windows.Input.ICommand in ModifierParamProxies is the BCL type (System.ObjectModel.dll), not WPF. Manual UI walkthrough was run by the user on plan-a-avalonia-wip on 2026-05-21 and PASSed (5-window + cross-cutting + regression checklist from PLAN.md). Recorded in the write-down. Verification: - dotnet build Mouse2Joy.sln: 0 warnings, 0 errors - dotnet test Mouse2Joy.sln: 348/348 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix dotnet format violation in MainWindow.axaml.cs CI's `dotnet format Mouse2Joy.sln --verify-no-changes` rejected an aligned-column layout in the AnchorPointOnRect switch expression (arrows + arguments padded with extra spaces to line up across the 9 cases). The repo's analyzer rules don't allow trailing whitespace inside expression arms, so the alignment had to go. Behavior unchanged; formatter output verified clean (exit 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address PR review feedback (PR #19) Fixes the 6 inline review comments from Copilot: 1. KeyCaptureBox: Extend static MapScancode table to cover NumPad 0-9 + decimal/multiply/divide/add/subtract, US OEM punctuation (~ - = [ ] \ ; ' , . / + backslash), and the lock/special keys (CapsLock, NumLock, Scroll, PrintScreen, Pause). For anything still uncovered (browser/media/launcher keys, locale-specific OEM), add a Win32 MapVirtualKeyW(vk, MAPVK_VK_TO_VSC_EX) fallback so the captured scancode is correct instead of silently dropping to PhysicalKey.None. 2. UiThread.Invoke: rewrite the doc to accurately describe post-on-off- thread semantics ("queued asynchronously when off the UI thread") and add a new InvokeAsync(Action) -> Task for callers that actually need wait-for-completion across threads. 3. WindowsGlobalHotkey.Register: bound the pending.Done.Wait on a 5 s timeout. On timeout, remove the pending entry under the lock and throw TimeoutException so a wedged pump thread cannot deadlock the caller (e.g. UI thread during startup). 4. WindowsGlobalHotkey.Dispose: replace PostMessageW(_hwnd, WM_QUIT, ..) (WM_QUIT is a thread-queue message, not dispatched to a WndProc) with a custom WM_APP_QUIT that the WndProc handles by calling DestroyWindow -> WM_DESTROY -> PostQuitMessage. Guarantees the pump loop exits via the documented Win32 shutdown idiom. Dropped the now- unused WM_QUIT constant. 5. PanicHotkey.Dispose: same WM_APP_QUIT fix as WindowsGlobalHotkey. 6. BindingEditorWindow: store the IDisposable returned by Subscribe on KeyCaptureBox.CapturedKeyProperty and dispose it (plus unhook PropertyChanged) on the window's Closed event so the dialog can be GC'd cleanly after close. Verification: - dotnet build Mouse2Joy.sln -c Release: 0 warnings, 0 errors - dotnet test Mouse2Joy.sln: 348/348 pass - dotnet format Mouse2Joy.sln --verify-no-changes: clean (exit 0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * BindingEditor: refresh ChainPreview on modifier collection + card changes Copilot review on PR #19 (comment 3277855467) flagged that the chain preview goes stale during edit because BindingEditorViewModel never raises PropertyChanged(nameof(Modifiers)) for add / remove / reorder / per-card edits — the VM uses a stable ObservableCollection reference, with per-card mutations flowing through ModifierCardViewModel's ModifierChanged event. Hook _vm.Modifiers.CollectionChanged + each card's ModifierChanged, refresh the preview from both, keep per-card hooks in sync on collection mutations, and unhook everything on the window's Closed event. Also dropped the dead nameof(Modifiers) arm from OnVmPropertyChanged with a comment explaining the new routing. Verification: - dotnet build Mouse2Joy.sln -c Release: 0 warnings, 0 errors - dotnet test Mouse2Joy.sln: 348/348 pass - dotnet format Mouse2Joy.sln --verify-no-changes: clean (exit 0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address PR #19 review findings: critical + perf + stability + UX Critical: - OverlayCoordinator: guard DisplaySettingsChanged dispatch on _disposed so a late-arriving display event after Dispose doesn't resurrect closed overlay windows (which would leak their 60Hz timers for process lifetime). - AvaloniaTrayIcon: rebind WindowNotificationManager when the MainWindow TopLevel changes (close-to-tray destroys the prior window; the cached notifier was targeting a dead TopLevel and silently dropping toasts). - WindowsGlobalHotkey: hold _gate across WM_APP_REGISTER handler so the Register() timeout-cleanup path can't dispose the ManualResetEventSlim between TryGetValue and Set (which would throw ObjectDisposedException out of DispatchMessageW and kill the pump). Also handle the race-won case where the pump completes after Wait() times out. - BindingEditor: add HeaderedContentControl.groupbox style emulating the WPF GroupBox chrome that FluentTheme strips by default. Perf (60Hz overlay hot path): - OverlayWidget: cache parsed brushes/pens per Config instance (was allocating a fresh SolidColorBrush every Read on every frame). - ButtonGridWidget: hoist Typeface to static field; cache 15 FormattedText instances keyed on font size. - StatusWidget: memoize RenderPlan against a key of every option + resolved snapshot text; drive size from MeasureOverride/InvalidateMeasure instead of mutating Width/Height inside Render (which inverted the Avalonia layout flow). - MouseActivityWidget: cache the arrow Pen keyed on (accent, thickness). - EngineStatusIndicatorWidget: hoist fallback brushes to static fields. - OverlayWindow: pause the 60Hz tick timer when IsVisible=false (was walking every widget + InvalidateVisual while Hide()d). Stability: - PanicHotkey: add ReadyTimeout guard on Register, double-dispose guard, separate _registered flag (so UnregisterHotKey runs even if a later exception flipped _registerOk back), snapshot HWND into the pump-thread local for finally cleanup, log every swallowed Win32/pump exception. - WindowsGlobalHotkey: snapshot HWND for finally; log pump exceptions instead of silently swallowing. - App.Teardown: log every per-step swallowed exception (shutdown is when you most want diagnostics; Log.CloseAndFlush still flushes after). - OverlayWindow: only start tick timer from OnOpened (not AttachEngine); re-pin overlay styles in ApplyMonitor (OS can reset layered/topmost state after DPI/resolution change). - WindowStyles: route SetWindowLongPtr / SetWindowPos failures through Serilog instead of Debug.WriteLine. Correctness: - KeyCaptureBox: drop the Pause => (0x45, false) arm (collided with NumLock's HID slot, silently misbinding to NumLock). Let the MapVirtualKey fallback surface unsupported instead. - CurveEditorCanvas.PixelToCurve: early-return on zero size to prevent NaN from a pre-layout pointer event propagating into persisted curve points. - Linear curve resamplers (ModifierParamProxies, CurveEditorWindow): guard against two control points sharing an X (divide-by-zero produced NaN that propagated through Fritsch-Carlson math). UX: - MainWindow: rename "Activate (SoftMuted)" to "Activate (soft mute)" (drop internal enum jargon). - MainWindow: re-add UpdateSourceTrigger=PropertyChanged on Profile name + Tick rate Hz TextBoxes so dependent UI updates live. - MainWindow: drop redundant "(Start with Windows is not yet wired up)" helper line — Avalonia shows tooltips on disabled controls. - BindingEditor: switch all 32 modifier-param TextBoxes to UpdateSourceTrigger=PropertyChanged so typing drives the sibling slider and chain preview live. - BindingEditor: replace 4 `{ReflectionBinding Tag}` lookups with `{Binding Tag, DataType=ComboBoxItem}` (compiled). - CurveEditorWindow: add Mode=TwoWay to the point-count TextBox binding. Docs: - PLAN_A_WINDOWS_AVALONIA.md: reconcile the tooltip-dim claim with the actual code (Opacity=0.7 on the TextBlock + SystemControlForeground BaseMediumLowBrush on chrome — explain when each is used). - TOOLTIP_AUTO_WRAP.md: add a Plan A update section noting the WPF mechanics described above are gone and the user-facing rendering contract is preserved; new authoring is the fluent builder + {tt:Tooltip} markup extension. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7af37dc commit d42f80b

62 files changed

Lines changed: 5414 additions & 3499 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Packages.props

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<ItemGroup>
33
<PackageVersion Include="Nefarius.ViGEm.Client" Version="1.21.256" />
44
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.3.2" />
5-
<PackageVersion Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
65
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
76
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
87
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
@@ -11,6 +10,19 @@
1110
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
1211
<PackageVersion Include="System.Reactive" Version="6.0.1" />
1312

13+
<!-- Microsoft.Win32.SystemEvents was implicitly pulled in by WPF; the
14+
overlay coordinator hooks DisplaySettingsChanged for monitor
15+
hotplug. With WPF gone, declare the package explicitly. -->
16+
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="8.0.0" />
17+
18+
<!-- Avalonia (Plan A: WPF -> Avalonia cutover). Track the latest 11.x stable. -->
19+
<PackageVersion Include="Avalonia" Version="11.2.3" />
20+
<PackageVersion Include="Avalonia.Desktop" Version="11.2.3" />
21+
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.2.3" />
22+
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.2.3" />
23+
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.2.3" />
24+
<PackageVersion Include="Avalonia.Diagnostics" Version="11.2.3" />
25+
1426
<!-- Test packages -->
1527
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
1628
<PackageVersion Include="xunit" Version="2.9.2" />

ai-docs/implementations/PLAN_A_WINDOWS_AVALONIA.md

Lines changed: 336 additions & 0 deletions
Large diffs are not rendered by default.

ai-docs/implementations/TOOLTIP_AUTO_WRAP.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,16 @@ Deliberately unchanged:
5353

5454
- If a future tooltip needs to be unbounded (e.g. a tooltip displaying a code snippet that should not wrap), set `MaxWidth="{x:Static sys:Double.PositiveInfinity}"` on that specific tooltip via local style or attribute. Hasn't come up.
5555
- If the `Description` itself needs internal paragraph breaks for very long tooltips, the cleanest extension is to either split into multiple `Description` paragraphs (e.g., a `Description1` / `Description2` field pair, or a `Description` array) or treat `\n\n` inside `Description` as a paragraph break. Defer until a tooltip actually needs it.
56-
- If the app switches to a custom WPF theme that overrides `ToolTip`, this style and templates would need to merge into that theme's resource dictionary rather than living in `App.xaml`.
56+
57+
## Plan A update (Avalonia cutover)
58+
59+
The WPF mechanics described above are gone — see [PLAN_A_WINDOWS_AVALONIA.md](PLAN_A_WINDOWS_AVALONIA.md) for the full migration write-down. The user-facing rendering contract is preserved (Typical italic+dim → Description → Advice, 320 px auto-wrap on plain strings); only the authoring surface changed.
60+
61+
What replaced what:
62+
63+
- **Authoring** — call sites no longer construct `TooltipContent` directly in code-behind. AXAML uses the `{tt:Tooltip Typical='…', Description='…', Advice='…'}` markup extension (positional + named); C# uses a fluent builder `Tooltip.Build().WithTypical(...).WithDescription(...).WithAdvice(...).Done()`. Both produce the same internal value carrier that the implicit ToolTip style binds to.
64+
- **`TooltipTemplateSelector.cs`** — removed. Avalonia resolves tooltip content via standard `DataTemplate` matching on the carrier's runtime type, so a selector class isn't needed.
65+
- **`App.xaml``App.axaml`** — the typed `DataTemplate` for the carrier and the implicit `Style Selector="ToolTip"` (320 px max width, content template) live here now. Typical line uses `Opacity="0.7"` on the foreground `TextBlock` (visually equivalent to a `BaseMediumLowBrush` against the default foreground without a second resource lookup per render).
66+
- **`TooltipContent.cs`** — still present in `src/Mouse2Joy.UI/Tooltips/`, repurposed as the internal carrier the builder + markup extension produce.
67+
68+
Old file refs in the "Files touched" list above are historical — the WPF `.xaml` and `.xaml.cs` files were deleted in the Avalonia port. The equivalent live files are the `.axaml` siblings in the same directories.

ai-docs/plans/windows-avalonia/PHASE3_WIP_STATUS.md

Lines changed: 502 additions & 0 deletions
Large diffs are not rendered by default.

src/Mouse2Joy.App/App.axaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<Application xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:tt="clr-namespace:Mouse2Joy.UI.Tooltips;assembly=Mouse2Joy.UI"
4+
x:Class="Mouse2Joy.App.App"
5+
RequestedThemeVariant="Default">
6+
<Application.Styles>
7+
<FluentTheme />
8+
9+
<!--
10+
App-wide ToolTip style. Caps width at 320px and routes content through
11+
a DataTemplate that renders TooltipContent records as sectioned text
12+
(Typical italic + dim first, then Description, then Advice). Plain
13+
strings hit the implicit string template Avalonia falls back to and
14+
wrap inside the same width cap. See
15+
ai-docs/implementations/TOOLTIP_AUTO_WRAP.md (rendering preserved
16+
from WPF) and the fluent Tooltip.Build() API in
17+
src/Mouse2Joy.UI/Tooltips/Tooltip.cs.
18+
-->
19+
<Style Selector="ToolTip">
20+
<Setter Property="MaxWidth" Value="320" />
21+
</Style>
22+
</Application.Styles>
23+
24+
<Application.DataTemplates>
25+
<!--
26+
Typed template for structured tooltip content. Renders Typical first
27+
(italic, dim) so the at-a-glance answer is closest to the input,
28+
then Description, then Advice. Null sections are bound via
29+
IsVisible so they collapse.
30+
-->
31+
<DataTemplate DataType="tt:TooltipContent">
32+
<StackPanel MaxWidth="320">
33+
<TextBlock Text="{Binding TypicalDisplay}"
34+
FontStyle="Italic" Opacity="0.7"
35+
TextWrapping="Wrap"
36+
IsVisible="{Binding HasTypical}" />
37+
<TextBlock Text="{Binding Description}" TextWrapping="Wrap"
38+
Margin="0,4,0,0"
39+
IsVisible="{Binding HasDescription}" />
40+
<TextBlock Text="{Binding Advice}" TextWrapping="Wrap"
41+
Margin="0,8,0,0"
42+
IsVisible="{Binding HasAdvice}" />
43+
</StackPanel>
44+
</DataTemplate>
45+
</Application.DataTemplates>
46+
</Application>

0 commit comments

Comments
 (0)