Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ A failed mutation or failed write does not replace committed settings and does n
| Bounded process identity cache | `ApplicationIdentityCache` |
| Overlay geometry | `OverlayBoundsResolver` |
| Overlay resource lifetime and retargeting | `OverlayController` |
| Native call failure classification and diagnostics | `NativeCall` |
| Native target, rendering, geometry refresh, and transition grace | `MagnifierOverlay` |
| Notification-area presentation | `TrayPresenter` |
| Application-table presentation and edit mechanics | `ApplicationProfilesGrid` |
Expand Down Expand Up @@ -111,6 +112,16 @@ A rendered frame may remain visible for at most 125 ms during target transition.

The current backend uses the same rectangle for the magnifier source and overlay destination.

## Native call failure policy

`NativeCall` classifies fallible Win32 and Magnification API operations explicitly:

- **critical** initialization and effect calls throw a `Win32Exception` containing the operation name and native error code;
- **transient** geometry, positioning, and source-update failures are diagnosed, hide the overlay, and allow a later timer tick to recover;
- **best effort** cleanup failures are diagnosed without replacing the primary application failure.

`ShowWindow` and `InvalidateRect` are handled explicitly at their call sites because their Boolean return values do not represent a standard extended-error success contract.

## Configuration grid boundary

`ApplicationProfilesGrid` owns columns, rows, selectors, status painting, selection, empty state, stable executable-path keys, separate typed change events, row updates, and failed-cell restoration. It does not know about persistence or dialogs.
Expand All @@ -130,7 +141,7 @@ The current backend uses the same rectangle for the magnifier source and overlay

## Architecture test strategy

Architecture checks are behavior-first. Transaction publication, defensive settings snapshots, failed persistence, expected and unexpected transaction failures, emergency ordering, runtime state transitions, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests.
Architecture checks are behavior-first. Transaction publication, defensive settings snapshots, failed persistence, expected and unexpected transaction failures, emergency ordering, runtime state transitions, native call classification, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests.

Source inspection is retained only for exhaustive negative rules that cannot be proven by a finite runtime scenario:

Expand Down
180 changes: 110 additions & 70 deletions src/SightAdapt/MagnifierOverlay.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel;
using System.Drawing;

namespace SightAdapt;
Expand Down Expand Up @@ -86,48 +85,48 @@ protected override void OnShown(EventArgs eventArgs)
{
base.OnShown(eventArgs);

NativeMethods.SetLayeredWindowAttributes(
Handle,
0,
255,
NativeMethods.LwaAlpha);

_magnifierWindow = NativeMethods.CreateWindowEx(
0,
NativeMethods.WcMagnifier,
"SightAdapt Magnifier",
NativeMethods.WsChild | NativeMethods.WsVisible,
0,
0,
Math.Max(ClientSize.Width, 1),
Math.Max(ClientSize.Height, 1),
Handle,
nint.Zero,
nint.Zero,
nint.Zero);

if (_magnifierWindow == nint.Zero)
{
throw new Win32Exception(
System.Runtime.InteropServices.Marshal.GetLastWin32Error(),
"Could not create the Windows magnifier control.");
}
NativeCall.RequireSuccess(
NativeMethods.SetLayeredWindowAttributes(
Handle,
0,
255,
NativeMethods.LwaAlpha),
"Set layered overlay opacity");

_magnifierWindow = NativeCall.RequireHandle(
NativeMethods.CreateWindowEx(
0,
NativeMethods.WcMagnifier,
"SightAdapt Magnifier",
NativeMethods.WsChild |
NativeMethods.WsVisible,
0,
0,
Math.Max(ClientSize.Width, 1),
Math.Max(ClientSize.Height, 1),
Handle,
nint.Zero,
nint.Zero,
nint.Zero),
"Create Windows magnifier control");

var transform = MagTransform.Identity;
if (!NativeMethods.MagSetWindowTransform(_magnifierWindow, ref transform))
{
throw new Win32Exception(
"Could not initialize the magnifier transform.");
}
NativeCall.RequireSuccess(
NativeMethods.MagSetWindowTransform(
_magnifierWindow,
ref transform),
"Initialize magnifier transform");

ApplyColorEffectToMagnifier();

var excludedWindows = new[] { Handle };
NativeMethods.MagSetWindowFilterList(
_magnifierWindow,
NativeMethods.MwFilterModeExclude,
excludedWindows.Length,
excludedWindows);
NativeCall.RequireSuccess(
NativeMethods.MagSetWindowFilterList(
_magnifierWindow,
NativeMethods.MwFilterModeExclude,
excludedWindows.Length,
excludedWindows),
"Exclude overlay window from magnifier source");

_initialized = true;
_updateTimer.Start();
Expand All @@ -144,7 +143,10 @@ protected override void Dispose(bool disposing)

if (_magnifierWindow != nint.Zero)
{
NativeMethods.DestroyWindow(_magnifierWindow);
NativeCall.BestEffort(
NativeMethods.DestroyWindow(
_magnifierWindow),
"Destroy magnifier control");
_magnifierWindow = nint.Zero;
}

Expand All @@ -165,11 +167,11 @@ protected override void WndProc(ref Message message)
private void ApplyColorEffectToMagnifier()
{
var colorEffect = _colorEffect;
if (!NativeMethods.MagSetColorEffect(_magnifierWindow, ref colorEffect))
{
throw new Win32Exception(
$"Could not apply the '{_transformId}' visual transform.");
}
NativeCall.RequireSuccess(
NativeMethods.MagSetColorEffect(
_magnifierWindow,
ref colorEffect),
$"Apply '{_transformId}' visual transform");
}

private void UpdateOverlay()
Expand All @@ -179,15 +181,17 @@ private void UpdateOverlay()
return;
}

var targetExists = NativeMethods.IsWindow(TargetHandle);
var targetExists =
NativeMethods.IsWindow(TargetHandle);
var targetAvailable = targetExists &&
NativeMethods.IsWindowVisible(TargetHandle) &&
!NativeMethods.IsIconic(TargetHandle) &&
IsTargetForeground();

if (!targetAvailable)
{
if (_hasRenderedFrame && IsWithinTransitionGrace())
if (_hasRenderedFrame &&
IsWithinTransitionGrace())
{
return;
}
Expand All @@ -198,7 +202,7 @@ private void UpdateOverlay()
}
else
{
NativeMethods.ShowWindow(Handle, NativeMethods.SwHide);
HideOverlay();
}

return;
Expand All @@ -211,38 +215,70 @@ private void UpdateOverlay()
_overlayScope,
out var geometry))
{
NativeMethods.ShowWindow(Handle, NativeMethods.SwHide);
HideOverlay();
return;
}

var destination = geometry.Destination;
NativeMethods.SetWindowPos(
Handle,
NativeMethods.HwndTopMost,
destination.Left,
destination.Top,
destination.Width,
destination.Height,
NativeMethods.SwpNoActivate | NativeMethods.SwpShowWindow);

NativeMethods.SetWindowPos(
_magnifierWindow,
nint.Zero,
0,
0,
destination.Width,
destination.Height,
NativeMethods.SwpNoActivate | NativeMethods.SwpNoZOrder);
if (!NativeCall.TryTransient(
NativeMethods.SetWindowPos(
Handle,
NativeMethods.HwndTopMost,
destination.Left,
destination.Top,
destination.Width,
destination.Height,
NativeMethods.SwpNoActivate |
NativeMethods.SwpShowWindow),
"Position overlay window"))
{
HideOverlay();
return;
}

if (!NativeCall.TryTransient(
NativeMethods.SetWindowPos(
_magnifierWindow,
nint.Zero,
0,
0,
destination.Width,
destination.Height,
NativeMethods.SwpNoActivate |
NativeMethods.SwpNoZOrder),
"Resize magnifier control"))
{
HideOverlay();
return;
}

var source = geometry.Source;
if (!NativeMethods.MagSetWindowSource(_magnifierWindow, source))
if (!NativeCall.TryTransient(
NativeMethods.MagSetWindowSource(
_magnifierWindow,
source),
"Set magnifier source rectangle"))
{
NativeMethods.ShowWindow(Handle, NativeMethods.SwHide);
HideOverlay();
return;
}

_hasRenderedFrame = true;
NativeMethods.InvalidateRect(_magnifierWindow, nint.Zero, true);

// A repaint request is intentionally best effort. InvalidateRect's
// return value does not provide a useful extended error contract.
_ = NativeMethods.InvalidateRect(
_magnifierWindow,
nint.Zero,
true);
}

private void HideOverlay()
{
// ShowWindow returns the previous visibility state, not success.
_ = NativeMethods.ShowWindow(
Handle,
NativeMethods.SwHide);
}

private bool IsWithinTransitionGrace()
Expand All @@ -265,8 +301,11 @@ private void ResetTransitionGrace()

private bool IsTargetForeground()
{
var foreground = NativeMethods.GetForegroundWindow();
foreground = NativeMethods.GetAncestor(foreground, NativeMethods.GaRoot);
var foreground =
NativeMethods.GetForegroundWindow();
foreground = NativeMethods.GetAncestor(
foreground,
NativeMethods.GaRoot);
return foreground == TargetHandle;
}

Expand All @@ -279,7 +318,8 @@ private static nint ValidateTarget(nint targetHandle)
nameof(targetHandle));
}

private static string NormalizeTransformId(string transformId)
private static string NormalizeTransformId(
string transformId)
{
return !string.IsNullOrWhiteSpace(transformId)
? transformId.Trim()
Expand Down
Loading
Loading