Skip to content

[ColorPicker] Migrate from WPF to WinUI 3#49174

Draft
moooyo wants to merge 47 commits into
mainfrom
yuleng/m/cp/1
Draft

[ColorPicker] Migrate from WPF to WinUI 3#49174
moooyo wants to merge 47 commits into
mainfrom
yuleng/m/cp/1

Conversation

@moooyo

@moooyo moooyo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

Migrates the ColorPicker module from WPF to WinUI 3 (Windows App SDK). The whole module — the picking overlay, the color editor, settings, the zoom magnifier, telemetry, and the Win32 input hooks — is ported off System.Windows onto Microsoft.UI.Xaml, MEF is replaced by Microsoft.Extensions.DependencyInjection, and the legacy WPF sources are removed once each piece is ported.

Draft / WIP: opened early for visibility and incremental review. The checklist below is intentionally left unchecked until the migration is finalized and fully validated on real hardware/DPI configurations.

demo:

Video.Project.43.mp4

PR Checklist

Detailed Description of the Pull Request / Additional comments

High-level areas ported (see the commit history for the step-by-step breakdown):

  • App shell & DI — WinUI 3 entry point / App / MainWindow, a Microsoft.Extensions.DependencyInjection container replacing MEF, and a ViewLocator replacing the implicit WPF DataTemplate mapping.
  • Picking overlay & editor — a TransparentWindow-based overlay (cursor-following tooltip) plus ColorEditorView / ColorPickerControl / ColorFormatControl ported off WPF (ContextMenuMenuFlyout, Visual State Manager, native WinUI styles merged into App.xaml).
  • Zoom magnifier — a Win2D CanvasControl that redraws the captured region with a brightness-adaptive pixel grid + center highlight (replacing the WPF ShaderEffect), and a Composition-based resize animation between zoom levels.
  • Core & helpers — color math + value converters on Windows.UI.Color, MonitorResolutionHelper on Windows.Foundation.Rect, ThrottledActionInvoker on DispatcherQueueTimer, ClipboardHelper on the WinRT DataPackage, and the Win32 mouse/keyboard hooks off WPF input types.
  • Resources — a .resw scaffold + ResourceLoaderInstance replacing the old .resx designer.
  • Build & installerColorPickerUI flipped to WinUI 3, launched from WinUI3Apps\; installer satellite + signing paths updated; leftover WPF sources removed.

Most recent fix in this branch: the zoom magnifier no longer follows the cursor on every mouse-wheel step. It is now centered on the cursor once, when the zoom session first starts, and then stays put while the wheel keeps zooming the region it opened over — restoring the original WPF behavior. (A separate, still-open item: aligning the zoom animation step/feel with the WPF original — under investigation.)

Validation Steps Performed

  • ColorPickerUI.csproj builds clean (x64 Debug, exit code 0).
  • ColorPickerUI.UnitTests covers settings, helpers (throttling, native methods, color conversion, clipboard), and DI smoke / frozen-contract tests.
  • Manual (magnifier fix): open ColorPicker, scroll to raise the magnifier, then move the mouse and keep scrolling — the magnifier stays anchored over the region it opened on while zooming 1x → 8x, instead of jumping to the new cursor location.
  • Broader manual validation across multiple monitors / DPI scales is still pending (draft).

Yu Leng and others added 30 commits June 25, 2026 14:37
…urces

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…builds

Exclude Helpers\ColorConverterTest.cs, Helpers\ColorFormatConversionTest.cs,
and Helpers\ColorRepresentationHelperTest.cs from compilation via
<Compile Remove> in ColorPickerUI.UnitTests.csproj. These tests reference
ColorPicker.Helpers types quarantined in the UI project during the WPF→WinUI 3
migration (Task 1). Files remain on disk for re-enablement in sub-project B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…jection

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lection

- git rm Common/ViewModelBase.cs and Common/RelayCommand.cs (dead WPF MVVM helpers)
- Remove their now-obsolete <Compile Remove> lines from ColorPickerUI.csproj
- Add RangeObservableCollectionTests (1 test: AddRange raises single Reset)
- All 2 Foundation tests pass (ServiceProviderSmokeTests + new collection test)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… .resx designer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, docs, manifest casing)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lper) off System.Windows.Point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tem) off MEF + DI singleton

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… §12a carry-forward)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sub-project D's NativeMethodsHelperTests is the first unit test to consume
an internal ColorPicker type (internal NativeMethodsHelper). The production
InternalsVisibleTo named "UnitTest-ColorPickerUI" but the test project's
assembly is "ColorPickerUI.UnitTests" (no AssemblyName override), so the
IVT did not apply and that test would fail to compile once the test project
builds. Align the IVT to the actual test assembly name. Found by the
whole-branch review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ows.UI.Color

- Move the ~76-key string table from the dead Properties\Resources.resx to
  Strings\en-us\Resources.resw and delete the .resx; normalize the legacy
  "Lightest color" key (stray space) to "Lightest_color" for MRT.
- ColorRepresentationHelper: System.Windows.Media.Color -> Windows.UI.Color;
  collapse the 42-case color-name switch to ResourceLoaderInstance.GetString;
  make ReplaceName resolve the color name lazily (only when the format contains
  the name placeholder) so numeric formats skip the resource lookup and the
  pure-conversion unit tests don't require the MRT .pri in the test host.
- ColorHelper re-included as-is (no WPF deps).
- HSVColor / ColorFormatModel / SerializationHelper: System.Windows.Media.Color
  -> Windows.UI.Color.
- 5 value converters: IValueConverter(string language) signature +
  Microsoft.UI.Xaml namespaces.
- Re-include the ported files past the csproj folder quarantines; un-quarantine
  the three color unit tests (378 color-logic test cases pass; build is clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…licate x:Class

The quarantined root MainWindow.xaml/.cs (the WPF picking-overlay shell:
AllowsTransparency, SizeToContent, Topmost, WindowStyle=None, MEF [Import]
IMainViewModel, HwndSource window-handle registration) declared the same
x:Class "ColorPicker.MainWindow" as the live WinUI 3 ColorPickerXAML\MainWindow.xaml,
coexisting only because the build quarantined the root file. Delete it -- git
history preserves the old WPF shell, and the sub-project C overlay port builds a
fresh AppWindow-based window rather than porting the WPF Window line-by-line -- and
drop the now-obsolete csproj quarantine entries, permanently removing the
duplicate-partial-class hazard before overlay work begins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… off WPF

Re-include the window-independent input layer past the Mouse/Keyboard quarantines:
- MouseHook: drop System.Windows.Input (MouseWheelEventArgs/InputManager/MouseDevice);
  the wheel event now carries the raw signed delta (high word of MSLLHOOKSTRUCT.mouseData),
  letting consumers derive zoom direction from its sign.
- IMouseInfoProvider: System.Windows.Point -> Windows.Foundation.Point.
- CursorManager, GlobalKeyboardHook, GlobalKeyboardHookEventArgs: re-included verbatim
  (already WPF-free Win32 / registry code).

The stateful consumers (MouseInfoProvider, KeyboardMonitor) stay quarantined because they
depend on AppStateHandler, which in turn depends on the not-yet-rebuilt overlay window.
Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ainView

Build the picking-overlay window on the shared Common.UI.Controls TransparentWindow
(introduced by PR #48176) instead of hand-rolling AppWindow transparency:
- ColorPickerOverlayWindow : TransparentWindow hosts the tooltip and inherits the
  transparent TransparentTintBackdrop, frameless tool-window chrome (out of taskbar/
  Alt-Tab, no Win11 phantom border), and a no-activate Show/Hide that never steals focus.
- Port MainView (the tooltip content): clr-namespace -> using, DynamicResource ->
  ThemeResource, ApplicationBackgroundBrush -> LayerFillColorDefaultBrush, keep loose
  {Binding} (no MainViewModel compile dependency yet), declare the two visibility
  converters locally, and replace the WPF AutomationEvents.MenuOpened narrator hack with
  LiveRegionChanged (paired with the existing LiveSetting=Assertive).
- Add a non-inverted BoolToVisibilityConverter.
- Reference Common.UI.Controls and CommunityToolkit.WinUI.Animations (the XAML compiler
  needs the latter to resolve TransparentWindow's ShowAnimations/HideAnimations).
- Narrow the Views compile/page quarantine to just the still-WPF editor and zoom views.

Not yet wired into the App launch path (waits on the MainViewModel / AppStateHandler
port). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port and un-quarantine IMainViewModel / IZoomViewModel / IColorEditorViewModel:
- IMainViewModel: System.Windows.Media.Brush -> Microsoft.UI.Xaml.Media.Brush;
  RegisterWindowHandle(HwndSource) -> RegisterWindowHandle(IntPtr).
- IZoomViewModel: System.Windows.Media.Imaging.BitmapSource -> Microsoft.UI.Xaml.Media.ImageSource.
- IColorEditorViewModel: System.Windows.Media.Color -> Windows.UI.Color (ICommand kept --
  System.Windows.Input.ICommand is the BCL type WinUI and CommunityToolkit.Mvvm already use).

These unblock the view models that implement them. Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…geSource)

ZoomViewModel now derives from CommunityToolkit.Mvvm ObservableObject (the custom
ViewModelBase was removed earlier) and exposes ZoomArea as
Microsoft.UI.Xaml.Media.ImageSource instead of WPF BitmapSource; the MEF export is
dropped (DI registration lands with the overlay wiring). Re-included past the ViewModels
quarantine; ZoomWindow / ZoomWindowHelper stay quarantined pending the Win2D magnifier.
Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd + FileSavePicker)

- ObservableObject base (custom ViewModelBase removed); CommunityToolkit.Mvvm
  RelayCommand<object> / AsyncRelayCommand<object> replace the custom RelayCommand.
- System.Windows.Media.Color -> Windows.UI.Color throughout (history + selected color).
- Export now uses the WinRT FileSavePicker (async) instead of WPF SaveFileDialog; a new
  WindowHandle property lets the host editor window anchor the picker (desktop requirement).
- MEF export dropped (DI registration lands with the overlay/editor wiring).

Re-included past the ViewModels quarantine; the editor window/view/controls stay
quarantined pending the XAML port. Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nto App.xaml

Port the shared resource dictionary off WPF (approach A: lean on native WinUI styling)
and merge it into App.xaml so the editor controls can resolve their styles/converters:
- Drop CustomAccentButtonStyle (consumers will use the built-in AccentButtonStyle).
- SubtleButtonStyle / TitleBarCloseButtonStyle now derive from the native DefaultButtonStyle
  via BasedOn, inheriting proper Fluent visual states (the WPF red-on-hover close fill is
  dropped pending editor visual polish).
- ColorShadeButtonStyle keeps a custom template, rewritten with the VisualStateManager and
  the built-in Control.CornerRadius -- the WPF-era ControlHelper.CornerRadius attached
  property is unnecessary in WinUI, so ControlHelper is not ported.
- Declare the six value converters and IconFontFamily.

App launches cleanly (startup smoke test passes; the merged dictionary loads at runtime
without error). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… DI)

- DependencyProperties on Microsoft.UI.Xaml; Color -> Windows.UI.Color.
- Bootstrapper MEF lookup -> App.GetService<IThrottledActionInvoker>().
- Copy-indicator WPF DoubleAnimation/BeginAnimation -> a Storyboard (the Height leg opts into
  EnableDependentAnimation); narrator MenuOpened -> LiveRegionChanged.
- {x:Static p:Resources.*} automation names / tooltips and the RelativeSource FindAncestor
  help-text binding (no WinUI equivalent) move to code-behind via ResourceLoaderInstance.
- ReadonlyTextBoxStyle simplified to BasedOn DefaultTextBoxStyle (approach A: lean on native).
- Re-included past the Controls quarantine (HSVColor + ColorFormatControl are default-glob now;
  only ColorPickerControl stays quarantined). Build is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tyle + Flyout)

The largest editor control, ported with approach A (lean on native WinUI):
- Slider: drop the WPF Track/TickBar/RepeatButton custom template; use native WinUI Sliders
  whose track theme brushes are overridden to Transparent so the gradient Borders behind
  (HueGradientBorder + the Saturation/Value LinearGradientBrush stops) show through.
- Adjust-color Popup -> a Button.Flyout; Popup.IsOpen toggling -> Flyout open/Hide().
- WPF DoubleAnimation/ThicknessAnimation via BeginAnimation -> a Storyboard (Height as a
  dependent animation; WinUI has no ThicknessAnimation, so the margin is set directly).
- Slider.ValueChanged RoutedPropertyChangedEventArgs<double> -> RangeBaseValueChangedEventArgs;
  PreviewTextInput -> TextBox.BeforeTextChanging; GotKeyboardFocus -> GotFocus.
- Color -> Windows.UI.Color; brushes/GradientStop/Point -> Microsoft.UI.Xaml.Media /
  Windows.Foundation; the custom automation peer derives from FrameworkElementAutomationPeer
  (no UserControlAutomationPeer in WinUI) and is partial (CsWinRT ABI requirement).
- {x:Static p:Resources.*} automation names/tooltips resolved in code via ResourceLoaderInstance.
- Extract AppStateHandler.BlockEscapeKeyClosingColorPickerEditor into a small EditorState static
  so the control no longer depends on the window-coupled AppStateHandler.

All Controls are now off the quarantine. Build is clean. The slider/flyout interaction needs
desktop verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…out, VSM)

- ListView.ContextMenu / MenuItem -> ListView.ContextFlyout / MenuFlyout(SubItem); the WPF
  FindAncestor PlacementTarget.SelectedItems command parameter -> {Binding SelectedItems,
  ElementName=HistoryColors}.
- The ListViewItem ControlTemplate triggers (custom selection ellipse) -> VisualStateManager
  CommonStates / SelectionStates.
- VirtualizingStackPanel -> ItemsStackPanel; MouseWheel -> PointerWheelChanged with
  ScrollViewer.ChangeView; DynamicResource -> ThemeResource (the SystemColors accent brush ->
  AccentFillColorDefaultBrush).
- {x:Static p:Resources.*} labels / automation names / menu headers resolved in code-behind
  via ResourceLoaderInstance.

Re-included past the Views quarantine (only ZoomView remains). Build is clean; the editor
list/menu interaction needs desktop verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the mutually-dependent overlay/editor integration cluster off WPF + MEF and wire it into
the WinUI 3 startup path. The module now launches, resolves its DI graph, creates the overlay,
registers the named events, and stays alive (startup smoke test passes); the picking/editor
flow is ready for desktop verification.

- AppStateHandler: MEF -> DI; drives the picking overlay via the TransparentWindow-based
  ColorPickerOverlayWindow (App.Window) Show()/Hide() instead of Application.Current.MainWindow;
  hosts the editor via ColorEditorWindow; HwndSource -> IntPtr; reads EditorState for the
  Esc-block flag; cursor move via NativeMethodsHelper GetCursorPos/SetCursorPos.
- ColorEditorWindow: WPF WindowChrome/DWM/custom-title-bar -> native WinUI title bar + a fixed
  OverlappedPresenter; AppWindow.Closing cancels and ends the session (hides), not destroys.
- MouseInfoProvider: MEF -> DI; DispatcherTimer -> DispatcherQueueTimer; System.Windows.Point ->
  Windows.Foundation.Point; the wheel hook now carries a raw delta.
- KeyboardMonitor: MEF -> DI; System.Windows.Input.Key/KeyInterop -> Windows.System.VirtualKey.
- MainViewModel: MEF -> DI + ObservableObject; the three NativeEventWaiter loops drop the
  dispatcher arg (the primitive captures the DispatcherQueue itself); Application.Current.Shutdown
  -> cancel the exit token + Environment.Exit; Brush/Color -> Microsoft.UI.Xaml.Media /
  Windows.UI.Color; RegisterWindowHandle(IntPtr).
- ZoomWindowHelper is a no-op placeholder (the Win2D magnifier is 7e-2).
- App.OnLaunched builds ColorPickerOverlayWindow, resolves IMainViewModel, hosts it in MainView,
  and registers the HWND; AppServices registers the new singletons.

Build is clean and the app starts. Hotkey-show / pick / editor behavior needs desktop
verification; the zoom magnifier (7e-2) is not implemented yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Yu Leng (from Dev Box) and others added 17 commits June 26, 2026 16:47
MouseInfoProvider samples the pixel under the cursor in its constructor (resolved during
App.OnLaunched, before the overlay is shown). On a non-interactive / disconnected session the
GDI CopyFromScreen can throw "the handle is invalid", which previously took down the whole app
at startup. Swallow the failure in GetPixelColor and return black; the next sample succeeds
once a real desktop is present. Found via the startup smoke test during 7e-1 verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-2)

Replace the no-op ZoomWindowHelper placeholder and the quarantined WPF ZoomWindow/ZoomView +
GridShaderEffect with a Win2D implementation (方案1):
- ZoomView hosts a single Win2D CanvasControl that draws the GDI-captured region scaled with
  nearest-neighbor filtering and overlays the pixel grid + center highlight (replacing the WPF
  ShaderEffect; the brightness-adaptive color + radius reveal from GridShader.fx are simplified
  for now and can be refined, or escalated to 方案2, after desktop verification).
- ZoomWindow derives from the shared TransparentWindow (transparent, frameless, no-activate).
- ZoomWindowHelper captures a 50x50 region around the cursor, converts the GDI bitmap to a
  CanvasBitmap, sizes + centers the magnifier on the cursor (physical pixels), and shows it;
  CopyFromScreen failures degrade gracefully. WindowCaptureExclusionHelper re-included as-is.
- Add the Microsoft.Graphics.Win2D package; un-quarantine ZoomView/ZoomWindow. The WPF
  Behaviors\ and Shaders\ stay quarantined (intentionally dead; GridShader.fx kept as the spec).

This was the last quarantined feature. Build is clean and the app starts; the magnifier's
on-screen behavior needs desktop verification (and the dev box's flaky GDI screen capture means
it can only be fully verified on a local interactive desktop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove the per-language ColorPicker satellite component from Resources.wxs: WinUI 3 localizes
  via the .pri compiled from Strings\*\Resources.resw, so PowerToys.ColorPickerUI.resources.dll
  is no longer produced and referencing it broke the WiX harvest.
- ESRPSigning: point PowerToys.ColorPickerUI.dll/.exe at the WinUI3Apps\ output (the WinUI 3 app
  now builds there), mirroring AdvancedPaste; the native PowerToys.ColorPicker.dll stays at root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…igration

The WPF->WinUI 3 migration is structurally complete, so delete the source that has no
remaining consumer:

- ColorPickerXAML\MainWindow.xaml(.cs): unused empty shell; App hosts ColorPickerOverlayWindow.
- Behaviors\*.cs (7 files): replaced by TransparentWindow built-ins, services, the native
  ListView reorder, and Win2D - none were re-included in the build.
- Shaders\GridShaderEffect.cs, Global.cs, GridShader.cso: the WPF pixel-grid shader, replaced by
  the Win2D ZoomView. GridShader.fx is kept as the reference spec for the alternative magnifier.
- Helpers\ControlHelper.cs: a WPF (System.Windows) attached-property helper with no consumer; its
  CornerRadius attached property is unnecessary in WinUI (see Styles.xaml). It was already
  excluded from the build by the Helpers\ quarantine glob.

Drop the now-empty Behaviors\ / Shaders\ Compile-Remove globs and refresh the csproj comment.
Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (WinUI)

The migration showed the picking overlay as a default ~1920x1030 window: the WPF MainWindow's
SizeToContent + ChangeWindowPositionBehavior (cursor-follow) + re-asserted Topmost were never
ported (a stale 7d TODO shipped through 7e-1, and the behavior lived in the quarantined Behaviors\).

ColorPickerOverlayWindow now:
- sizes the window to the MainView tooltip (WinUI has no SizeToContent), measured only on show /
  content change and cached otherwise;
- follows the cursor via IMouseInfoProvider.MousePositionChanged, with the WPF behavior's
  monitor-edge dead-zone flip; it also positions on Show() since the event only fires on movement
  (a stationary-cursor show would otherwise leave the tooltip parked);
- stays HWND_TOPMOST so the tooltip sits above the (non-top-most) zoom magnifier, matching the WPF
  helper that re-asserted MainWindow.Topmost after showing the zoom;
- caches the DPI scale + monitor bounds per pick session and moves with a single SetWindowPos per
  cursor tick (no per-tick layout pass), keeping following light.

App.OnLaunched wires the shared IMouseInfoProvider singleton into the overlay.

Desktop-verified: the small tooltip follows the cursor, the color readout updates, pick-in-magnifier
works, and the card sits above the magnifier and returns after zoom-out. Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… HSV sliders, clean history)

Desktop-verification polish for the ported editor (Approach A - native WinUI chrome):

- ColorEditorWindow: set the title-bar/taskbar icon to the ColorPicker app icon (was the generic
  WinUI placeholder), and add a Mica SystemBackdrop so the window's uncovered header no longer
  renders as an opaque black bar against the content card.
- ColorPickerControl: grow the H/S/V gradient bars from 8px to 16px (CornerRadius 8) so the native
  Slider thumb sits on them naturally instead of dwarfing a thin strip.
- ColorEditorView: hide the color-history horizontal scrollbar - WinUI's persistent Fluent scroll
  indicator read as a stray gray bar under the swatches; the mouse wheel still scrolls the history
  via the existing PointerWheelChanged handler.

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run xstyler over the WinUI 3 ColorPicker XAML so it conforms to
src/Settings.XamlStyler, fixing the "Verify XAML formatting" CI step.
Formatting-only: add UTF-8 BOM, reorder x:FieldModifier, and split
multi-line comment delimiters. No semantic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WinUI3Apps asset-conflict audit (.pipelines/verifyPossibleAssetConflicts.ps1)
rejects any *.xbf in the shared WinUI3Apps output root. ColorEditorWindow,
ColorPickerOverlayWindow and ZoomWindow lived at the project root, so their
compiled .xbf landed directly in WinUI3Apps\ and failed the audit.

Move all three .xaml + .xaml.cs into the existing ColorPickerXAML\ subfolder
(alongside App.xaml), matching every other WinUI3 app (AdvancedPaste/PowerDisplay/
Hosts/MeasureTool). No csproj change (default SDK globs) and no code change
(x:Class/namespace are folder-independent); the .xbf now lands under
WinUI3Apps\ColorPickerXAML\.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…installer root

The "Verify all binaries are signed and versioned" stage failed on
BaseApplicationsFiles_File_PowerToys.ColorPickerUI.exe (Not Signed).

A SelfContained Exe->WinExe ProjectReference (ColorPickerUI.UnitTests ->
ColorPickerUI) makes MSBuild copy the WinExe apphost (.exe/.deps.json/
.runtimeconfig.json) into the ROOT output dir as a side effect. The installer's
BaseApplications harvest picks up that leaked root PowerToys.ColorPickerUI.exe,
which ESRP never signs (it only signs the WinUI3Apps copy). Because the signed
and unsigned copies differ in hash, the dedup step kept both and the MSI shipped
the unsigned root one.

Mirror the existing ImageResizer leak workaround: strip the ColorPickerUI
apphost trio from BaseApplications. The dedup loop then keeps the signed
WinUI3Apps copy (which the native module launches anyway). The native root
PowerToys.ColorPicker.dll is unaffected (the UI suffix disambiguates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two small WPF behaviors were dropped during the WinUI 3 migration and are restored here:

- ColorEditorWindow now calls CenterOnScreen() (ports WindowStartupLocation="CenterScreen"); it previously opened at the OS default position.

- The picking overlay fades in over 250ms (CubicEase) on Show(), porting the WPF AppearAnimationBehavior that was lost when the merged TransparentWindow refactor removed ShowAnimations. Window has no Opacity, so the hosted MainView content is animated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the WPF ResizeBehavior feel (grow easeOutSine / shrink easeInQuad, 200ms) that the WinUI 3 magnifier lost.

ALT-1 (the WPF structural model): the zoom window is now a constant max-size host (50*8+30 = 430px) re-centered on the cursor each step, and only the inner card animates its Microsoft.UI.Composition Scale (CenterPoint pinned to card center). A fixed window that always exceeds the centered card means the briefly-larger card during a shrink never clips, so there is no per-step window resize and no shrink-trim bookkeeping. Win2D still draws at the final factor; the compositor scales that crisp texture during the tween.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A thorough review of the WinUI 3 migration (vs origin/main) surfaced
several issues; this applies the fixes. Builds 0/0 on ColorPickerUI and
ColorPickerUI.UnitTests.

Correctness:
- Editor Export was broken: ColorEditorViewModel.WindowHandle was never
  assigned, so the FileSavePicker was initialized with IntPtr.Zero. Add
  WindowHandle to IColorEditorViewModel and set it from AppStateHandler
  once the editor window exists.
- Installer harvested ColorPicker assets from the pre-WinUI3 root path;
  repoint generateAllFileComponents.ps1 + ColorPicker.wxs to WinUI3Apps
  so colorPicker.cur / icon.ico ship in the package.
- Restore the system cursors on process exit (AppDomain.ProcessExit):
  the Environment.Exit paths bypassed MouseInfoProvider.DisposeHook,
  leaking the crosshair as the system cursor (parity with WPF OnExit).
- Overlay tooltip re-queries the cursor monitor's DPI per move instead
  of caching it once, fixing sizing/offset on mixed-DPI multi-monitor.
- Dispose the Win2D CanvasBitmap per zoom session (ZoomWindowHelper +
  ZoomView.ClearBitmap) to stop leaking a GPU surface on every pick.
- Re-add the ETWTrace diagnostic session dropped during the migration
  (matches the WinUI 3 sibling modules).

Cleanup:
- Collapse the ~100-line csproj Compile-Remove + re-include quarantine
  (it excluded nothing) back to the default SDK glob; DefineConstants
  now appends so inherited DEBUG/TRACE survive.
- Delete dead code: ViewLocator (+test), ZoomViewModel/IZoomViewModel
  (+DI registration + ZoomWindowHelper wiring), AppStateHandler.SetTopMost,
  NativeMethods.SetToolWindowStyle (+orphaned interop), and App's
  ExitToken / RunnerPid + stale comments. App.Dispose now actually runs
  (via ProcessExit), so its IDisposable is no longer dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
方案2 magnifier grid (replaces the flat 方案1 grid):
- Grid line color now adapts to each cell's source-pixel brightness (dark over
  light areas, light over dark) so it stays visible on very light and very dark
  regions; grid alpha fades toward the magnifier edge (the shader's radius
  reveal); the center cursor-pixel highlight picks dark/light from its own
  brightness so it shows on a white pixel too. Source pixels are cached once per
  capture (ZoomView) instead of read back from the GPU on every draw.

Dead-code cleanup (all pre-existing, dead before the migration):
- ColorRepresentationHelper: drop 15 never-called ColorTo* methods (keep ColorToHex).
- Delete unused ColorToBrushConverter / ColorToStringConverter (+ their Styles.xaml
  registrations) and the duplicate converter keys in MainView.xaml.
- Remove ColorPickerControl.GetChildOfType<T>, HSVColor.GetSpectrum,
  ColorEditorViewModel.HideColorFormatCommand, the orphaned TitleBarCloseButtonStyle,
  the unused MainBorder x:Name, a stale using in SerializationHelper, and a duplicate
  IOException catch in UserSettings.
- Delete the orphaned Shaders/GridShader.fx (方案2 supersedes it as the reference).

Polish:
- Overlay GetMonitorBounds: when no monitors are enumerated, log a warning and fall
  back to the primary screen bounds instead of an unbounded rect, so edge-flipping
  still works.
- Overlay Show(): present the card transparent, then start the fade-in + initial
  positioning at Low priority after base.Show() so the 250ms fade begins when the
  window is actually on screen (no early/partial fade) and without a flash.

Builds 0/0 on ColorPickerUI + UnitTests. Magnifier look + fade verified on the real desktop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WinUI 3 magnifier re-centered its window on the cursor on every mouse-wheel step, so moving the mouse and scrolling again dragged the magnifier to the new cursor location instead of zooming the region it was first opened over. This regressed from the WPF version, which only positions the window on first appearance (while it is still transparent, Opacity < 0.5).

Move the window sizing and positioning into the first-appearance branch, so the magnifier is centered on the cursor once per zoom session and then stays put, only animating the card between zoom levels.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added Area-Localization issues regarding to Localization the application Product-Color Picker All things around the Color Picker utility labels Jul 7, 2026
/// The color editor window. Approach A: lean on the native WinUI title bar + a fixed-size
/// OverlappedPresenter instead of the WPF WindowChrome / DWM / custom-title-bar machinery.
/// </summary>
public sealed partial class ColorEditorWindow : Window

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use WinUIEX so we can do more stuff in XAML directly vs code behind (see the Workspaces Editor PR).

Apparently agents aren't picking up on that.. should we call it out specifically in our migration skills?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Localization issues regarding to Localization the application Product-Color Picker All things around the Color Picker utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Color Picker to WinUI3

2 participants