diff --git a/.github/Feature-Prompts/Net11/Application.SystemTextAwareness'/Application.SystemTextAwareness.md b/.github/Feature-Prompts/Net11/Application.SystemTextAwareness'/Application.SystemTextAwareness.md new file mode 100644 index 00000000000..da7edcf854e --- /dev/null +++ b/.github/Feature-Prompts/Net11/Application.SystemTextAwareness'/Application.SystemTextAwareness.md @@ -0,0 +1,180 @@ +# Task: Create a new API — `Application` system-text-size awareness — and the respective API proposal + +## What to do + +Create a **new API proposal / API review issue in the upstream `dotnet/winforms` repo** +(`origin` = my fork, `upstream` = the Microsoft repo where this lands). Write it per the +WinForms repo conventions and the relevant skills for authoring new-API issues. Apply the +skills; don't ask me for boilerplate. + +This proposal introduces **runtime awareness of the Windows Accessibility text-size +setting** at the `Application` level, plus a per-`Form` change notification. It is the +**foundation** proposal; a companion `TreeView.NodeLeading` proposal references this one. + +## Before you implement anything + +**Verify every premise below against current source before committing to the design.** +Verify, don't trust. If any premise is wrong, stop and tell me. Key files (VMR @ +`96982699e0dd8c046f397541dc0eb235ea8a4958`): + +- `src/winforms/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs` +- `src/winforms/src/System.Windows.Forms/System/Windows/Forms/Application.ParkingWindow.cs` +- `src/runtime/src/libraries/Microsoft.Win32.SystemEvents/...` (SystemEvents / UserPreferenceCategory) + +## Rationale — surface and complete an existing partial implementation + +This is **not** a net-new feature; it **finishes a half-built one**. WinForms already reads +the Accessibility text-size setting, but only once and only for the default font: + +`ScaleHelper.ScaleToSystemTextSize(Font?)` reads +`HKEY_CURRENT_USER\Software\Microsoft\Accessibility` → value `TextScaleFactor` +(REG_DWORD, clamped 100–225), and returns the font scaled by `TextScaleFactor / 100` +(or `null` if 100, if the font `IsSystemFont`, or if the OS is < Windows 10 1507). It is +documented in-source as the **Settings → Display → Make Text Bigger** setting. + +The gaps: (1) the value is **never surfaced** to developers; (2) it is read **once**, at +default-font construction, and the app **never reacts** when the user changes the setting at +runtime; (3) there is **no notification** mechanism. This proposal fills those three gaps. + +## Three-knob disambiguation (MANDATORY callout — reviewers will conflate these) + +Windows surfaces three *different* sizing mechanisms; the Settings UI even puts two on one +page (`System → Display → Custom scaling` shows a "Custom scaling 100–500%" box AND a +"Text size" link). The proposal MUST state plainly which one it targets: + +1. **Display / Custom scaling (100–500%)** → this is **DPI**. Already handled by + `HighDpiMode` and the DPI events (`WM_DPICHANGED`, `Control.DpiChanged*`). **NOT** this + proposal. +2. **Accessibility → Text size (100–225%)** → registry `TextScaleFactor` under + `HKCU\Software\Microsoft\Accessibility`; WinRT `UISettings.TextScaleFactor` / + `TextScaleFactorChanged`. **THIS is the target.** Independent of DPI. +3. **Legacy pre-Win10 per-element text sizing** (title bars/menus) → **removed** in Windows + 10 1703; the Accessibility slider replaced it. Mentioned only to close the loop. + +State explicitly that `Application.SystemTextSize` reflects **#2 only**, and is orthogonal to +DPI (#1). + +## Proposed API + +### `Application` (process-static) + +- **`public static double SystemTextSize { get; }`** — the current Accessibility text-scale + factor (1.0–2.25; i.e. `TextScaleFactor / 100`). **Live getter** — re-reads the value, does + not cache, because it is a system setting that changes at runtime. Well-defined regardless + of whether any `Form` exists or how many UI threads are running (it is process-global). +- **`public static SystemTextSizeAwareness SystemTextSizeAwareness { get; set; }`** — the + mode. **Enum, not bool**, deliberately, to reserve room for a future `Automatic`: + - `Unaware` (default) — no notification raised; fully back-compatible, nothing changes. + - `Notify` — raise change notifications (see below); the app decides how to respond. + - *(reserved, NOT implemented now: `Automatic` — framework re-flows for you. Reserving the + enum slot now avoids a future breaking bool→enum change, the same lesson `HighDpiMode` + learned.)* +- **`public static event EventHandler? SystemTextSizeChanged`** — fires once per process when + the setting changes (only when awareness is `Notify`). + +### `Form` (instance) + +- **`public event EventHandler? SystemTextSizeChanged`** — instance event, raised on each + top-level `Form` when the setting changes. +- **`protected virtual void OnSystemTextSizeChanged(EventArgs e)`** — overridable, fires the + instance event via the `EventHandlerList` pattern (`Events[s_systemTextSizeChangedEvent]`). + +## The trigger architecture (the leak-critical part — get this exactly right) + +A naive design — a static `Application.SystemTextSizeChanged` that `Form`s/`Control`s +subscribe to — **leaks**: the static event strongly roots every subscriber, so no `Form` +that subscribes is ever collected. Avoid this by **mirroring the DPI architecture**, where +`Control`/`Form` learn of DPI changes from their **own `WndProc`** (`WM_DPICHANGED` → +`OnDpiChanged` → instance event via `EventHandlerList`), **not** from a static subscription. + +Verified facts that constrain the design: + +- **`WM_SETTINGCHANGE` is broadcast** (`HWND_BROADCAST`) and delivered directly to top-level + windows' `WndProc`s — it bypasses the thread message queue, so **`IMessageFilter` does NOT + see it.** Do not use a message filter. +- **The WinForms parking window is message-only** (`CreateParams.Parent = HWND_MESSAGE`). + Message-only windows are **excluded from broadcasts**, so the parking window **cannot** + receive `WM_SETTINGCHANGE`. Do not use it. +- **`Application` has no `MainForm`.** The main form lives on `ApplicationContext` (per-run, + per-UI-thread), can be `null` (tray/loop-only apps), and is mutable (splash→main handoff). + So the main form is **not** a reliable receiver. Do not anchor the app-level event to it. +- **`SystemEvents` already owns a hidden top-level broadcast-receiving window** (its + `.NET-BroadcastEventWindow`), and exposes `UserPreferenceChanged` with a + `UserPreferenceCategory` (the relevant value is `Accessibility`, which is **coarse** — it + covers any accessibility change, so you must re-read `TextScaleFactor` and diff to confirm + it was text-scale). + +**Resulting design:** + +- **App-level:** `Application` makes **one internal, process-lifetime** subscription to + `SystemEvents.UserPreferenceChanged`, filters `Category == Accessibility`, re-reads + `TextScaleFactor`, diffs against the cached value, and if changed raises the static + `SystemTextSizeChanged`. This is a single framework-static→framework-static link — it does + **not** root any user object, so it is **not** the leak hazard. Reuses the existing hidden + broadcast window; **no new HWND** required. +- **Form-level:** each top-level `Form` handles `WM_SETTINGCHANGE` in its **own `WndProc`** + (it is a broadcast — every top-level window receives it), re-reads + diffs, and raises its + **instance** `SystemTextSizeChanged` via `OnSystemTextSizeChanged`. `Form`s do **not** + subscribe to `Application` — no rooting, lifetime = the window. + +Caveat to document: there is **no dedicated `WM_TEXTSCALECHANGED`** message. Both paths must +recognize a *relevant* change by re-reading `TextScaleFactor` and comparing, not by the +message alone. + +## Aids vs. leave-it-to-the-user + +**Notify-only. No automatic re-layout / font-rescaling aids in this proposal.** Reasons: +text scale interacts with `AutoScaleMode`, anchored/docked layout, and explicitly-set fonts +in app-specific ways; a generic "scale all fonts by the factor" helper breaks more than it +fixes. The reserved `Automatic` enum value is exactly where such behavior would live later. +`Notify` gives the developer the factor and the event; they decide. (Consistent with the +companion `NodeLeading` proposal's conservative-default philosophy.) + +## Why this matters across controls (evidence — include the matrix) + +Multiple text-measuring controls derive item/row/tile extents from a `Font` that today only +reacts to the text-size setting once at startup (via `ScaleToSystemTextSize` on the default +font) and never again. So **any single cached height scalar is wrong the moment text size +changes at runtime** — which is the core argument for runtime awareness: + +- **ListBox / ComboBox** — have `MeasureItem` + `OwnerDrawVariable` (a real per-item measure + hatch) and `ItemHeight`. Their gap is the legacy default base calc + the missing runtime + text-scale reaction — i.e. exactly this proposal. +- **TreeView** — has neither `MeasureItem` nor a wrapped native height API; only a uniform + native item height. Worst-positioned for a managed fix; addressed by the companion + `TreeView.NodeLeading` proposal, which depends on this one. +- **ListView (Details)** — no `MeasureItem`, no native row-height message; row height is + comctl-computed from `SmallImageList` + control font, while per-item/subitem fonts are + honored via `NM_CUSTOMDRAW` (`CDRF_NEWFONT`). Userland workarounds (phantom `SmallImageList`; + `LVS_OWNERDRAWFIXED` + one-shot reflected `WM_MEASUREITEM`; "inflate control font / shrink + item fonts") each have holes (header leak, set-once, exhaustive per-item font setting, + owner-draw-all). **No clean complete userland solution exists** — strengthening the case + that text-size reaction belongs in the framework. + +## XML doc requirements + +- Document that `SystemTextSize` is the **Accessibility text-size** factor (Settings → + Display → Make Text Bigger), **not** DPI/display scaling, and is process-global / live. +- Document the `Unaware`/`Notify` semantics and that `Automatic` is reserved for future use. +- Document the no-rooting design note on the static event (so consumers understand instance + vs. static). + +## Open questions for review + +- Should `SystemTextSize` be `double` (1.0–2.25) or expose the raw int percent (100–225)? +- Behavior on OS < Windows 10 1507 (where `ScaleToSystemTextSize` no-ops): `SystemTextSize` + returns 1.0 and no events fire? +- Whether to also expose the value/event on `Application` only, leaving `Form` consumers to + use their own `WndProc` override — or provide the `Form` instance event as proposed + (recommended, for parity with the DPI event model). + +## Output + +The upstream issue per the skills: summary; the "complete a partial implementation" +rationale; the mandatory three-knob disambiguation; the proposed API; the leak-safe trigger +architecture with the four verified constraints (broadcast vs. IMessageFilter, message-only +parking window, no MainForm on Application, SystemEvents reuse); Notify-only stance; the +cross-control matrix; XML-doc requirements; open questions. Flag anything the source +contradicts. diff --git a/.github/copilot/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md b/.github/copilot/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md new file mode 100644 index 00000000000..09894f9d23f --- /dev/null +++ b/.github/copilot/Application/net11-VisualStylesMode/TextBoxBase-VisualStyles-WorkOrder.md @@ -0,0 +1,137 @@ +# Work Order — Port `TextBoxBase` NC-Painting + `VisualStylesMode` Chrome onto `VisualStylesNet11` + +**Target branch:** `KlausLoeffelmann/winforms` → `VisualStylesNet11` (current-`main`-based; modern path layout, no `/src/src/` doubling; `TextBoxBase.cs` ≈ 2130 lines). + +**Source of the original implementation (pinned):** `KlausLoeffelmann/winforms` @ `cf32e9c4efeba9d77e1a14025a8590b104e3c705` (old `/src/System.Windows.Forms/src/...` layout; `TextBoxBase.cs` ≈ 2562 lines). Relevant files: +- `.../Controls/TextBox/TextBoxBase.cs` — NC paint, NC calc, focus invalidation, `GetVisualStylesPadding`, helpers. +- `.../Controls/TextBox/TextBoxBase.NonClientBitmapCache.cs` — the offscreen cache class. +- `.../Controls/TextBox/TextBox.cs` — derived-level overrides (`CreateParams`, `WndProc`, `OnBackColorChanged`, `PRF_NONCLIENT` path). + +**Standing approval:** API review board sign-off from the .NET 9 cycle is still valid; DRI owns the call. This is a port + cleanup, **not** a redesign. Do not invent new public API beyond what the original exposed plus the `Padding` unshadowing called out below. + +**House style (apply throughout):** namespaces globally imported (no `using`/`imports` noise); C# 13/14; NRTs on; `var` only for long type names or when the type is obvious from the RHS, explicit type names for primitives; blank line between a new block and a following `return`; pattern matching / `is` / `and` / `or` / switch expressions preferred; collection expressions (`List x = [];`); expression-bodied members for single-line methods/read-only props formatted with the `=>` on the next line, 1-space-indented. Generate XML doc comments; use ``/``. + +--- + +## PROMPT 1 — Carry-over / Port + +> **Role:** You are porting a working-but-imperfect feature across a 3-year gap in the surrounding file. Faithfully reproduce the *mechanism*; do not improve, simplify, or "modernize" the algorithm except where this document explicitly says to. Where the surrounding `VisualStylesNet11` code has moved on, rebase onto the new shape rather than pasting. + +**Task.** Bring the non-client (NC) painting feature for `TextBoxBase` (and the `TextBox` derived touchpoints) from the pinned SHA `cf32e9c4…` onto `VisualStylesNet11`. The target branch currently has **none** of this work (no `WmNcPaint`/`WmNcCalcSize`/`OnNcPaint`/`GetVisualStylesPadding`/`NonClientBitmapCache`), but it **does** have the `VisualStylesMode` property infrastructure referenced in doc comments — wire into that, don't redeclare it. + +**Steps, in order:** + +1. **Fetch and diff the three source files** at SHA `cf32e9c4…` against their `VisualStylesNet11` counterparts. Produce a short inventory of every member you intend to add or modify, grouped by file, before writing any code. + +2. **Port `TextBoxBase.cs` members:** + - `WmNcPaint` / `OnNcPaint` (offscreen bitmap fill → AA rounded/single chrome → blit; `GetWindowDC`/`ReleaseDC` in `finally`). + - `WmNcCalcSize` (carves the padding band from `NCCALCSIZE_PARAMS->rgrc[0]`; gated on the `_triggerNewClientSizeRequest` latch). + - `InitializeClientArea` (the one-shot `SetWindowPos(SWP_FRAMECHANGED|NOMOVE|NOSIZE|NOZORDER|NOACTIVATE)` that provokes the single NC-calc). + - `GetVisualStylesPadding` / `GetScrollBarPadding` and the `VisualStyles{Fixed3D|FixedSingle|NoBorder}BorderPadding`, `BorderThickness` consts. + - The `WM_NCCALCSIZE` / `WM_NCPAINT` cases in `WndProc`. + - `OnGotFocus` / `OnLostFocus` / `OnSizeChanged` NC-frame invalidation (`RedrawWindow` with `RDW_FRAME|RDW_INVALIDATE`). + - `PreferredHeight` split (`PreferredHeightClassic` vs `PreferredHeightCore` selected by `VisualStylesMode`). + - Reconcile `GetPreferredSizeCore` with the modern branch already present on target. + +3. **Port `TextBoxBase.NonClientBitmapCache.cs`** — BUT this is a **decision point**, see Step 6. + +4. **Reconcile `TextBox.cs` (derived):** the target already has `CreateParams`, `WndProc`, `OnBackColorChanged` (special-casing `Fixed3D`), `OnGotFocus`, and a `WM_PRINTCLIENT`/`PRF_NONCLIENT` + `Application.RenderWithVisualStyles` path. Merge the NC behavior so the derived overrides cooperate with the new base NC painting (no double border draw, no fighting the `PRF_NONCLIENT` path). Call out every conflict you resolve. + +5. **Unshadow `Padding`.** On target it is still the neutered shadow (`[Browsable(false)]`, `EditorBrowsableState.Never`, `DesignerSerializationVisibility.Hidden`, `get/set => base.Padding`). Make it a real, browsable, serializable property that feeds the NC band via `GetVisualStylesPadding`. Preserve classic-mode behavior when `VisualStylesMode` is `Disabled`/`Classic`. Note the designer-serialization and back-compat implications in the PR description. + +6. **Retarget the rounded-rectangle helpers to the framework.** The original called fork-local `FillRoundedRectangle`/`DrawRoundedRectangle`. These now ship as **`System.Drawing.Graphics` instance methods** (`(Pen, Rectangle, Size)` + `RectangleF`/`SizeF` overloads; landed in the .NET 9 wave, current on target). **Delete the fork-local helpers and call the shipped methods.** ⚠️ Verify a **`FillRoundedRectangle(Brush, …)`** overload actually exists on target — the public ref only enumerates the `Draw`(`Pen`) overloads. If the `Fill`/`Brush` overload is absent, STOP and flag it; do not re-add a private helper without surfacing the gap. + +7. **Cache → `BufferedGraphics` (DECIDED — implement, do not re-evaluate).** The original used a hand-rolled per-instance `NonClientBitmapCache` (`CreateCompatibleBitmap` + `Image.FromHbitmap` + manual `DeleteObject`, `EnsureSize` realloc). Jeremy's late "use the existing cached bitmap" is confirmed to mean WinForms' own **`BufferedGraphics`/`BufferedGraphicsContext`** (the engine behind `OptimizedDoubleBuffer`; in `System.Drawing.Common` since .NET Framework 2.0, present on target). It is **not** `System.Drawing.Imaging.CachedBitmap` — that type is a read-only, device-dependent, blit-only frozen copy (no `Graphics`, translation-only, dies on bit-depth change) and cannot be a render target. **Delete `NonClientBitmapCache` entirely** (and its file `TextBoxBase.NonClientBitmapCache.cs`) and the `_cachedBitmap` field; replace with the shared buffer. Exact wiring: + - Inside `OnNcPaint`, get the shared context and allocate the buffer against the **window-DC `Graphics` already created in `WmNcPaint`**, sized to the window `bounds`: + `BufferedGraphicsContext context = BufferedGraphicsManager.Current;` + `using BufferedGraphics buffer = context.Allocate(graphics, bounds);` + `Graphics offscreenGraphics = buffer.Graphics;` + - **Do NOT `using`/dispose `buffer.Graphics`** — the `buffer` owns it; `using` the **buffer** only. (The original `using`-disposed its `GetNewGraphics()` because it owned that `Graphics`; that ownership is now the buffer's.) + - **All drawing into `offscreenGraphics` is unchanged** — the `FillRectangle(parentBackgroundBrush…)` corner-fill, the `BorderStyle` switch, the focus line: byte-for-byte identical, just a different `Graphics` target. + - **`ExcludeClip(clientBounds)` stays on the *target* `graphics`** (the window-DC one), exactly where it is now, set *before* the buffer draws. It governs where `Render()` may blit, protecting the client area — unchanged semantics. + - Replace the final blit `graphics.DrawImageUnscaled(offscreenBitmap, Point.Empty);` with **`buffer.Render();`** (no argument — it blits to the `graphics` captured at `Allocate` time). + - While here, fix the pre-existing **double-dispose** in `WmNcPaint`: it has both `using Graphics graphics = …` and an explicit `graphics.Dispose()` in `finally`. Drop the explicit `Dispose()`; keep the `using` (or keep explicit and drop `using` — one, not both). + - **Rationale to record in PR notes:** WinForms paints NC serially (one HWND at a time on the UI thread), so a single shared buffer suffices for any number of controls; the per-instance cache kept N resident GDI bitmaps to serve a one-deep queue. Steady-state allocation is unchanged (zero — shared buffer is reused when size fits); resident GDI memory drops from N× to 1×. The only cost is buffer-resize churn if controls of *wildly varying* sizes paint in a grow/shrink-alternating order — see smoke scenario 7 instrumentation. + +8. **Carve clamp + chrome degradation (settled design — implement exactly as stated, do NOT add a minimum size).** The original `WmNcCalcSize` does raw subtraction on `rgrc[0]` with **no clamp**, so a large `Padding` (made worse because `GetVisualStylesPadding(true)` *adds* the live scrollbar allowance from `GetScrollBarPadding` on top of the border padding) can drive the carved client rect to **zero or inverted**. Two separate fixes, and they are deliberately *not* a `MinimumSize`: + - **(8a) Never-invert clamp in `WmNcCalcSize`.** Floor each carved extent so the client rect can never invert: after the four adjustments, ensure `bottom >= top` and `right >= left` (e.g. clamp so the resulting client width/height is `Math.Max(0, …)`). A 0–1px client area is **acceptable and intended** — shipping multiline `TextBox` already shrinks to ~1px with scrollbars present, and we match that exactly. **Do NOT introduce a min-height/`MinimumSize`**, and do NOT make sizing behavior differ by `VisualStylesMode` (that would fracture the appearance-only contract of the opt-in). The clamp only prevents *underflow past zero*, which raw subtraction does and plain shrinking does not. + - **(8b) Paint-time chrome degradation in `OnNcPaint`.** The rounded `Fixed3D` chrome (15px radius) renders as a broken lozenge below roughly `2 × cornerRadius + BorderThickness` in height. When the available band/height is below that viable threshold, **fall back to the original/simple chrome render** (flat or single-style border) instead of the rounded path. This is a *rendering* fallback only — it does not change size or layout. Rationale on record: if the box is so small there's no usable client area, the control isn't usable anyway, so graceful visual degradation (not a size floor) is the correct response. + +9. **Build** `System.Windows.Forms` for the target TFM. Resolve all errors. Do not suppress new analyzer warnings without a one-line justification each. + +**Deliverable:** a single commit (or tight series) on `VisualStylesNet11` plus a PR description that lists: members added/modified per file, every `TextBox.cs` conflict resolved, confirmation that `NonClientBitmapCache` was removed and replaced by `BufferedGraphics` (Step 7) with the resize-churn rationale, the clamp/degradation (Step 8) confirmed as render-only with no size minimum, the `Padding` unshadowing implications, and any flagged gaps (Step 6 `Fill` overload). + +--- + +## PROMPT 2 — Critical Review (run AFTER Prompt 1, BEFORE smoke test) + +> **Role:** Adversarial reviewer. The author wants the issues a sharp WinForms maintainer would catch, not reassurance. Cite file + line for every finding. Classify each as **MUST-FIX**, **SHOULD-FIX**, or **PRESERVE (do not 'improve')**. + +Audit the ported code against this checklist. For each item, state the finding and the exact location. + +1. **DPI scaling of the corner radius.** The original hardcodes `const int cornerRadius = 15` and `BorderThickness = 1` in device-independent units, then uses them inside a DPI-scaled NC band, while `GetVisualStylesPadding` *does* take a DPI path (`_deviceDpi`). Confirm whether the radius/thickness now scale Per-Monitor-V2. If not → **MUST-FIX** (corners look proportionally too tight at 150/200%). + +2. **Full-frame NC repaint vs. partial `hrgnClip`.** `WmNcPaint` ignores the wParam clip region and repaints the whole frame. This is the **intended** fix for the offscreen-restore "dirty corners" artifact — verify it's preserved. But confirm `base.WndProc(ref m)` is still invoked with the original message and isn't double-painting the native border under the custom chrome. Classify the "ignore clip" behavior as **PRESERVE**. + +3. **Corner-blend source = `Parent?.BackColor ?? BackColor`.** This is the known ceiling: corners blend against the parent's flat back color, so they mismatch over a gradient/image/Mica/sibling. For the common case (solid form/panel) it's correct. **PRESERVE** — do not let it be "improved" into a fake general-case solution. Note it as a documented limitation only. + +4. **`WM_NCCALCSIZE` ↔ `Padding` round-trip + underflow.** Verify the band carved in `WmNcCalcSize` matches what `GetVisualStylesPadding(true)` reports and what `GetPreferredSizeCore`/`SizeFromClientSize` assume, for all three `BorderStyle` values × `Multiline` × scrollbars. Off-by-one here clips text or the caret. **Additionally** confirm the never-invert clamp (Prompt 1 Step 8a) is present and correct: with large `Padding` on a small multiline box *with both scrollbars*, the carved client rect must floor at 0, never invert. Remember the threshold is **border padding + live scrollbar padding** (`GetScrollBarPadding` reads `WS_HSCROLL`/`WS_VSCROLL`), so underflow hits sooner than the `Padding` value alone implies. Classify "0–1px client area is allowed, no min-size" as **PRESERVE** — do not let a reviewer or the agent add a `MinimumSize` floor. + +4b. **Chrome degradation below viable height.** Confirm `OnNcPaint` (Prompt 1 Step 8b) falls back to simple/flat chrome when height < ≈`2 × cornerRadius + BorderThickness`, instead of drawing a corrupted rounded rect. This is **render-only**; assert it does **not** alter size, layout, or `ClientSize`. Verify the fallback path itself is DPI-correct (the threshold scales with the radius, which per item 1 must scale). + +5. **`BorderStyle` fork + native edge suppression.** `Fixed3D` → rounded chrome, `FixedSingle` → single + underline, `None` → fill. Confirm the native `WS_EX_CLIENTEDGE`/`WS_BORDER` from `CreateParams` is suppressed when NC chrome is active, so the native edge isn't drawn under the custom one. + +6. **`VisualStylesMode` gating is total.** Every NC entry point (`WmNcPaint`, `WmNcCalcSize`, `InitializeClientArea`, the focus/size invalidations) must early-out to byte-for-byte classic behavior when `VisualStylesMode` is `Disabled`/`Classic`. One missing guard = a back-compat regression. Note the original's `OnLostFocus` was **missing** the guard that `OnGotFocus` had — verify the port fixed this asymmetry. + +7. **DC / GDI lifetime.** `GetWindowDC`→`ReleaseDC` in `finally`, and `Graphics.FromHdc`+`Dispose` ordering: correct **only** because `FromHdc` doesn't own the DC. **PRESERVE** — flag any "tidy into a single `using`" as a regression. Confirm the `WmNcPaint` **double-dispose** was fixed (it had both `using Graphics` and an explicit `graphics.Dispose()`). For the `BufferedGraphics` swap (Prompt 1 Step 7): verify the **buffer** is `using`-scoped but **`buffer.Graphics` is NOT separately disposed**; verify `Allocate` targets the window-DC `graphics` and `Render()` is called with no argument; confirm `NonClientBitmapCache` and the `_cachedBitmap` field are fully removed with no dangling refs. Audit for any HBITMAP/HDC leak in the new path (there should be none — the buffer owns it). + +8. **DPI-change without handle recreate.** `_triggerNewClientSizeRequest` is a one-shot latch reset on handle recreate. Does a DPI change that does *not* recreate the handle re-carve the band with new padding? If the band can go stale on monitor move → **MUST-FIX** or at least an explicit tracked issue. + +9. **Caret / IME / selection repaint.** The native `EDIT` invalidates aggressively. Confirm NC chrome doesn't go stale on caret blink/IME composition, and conversely that NC isn't thrashing-repainting on every caret tick. (Author never confirmed this was clean in the original.) + +10. **`TextBox.cs` derived reconciliation.** Verify the derived `WndProc`, `OnBackColorChanged` `Fixed3D` special-case, and the `PRF_NONCLIENT`/`Application.RenderWithVisualStyles` path don't conflict with base NC painting (double draw, wrong-mode paint). + +11. **Allocation churn.** Brushes/pens use cached scopes (`GetCachedSolidBrushScope`/`GetCachedPenScope`) — good; confirm preserved. Confirm the offscreen surface isn't reallocated per paint (only on size change). + +**Deliverable:** a findings list (file:line, severity, recommendation). MUST-FIX items get fixed in this pass; SHOULD-FIX either fixed or filed; PRESERVE items annotated in code with a brief `// Intentional:` comment so the next reader doesn't "fix" them. + +--- + +## PROMPT 3 — Smoke Test Harness + +> *("Smoke test" = the shallow "does it power on without catching fire" pass — from hardware bring-up, where first power-on literally checked for smoke — run before any deep/perf testing. Goal here: broad coverage that it comes up and behaves on the obvious axes, with the two known-fragile cases as explicit named tests.)* + +**Task.** Build a throwaway WinForms test app (separate project, not shipped) that exercises the ported feature across its permutation space and **specifically reproduces the two regressions this feature is prone to.** + +**Permutation grid** — generate a form populated with `TextBox`es (and at least one `RichTextBox`, since `TextBoxBase` is the shared base) covering the cross-product of: +- `BorderStyle`: `None` × `FixedSingle` × `Fixed3D` +- `Multiline`: `false` × `true` (+ `WordWrap` on/off for multiline) +- `Padding`: `Empty` × asymmetric (e.g. `2,6,2,6`) × large (`12`) +- Scrollbars: none × vertical × both +- `VisualStylesMode`: `Disabled`/`Classic` (must look exactly like today) × `Net10`+ (new chrome) +- Focused vs unfocused (drive focus programmatically to capture the adorner/underline) + +**Named, must-pass scenarios (the ones that silently regress):** + +1. **Offscreen-restore ("dirty corners").** Move the window partly off the left/top screen edge, then back. Assert the NC corner regions are repainted clean (no stale pixels). This is the artifact that drove the full-frame-repaint design. Automate the drag via `SetWindowPos`/`MoveWindow`; capture before/after. + +2. **Partial NC invalidation.** Trigger a partial `WM_NCPAINT` (e.g. overlap then reveal a sliver of the frame) and assert the whole chrome is coherent, not just the revealed strip. + +3. **Per-Monitor-V2 DPI.** Run DPI-aware; move forms between a 100% and a 150%/200% monitor (or fake via `LogicalToDeviceUnits`/DPI-changed messages). Assert corner radius, border thickness, and padding band all scale; assert no clipped text/caret. + +4. **Classic-mode parity.** With `VisualStylesMode = Disabled`, assert the control is pixel-identical to baseline `main` (native edge, no custom NC). A regression here is the back-compat line breaking. + +5. **`BorderStyle` switch at runtime** (`Fixed3D`↔`FixedSingle`↔`None`) and **`Padding` change at runtime** — assert the band re-carves and chrome redraws without artifacts (exercises the `_triggerNewClientSizeRequest` reset path). + +6. **Focus transitions** — tab through the grid; assert the focus underline (single) / 3D focus line (Fixed3D, shortened to clear the corner curve) appears/clears correctly. + +7. **Shrink-to-collapse (clamp + degradation).** Take a multiline `Fixed3D` box with large `Padding` (e.g. `12`) and **both** scrollbars visible, then programmatically drag/resize its height down toward 1px. Assert: (a) **no crash / no inverted client rect** handed to the native `EDIT` — the carve floors at 0 (Step 8a); (b) below ≈`2 × cornerRadius + thickness` the chrome **falls back to flat/simple render** rather than drawing a corrupted lozenge (Step 8b); (c) the control **still collapses** to ~1px exactly like classic multiline — assert it is **not** held open by any min-size (regression if a floor appeared). Repeat at 150%/200% DPI so the degradation threshold is verified scaled, not fixed at 96-dpi pixels. + +8. **BufferedGraphics allocation churn (perf sanity).** Build two forms: (a) **40 same-size** textboxes, (b) **40 wildly varying-size** textboxes (mix tiny and large), all `VisualStylesMode ≥ Net10`. Force a full repaint storm (invalidate all NC frames repeatedly; resize the form to cascade re-layout). Instrument the shared buffer: wrap/observe `BufferedGraphicsManager.Current` and count actual **bitmap (re)allocations** vs. reuses across the storm. Assert: case (a) allocates the buffer ≈once then reuses (steady-state alloc ≈ 0); case (b) may reallocate on grow but must **not** allocate-per-paint. Log alloc count per case. This empirically confirms the shared-buffer reasoning from Prompt 1 Step 7 and catches any accidental per-paint allocation regression. + +**Harness mechanics:** +- A "capture all" button that screenshots each form to disk per `VisualStylesMode`, for eyeball diffing classic-vs-modern and pre-vs-post-DPI. +- A console/log line per assertion (pass/fail) so it can run semi-automated. +- Keep it dependency-light: raw WinForms + `SetWindowPos`/`RedrawWindow` P/Invoke for the offscreen and invalidation drivers. + +**Deliverable:** the test project + a one-screen README naming the six scenarios and how to run them, plus a results log from one full run on the porter's machine (note DPI of monitors used). diff --git a/.github/skills/building-code/SKILL.md b/.github/skills/building-code/SKILL.md index 040dd437081..67fd8c7ba30 100644 --- a/.github/skills/building-code/SKILL.md +++ b/.github/skills/building-code/SKILL.md @@ -11,6 +11,22 @@ metadata: # Building the WinForms Repository +> ## 🛑 TENET — Build the solution ONLY with `build.cmd` +> +> **Never** build, validate, or declare the WinForms solution "clean" with a plain +> `dotnet build` / `dotnet msbuild` of `Winforms.sln`. Only **`build.cmd`** (Arcade) applies the +> repository's CI configuration — the **PublicAPI analyzer (RS0016/RS0017)**, the code-style and +> documentation analyzers, and **`-warnAsError`**. A plain `dotnet build` silently downgrades or +> skips these, so **"0 warnings" there does NOT mean CI is green** — the very same change can fail +> the official build with errors. +> +> * **Full / release / package / "is it clean?" verification → always `build.cmd`** (see §2). +> * A single-project `dotnet build` (see §3) is an **inner-loop convenience only**. It is fine while +> iterating, but you **must re-verify with `build.cmd` before claiming a change builds cleanly**. +> * If `build.cmd` cannot run in your environment, the closest fallback is +> `dotnet build /p:ContinuousIntegrationBuild=true /p:TreatWarningsAsErrors=true` — and +> you must say so explicitly rather than implying a `build.cmd` result. + ## Prerequisites * Windows is required for WinForms runtime scenarios, test execution, and Visual @@ -45,6 +61,12 @@ You can pass any extra `Build.ps1` flags after `Restore.cmd`, e.g. ## 2 Full Solution Build (preferred) +> **Always use `build.cmd` (Arcade) for full, release, and package builds.** Do **not** use a plain +> `dotnet build` of the solution for these — only `build.cmd` guarantees the Arcade-supported build +> options and the download of the correct base SDK (`global.json`) needed to compile. Plain +> `dotnet build` is reserved for the fast single-project inner loop (see Section 3), and even then +> only after at least one successful `build.cmd` / `Restore.cmd`. + ``` .\build.cmd ``` @@ -57,6 +79,56 @@ Under the hood this runs: eng\common\Build.ps1 -NativeToolsOnMachine -restore -build -bl ``` +### 2.1 Full (clean) test build — required workflow + +For a full, clean build of the whole solution, **clean the artifacts first, then build**: + +```powershell +# 1. Clean the artifacts folder. +.\build -clean + +# 2. Build the full solution. +.\build +``` + +**Reporting requirement:** a full build is long-running, so while it runs **report progress back to +the user in the console to bridge the wait and give early orientation.** As assemblies complete, +report which assemblies have been **built successfully** and which **failed and with how many +errors**. Prefer running the build with a binary log (the default `-bl`) and/or stream the console +output so per-project results can be surfaced as they happen rather than only at the end. + +### 2.2 Release build + +```powershell +.\build -configuration release +``` + +### 2.3 Creating packages + +```powershell +# Debug packages +.\build -pack + +# Release packages +.\build -configuration release -pack +``` + +### 2.4 Full `Build.ps1` parameter list + +`build.cmd` forwards every extra argument to `eng\common\Build.ps1`. The full surface is: + +``` +Build.ps1 [-configuration ] [-platform ] [-projects ] + [-verbosity ] [-msbuildEngine ] [-warnAsError ] + [-warnNotAsError ] [-nodeReuse ] [-buildCheck] [-restore] + [-deployDeps] [-build] [-rebuild] [-deploy] [-test] [-integrationTest] + [-performanceTest] [-sign] [-pack] [-publish] [-clean] [-productBuild] + [-fromVMR] [-binaryLog] [-binaryLogName ] [-excludeCIBinarylog] + [-ci] [-prepareMachine] [-runtimeSourceFeed ] + [-runtimeSourceFeedKey ] [-excludePrereleaseVS] + [-nativeToolsOnMachine] [-help] [-properties ] [] +``` + ### Common flags | Flag | Short | Description | @@ -90,6 +162,12 @@ eng\common\Build.ps1 -NativeToolsOnMachine -restore -build -bl ## 3 Optimized Building a Single Project (fast inner-loop) +> **Inner-loop only.** Use plain `dotnet build` of a single project **only** for quick iteration on +> one project, and **only after** at least one successful `.\build.cmd` / `.\Restore.cmd`. It does +> **not** guarantee the Arcade-supported build options or the download of the correct base SDK, so it +> must **never** be used for a full solution build, a release build, packaging, or any build whose +> result you intend to report as authoritative. For those, always use `build.cmd` (Section 2). + Prefer rebuilding just the project(s) with recent changes by using the standard `dotnet build` command, **after** at least one initial successful full restore (via `.\Restore.cmd` or `.\build.cmd`). diff --git a/.github/skills/control-api-tests/SKILL.md b/.github/skills/control-api-tests/SKILL.md index 5badcc2547c..ff4e9304230 100644 --- a/.github/skills/control-api-tests/SKILL.md +++ b/.github/skills/control-api-tests/SKILL.md @@ -66,6 +66,28 @@ The project uses **xUnit** with **FluentAssertions**. Key attributes: These are custom xUnit attributes that ensure tests run on an STA thread, which WinForms requires for COM interop and UI operations. +### 1.4 Async tests: pass a CancellationToken, respect `#nullable` + +The repository runs **xUnit v3** and enforces the relevant analyzers as **errors** under the CI +build (`build.cmd`). Two pitfalls fail CI even though a plain `dotnet build` may not flag them: + +* **CA2016 / xUnit1051 — always pass a `CancellationToken` to async calls.** Methods such as + `Task.Delay` must receive a token so a cancelled test run stops promptly. In xUnit v3 use + `TestContext.Current.CancellationToken`: + + ```csharp + await Task.Delay(25, TestContext.Current.CancellationToken); + ``` + + When you receive a `CancellationToken ct` (e.g. in a callback), **forward it** rather than dropping it. + +* **CS8632 — nullable annotations need a `#nullable` context.** If a test file uses `?` reference + annotations (e.g. `object? sender`) but the project does not enable nullable, add `#nullable enable` + at the top of the file (or remove the annotation). Match the surrounding files' convention. + +> Verify with `build.cmd` (CI parity) — see the `building-code` skill's build tenet. A plain +> single-project `dotnet build` can report these as 0 warnings while CI fails them as errors. + --- ## 2. Test Method Naming diff --git a/.github/skills/new-control-api/SKILL.md b/.github/skills/new-control-api/SKILL.md index ce6c58b6d2e..3030cf92ef7 100644 --- a/.github/skills/new-control-api/SKILL.md +++ b/.github/skills/new-control-api/SKILL.md @@ -148,7 +148,34 @@ System.Windows.Forms.MyEnum.Value2 = 1 -> System.Windows.Forms.MyEnum **Nullable annotations:** `?` = nullable reference, `!` = non-nullable reference. Value types do not carry these markers unless `Nullable`. -### 2.4 Publicly accessible interfaces +### 2.4 New `override` members must be tracked too + +The PublicAPI analyzer (RS0016) treats a **newly introduced `override`** of a public or +protected member as new API surface — even though the base member is already public. Whenever +you **add an `override` that did not previously exist on that type**, add a line for it to +`PublicAPI.Unshipped.txt` with the `override` prefix. This is easy to miss for paint/lifecycle +overrides added to support a feature. Examples: + +```text +override System.Windows.Forms.CheckBox.OnPaint(System.Windows.Forms.PaintEventArgs! pevent) -> void +override System.Windows.Forms.CheckBox.Dispose(bool disposing) -> void +override System.Windows.Forms.ButtonBase.OnVisualStylesModeChanged(System.EventArgs! e) -> void +``` + +> **CI catches this, a plain `dotnet build` may not.** RS0016 is enforced as an **error** under +> the CI/Arcade build (`build.cmd`); a single-project `dotnet build` can report it as 0 warnings. +> Always re-verify API tracking with `build.cmd` (see the `building-code` skill's build tenet). + +### 2.5 Related pitfalls when adding members to a control + +* **Hiding an inherited member (CS0114):** if your new member intentionally hides an inherited + one (e.g. a `private new bool ShouldSerializePadding()` shadowing `Control.ShouldSerializePadding()`), + you **must** use the `new` keyword, or the CI build fails. +* **`cref` to internal types in another assembly (CS1574):** XML-doc `` cannot + resolve a type that is `internal` in a *different* assembly (even via `InternalsVisibleTo`). Use + `TypeName` (plain code font) instead of a `cref` for such references. + +### 2.6 Publicly accessible interfaces If a new **public or protected interface** is introduced (or an existing one gains new members), every member that is publicly accessible must also appear @@ -468,51 +495,73 @@ protected virtual void OnMyPropertyChanged(EventArgs e) --- -## 7. .NET Version Guard — Mandatory +## 7. API Stability: Experimental vs. Stable — and Version Guards -All new public APIs **must** be guarded with a preprocessor directive for the -target .NET version. Currently, new APIs target at least **.NET 11**: +### 7.1 New APIs are STABLE by default — do NOT mark them `[Experimental]` -```csharp -#if NET11_0_OR_GREATER - /// - /// Gets or sets the corner radius for the control's border. - /// - public int CornerRadius - { - get => Properties.GetValueOrDefault(s_cornerRadiusProperty, 0); - set - { - ArgumentOutOfRangeException.ThrowIfNegative(value); +New public APIs ship as **normal, stable APIs by default**. Do **not** add the +`[Experimental(...)]` attribute, a `WFO5xxx` diagnostic ID, or `[WFO5xxx]` +PublicAPI prefixes unless the work item **explicitly** asks for an experimental +API. - if (Properties.GetValueOrDefault(s_cornerRadiusProperty, 0) != value) - { - Properties.AddOrRemoveValue(s_cornerRadiusProperty, value, defaultValue: 0); - OnCornerRadiusChanged(EventArgs.Empty); - } - } - } -#endif -``` +> **Never make an API experimental implicitly.** Experimental status is a +> deliberate, requested decision (it changes the customer contract and requires a +> diagnostic ID + suppression to consume). If the context does not explicitly call +> for it, the API is stable. + +### 7.2 When an experimental API *is* explicitly requested + +Only when the task explicitly requests an experimental API: + +1. Add (or reuse) a diagnostic ID in the `WFO500x` group in + `src\System.Windows.Forms.Analyzers\src\System\Windows\Forms\Analyzers\Diagnostics\DiagnosticIDs.cs` + (e.g. `ExperimentalDarkMode = "WFO5001"`, `ExperimentalAsync = "WFO5002"`, + `ExperimentalAsyncDropTarget = "WFO5003"`). New IDs continue the sequence. +2. Decorate the API: + ```csharp + [Experimental(DiagnosticIDs.ExperimentalXxx, UrlFormat = DiagnosticIDs.UrlFormat)] + ``` +3. Prefix every PublicAPI entry for that API with the diagnostic ID, e.g. + `[WFO5001]System.Windows.Forms.SomeNewApi.get -> ...`. +4. Add a row to **both** `docs\analyzers\Experimental.Help.md` and + `docs\list-of-diagnostics.md`. +5. Suppress the diagnostic where the framework itself consumes the API + (`#pragma warning disable WFOxxxx` / `#Disable Warning WFOxxxx` in VB). -> **Why?** Version guards ensure new APIs are only available on the .NET version -> they were approved for, preventing accidental use on older runtimes. The guard -> applies to the entire API surface: property, event, `On` method, and any -> associated types. +When the API later **graduates to stable** (typically the next release), reverse +all five steps: remove the attribute, the `[WFOxxxx]` PublicAPI prefixes, the +suppressions, the docs rows, and the unused diagnostic ID. -The matching tests must use the **same** preprocessor guard: +### 7.3 Version guards + +This repository **single-targets the current in-development .NET** (see +`TargetFramework` / `NetCurrent`), so source is **not** wrapped in +`#if NETxx_0_OR_GREATER` guards — there are none in `System.Windows.Forms`. Do +**not** add `#if NET11_0_OR_GREATER` blocks around new APIs. Add the member +directly: ```csharp -#if NET11_0_OR_GREATER - [WinFormsFact] - public void MyControl_CornerRadius_Set_GetReturnsExpected() +/// +/// Gets or sets the corner radius for the control's border. +/// +public int CornerRadius +{ + get => Properties.GetValueOrDefault(s_cornerRadiusProperty, 0); + set { - using MyControl control = new() { CornerRadius = 5 }; - Assert.Equal(5, control.CornerRadius); + ArgumentOutOfRangeException.ThrowIfNegative(value); + + if (Properties.GetValueOrDefault(s_cornerRadiusProperty, 0) != value) + { + Properties.AddOrRemoveValue(s_cornerRadiusProperty, value, defaultValue: 0); + OnCornerRadiusChanged(EventArgs.Empty); + } } -#endif +} ``` +Tests do not need a version guard either. + --- ## 8. Checklist Before Submitting @@ -521,7 +570,8 @@ Before considering the implementation complete, verify: * [ ] API proposal issue exists (upstream or fork) with full proposal format * [ ] All new public/protected members are in `PublicAPI.Unshipped.txt` -* [ ] New APIs guarded with `#if NET11_0_OR_GREATER` (or appropriate version) +* [ ] API is **stable** (no `[Experimental]`/`WFO5xxx`) unless experimental was + explicitly requested; no `#if NETxx_0_OR_GREATER` guards * [ ] Property values stored via `PropertyStore` (not backing fields) * [ ] Every property has a CodeDOM serialization strategy * [ ] Every property has `On[Property]Changed` + `[Property]Changed` event @@ -533,7 +583,7 @@ Before considering the implementation complete, verify: * [ ] XML documentation on every new public/protected member * [ ] Naming follows precedent on the control and its base classes * [ ] Publicly accessible interface members are tracked in PublicAPI files -* [ ] Unit tests cover the new API surface (with matching version guard) +* [ ] Unit tests cover the new API surface ### 8.1 API issue checklist diff --git a/Winforms.sln b/Winforms.sln index fef423d18a4..4deb770ce1b 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -197,9 +197,6 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Private.Windows.GdiPlus", "src\System.Private.Windows.GdiPlus\System.Private.Windows.GdiPlus.csproj", "{442C867C-51C0-8CE5-F067-DF065008E3DA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Copilot", "Copilot", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" - ProjectSection(SolutionItems) = preProject - .github\copilot-instructions.md = .github\copilot-instructions.md - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GDI", "GDI", "{D619FF8C-D99A-48AB-B16B-2F0E819B46D5}" ProjectSection(SolutionItems) = preProject @@ -218,6 +215,13 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Private.Windows.P EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Private.Windows.Core", "src\System.Private.Windows.Core\src\Microsoft.Private.Windows.Core.csproj", "{36A02BBB-B60B-5F23-6AF0-F41561A9275C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{4D53E144-73EF-49BC-BF11-F416E187944A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net11-VisualStylesMode", "net11-VisualStylesMode", "{59D2720E-DBA9-4282-869E-0717D6311BDF}" + ProjectSection(SolutionItems) = preProject + .github\copilot\Application\net11-VisualStylesMode\TextBoxBase-VisualStyles-WorkOrder.md = .github\copilot\Application\net11-VisualStylesMode\TextBoxBase-VisualStyles-WorkOrder.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1188,6 +1192,8 @@ Global {4A2BD741-482C-4BF7-8A2D-5535A770DB69} = {583F1292-AE8D-4511-B8D8-A81FE4642DDC} {799CC0C2-236B-4A76-8CE3-65C346182CC1} = {77FEDB47-F7F6-490D-AF7C-ABB4A9E0B9D7} {36A02BBB-B60B-5F23-6AF0-F41561A9275C} = {77FEDB47-F7F6-490D-AF7C-ABB4A9E0B9D7} + {4D53E144-73EF-49BC-BF11-F416E187944A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {59D2720E-DBA9-4282-869E-0717D6311BDF} = {4D53E144-73EF-49BC-BF11-F416E187944A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136} diff --git a/build.cmd b/build.cmd index 9852a061a26..c25a3fbebe1 100755 --- a/build.cmd +++ b/build.cmd @@ -1,3 +1,3 @@ @echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -NativeToolsOnMachine -restore -build -bl %*" +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0eng\build.cmd.ps1" %* exit /b %ErrorLevel% diff --git a/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md b/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md new file mode 100644 index 00000000000..bc73ee83449 --- /dev/null +++ b/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/Create-Github-API-Proposal-Prompt.md @@ -0,0 +1,155 @@ +# Copilot Prompt 1 — Author the WinForms API Suggestion (GitHub issue) + +## Your task + +Write a complete API suggestion for the **dotnet/winforms** repository, ready to be filed +as a GitHub issue with the `api-suggestion` label. The issue covers one cohesive feature +area — *flicker-free UI mutation in WinForms* — composed of **three severable sub-features**. + +You are not transcribing a settled spec. You are an experienced WinForms/.NET API designer +collaborating on this. The sections below give you **settled facts** and **current thinking +with reasoning**. Treat them differently (see "How to treat this briefing"). + +## How to treat this briefing + +- **Settled — do not change:** the three sub-features and their scope; the public API + *names* and *enum values* listed under "Settled API surface" below. These were + argued through already. +- **Current thinking — challenge freely:** every *mechanism*, *risk framing*, + *implementation strategy*, and *open question* below is our current lean with our + reasoning attached. If you find a stronger argument, pivot — and say why. Pitch + approaches as approaches, not gospel. We expect the API review board (and likely + Stephen Toub) to pressure-test the mechanism choices; pre-empt that. +- **Actively look for what we missed.** Compatibility hazards, interaction with existing + WinForms subsystems (data binding, `BindingSource`, `TableLayoutPanel`, MDI, DPI + changes, `Control.RecreateHandle`, accessibility/UIA, designer surface), threading, + trimming/AOT. If something here is wrong or naive, the most useful thing you can do + is say so. + +## Deliverable + +A single Markdown document structured as a fileable `api-suggestion` issue, using exactly +these sections (this is the dotnet/winforms house format): + +- `## Rationale` +- `## API Proposal` (C# signatures in fenced blocks, `namespace` declared) +- `## API Usage` +- `## Alternative Designs` +- `## Risks` +- `## Will this feature affect UI controls?` +- `### Status Checklist` (the standard api-suggestion checklist) + +If during drafting you conclude the three sub-features should be filed as separate issues +rather than one, say so explicitly at the top and structure accordingly — that is a +legitimate pivot. + +--- + +## Sub-feature A — `ISupportSuspendPainting` / `ISupportSuspendRelocation` + +### Settled API surface +- Two free-standing public interfaces, `System.Windows.Forms` namespace: + `ISupportSuspendPainting` with `BeginSuspendPainting()` / `EndSuspendPainting()`; + `ISupportSuspendRelocation` with `BeginSuspendRelocation()` / `EndSuspendRelocation()`. +- `Control` implements both. +- `ListView`, `ListBox`, `ComboBox`, `TreeView`, `RichTextBox` override the *painting* + methods to forward to their existing public `BeginUpdate` / `EndUpdate` (which remain + unchanged in shape and behavior — source and binary compat). +- User-facing scope objects + extension methods (`SuspendPainting()`, + `SuspendRelocation()`). + +### Current thinking — challenge freely +- **Default `Control` painting suspension** via `WM_SETREDRAW`, refcounted; resume edge + calls `Invalidate(true)`. Layout suspension forwards to existing + `SuspendLayout` / `ResumeLayout`. +- **Refcount state** lives lazily on `Control` via the existing property-store slot + pattern (zero cost until used). We considered default interface methods to avoid + touching `Control`; rejected because `WM_SETREDRAW` is not reentrant and DIMs cannot + hold per-instance state without a `ConditionalWeakTable` indirection that is strictly + worse. Re-test this conclusion. +- **Not tied to `IArrangedElement`** — deliberately. `IArrangedElement` is internal, and + `ToolStripItem` (an implementer) has no meaningful painting-suspension story. Future + HWND-less "visuals" should implement these interfaces directly with their own + mechanism. Evaluate whether the *relocation* interface specifically has a better home. +- **Scope type — our lean, expect pushback:** make the scopes `readonly ref struct` + (pattern-based `Dispose`, works with `using`) rather than `class : IDisposable`. + Reasoning: `ref struct` makes "forgot the `using`" / leaked-scope a *compile error*, + which is what lets us honestly downgrade the unbalanced-refcount risk. Tradeoff: no + `async`/iterator/lambda-capture/field storage, and you lose polymorphic `IDisposable` + return. We think that tradeoff is fine for synchronous "mutate now" code paths. + **This is a recommendation we expect to be pressure-tested in review — present both + options with the tradeoff and recommend, do not assert.** +- **Refcount risk framing:** even with `ref struct` scopes, the interface methods stay + `public` (designer-generated `InitializeComponent` must call them, and that code lives + in the user's assembly). So a developer *can* call them directly. The honest claim is + "the ergonomic path makes imbalance hard to hit accidentally; the refcount remains the + correctness backstop" — not "the risk is eliminated." Nested scopes are supported by + design, so the counter is necessary regardless. + +## Sub-feature B — `DeferLocationChange` + `DeferWindowPos` batching + +### Settled API surface +- A recommended user-facing entry point `DeferLocationChange()` returning a disposable + scope, with multi-arg overloads to opt out of individual bundled behaviors + (`suppressRender`, `suspendLayout`). + +### Current thinking — challenge freely +- The scope bundles three things for a "I'm about to move many children" code path: + Win32 `BeginDeferWindowPos` / `DeferWindowPos` / `EndDeferWindowPos` batching; + `SuspendLayout` / `ResumeLayout`; and paint suppression (compose this from + sub-feature A rather than duplicating `WM_SETREDRAW` logic). +- **Perf claim — be precise, do not overclaim.** `DeferWindowPos` improves *throughput*: + one synchronized native move pass instead of N `SetWindowPos` calls, each with its own + `WM_WINDOWPOSCHANGED`/`WM_SIZE`/invalidation/intermediate repaint. The + `SuspendLayout` bundling separately improves *computation*: N `PerformLayout` + invocations collapse to one. Neither speeds up the `LayoutEngine` algorithm itself. + The proposal must keep these two wins distinct and must NOT claim "the layout engine + got faster." +- **`HDWP` lifetime is the sharpest mechanical edge.** `BeginDeferWindowPos` allocates; + each `DeferWindowPos` *returns a new HDWP* (must be captured/threaded); on failure it + returns `NULL` and the *entire batch is lost*. The scope's `Dispose` must handle a + `NULL` HDWP coherently (fall back to individual `SetWindowPos`, or abort cleanly — + never `EndDeferWindowPos` on `NULL`) and must not leak a half-built HDWP if an + exception unwinds through the `using` body. This deserves its own risk bullet. +- Same `ref struct` recommendation as A applies to this scope. Note: if the scope is + `ref struct` it cannot be returned as `IDisposable` — evaluate whether the + multi-overload story still works (it should; `using` is pattern-based). + +## Sub-feature C — `Application.SetFormAppearanceMode` (deferred form display) + +### Settled API surface +- `Application.SetFormAppearanceMode(FormAppearanceMode mode)` — process-wide + configuration API, called early (before the first form), consistent in pattern and + lifecycle with `Application.SetColorMode` and `Application.SetHighDpiMode`. +- `enum FormAppearanceMode { Classic = 0, Deferred = 1 }`. +- `Classic` = pre-.NET 11 behavior (opt-out). `Deferred` = .NET 11 default. +- Note the deliberate split: `Classic` is the enum's *zero value* (conservative + `default`), while `Deferred` is the *runtime default* applied when the API is never + called. Call this out so review does not read it as a contradiction. + +### Current thinking — challenge freely +- Mechanism: cloak top-level forms via DWM (`DWMWA_CLOAK`) at handle creation, uncloak + once the background has been painted, so the form is revealed in one step instead of + flashing a default (white) background — most visible in dark mode. +- **Uncloak timing is genuinely open — this is the part most likely to need a better + idea.** Our naive lean is "uncloak after the first `WM_PAINT` that paints the form + background." Uncloak too early → still flashes; too late → window appears slow to + open. Unlike Edge, WinForms has no single universal "first real frame ready" signal — + it depends on double-buffering, custom `OnPaintBackground`, late-painting child + controls. Evaluate alternatives and recommend; flag remaining uncertainty honestly. +- **Honesty caveat that must survive into the docs:** deferral applies to the *form + background*. A deep tree of late-painting child controls can still produce visible + updates after reveal. The XML doc / proposal must state this so a late-child blink is + not later mis-filed as a regression. (The flash-elimination benefit belongs in the + XML ``; the caveat in ``.) +- Evaluate interaction with: MDI child forms, `Form.Show` vs `ShowDialog`, splash + screens / forms that *want* to appear instantly, owned/tool windows, per-monitor DPI + changes during creation, and `Form.Opacity` / layered windows. + +## Filing instruction + +If your final assessment is that the design is sound, produce the issue body ready to +file with the `api-suggestion` label. If you found a reason to pivot on anything outside +the settled API surface, lead with a short "Deviations from the briefing" note +explaining what you changed and why, then give the proposal. Either way, the proposal +itself is the deliverable. diff --git a/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md b/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md new file mode 100644 index 00000000000..bcff8090eff --- /dev/null +++ b/docs/Feature-Prompts/SuspendRelocationPaintingFlashPreventing/SuspendRelocationAndPainting-API-Feature-Prompt.md @@ -0,0 +1,76 @@ +# Copilot Prompt 2 — Implement the flicker-free UI mutation APIs + +## Prerequisite + +This prompt consumes the **approved API suggestion** produced by Prompt 1 (and refined +through API review). Before implementing, read that proposal in full and treat its final +`API Proposal` section as the contract. Where the approved proposal and this prompt +disagree, the **approved proposal wins** — note any such conflict explicitly rather than +silently picking one. + +## Your task + +Implement the three sub-features in **dotnet/winforms**, production quality, against the +approved API surface. You have engineering latitude on *internals* — the public surface +is fixed by the proposal, the implementation is yours to do well. + +## Scope + +### A — `ISupportSuspendPainting` / `ISupportSuspendRelocation` +- The two interfaces, `System.Windows.Forms` namespace. +- `Control` implementation: refcounted painting suspension; lazy refcount state via the + existing property-store slot pattern (follow the established `Control` precedent — do + not add an eager field). Layout-suspension methods forward to existing + `SuspendLayout` / `ResumeLayout`. +- Overrides on `ListView`, `ListBox`, `ComboBox`, `TreeView`, `RichTextBox` forwarding + the painting methods to their existing `BeginUpdate` / `EndUpdate`. The existing public + `BeginUpdate` / `EndUpdate` signatures and behavior MUST NOT change. +- The user-facing scope type(s) and extension methods, exactly as the approved proposal + specifies them (`ref struct` vs `class` per the proposal's final decision). +- Unbalanced `End*` must match `ResumeLayout` precedent (the proposal will have settled + throw-vs-no-op; follow it). + +### B — `DeferLocationChange` + `DeferWindowPos` batching +- The scope and its overloads per the approved proposal. +- Win32 batching via `BeginDeferWindowPos` / `DeferWindowPos` / `EndDeferWindowPos`. + Capture and thread the returned `HDWP` correctly on every `DeferWindowPos` call. +- `Dispose` must handle a `NULL` HDWP coherently and must not leak on exception unwind. +- Compose paint suppression from sub-feature A; do not duplicate `WM_SETREDRAW` logic. + +### C — `Application.SetFormAppearanceMode` + `FormAppearanceMode` +- The enum (`Classic = 0`, `Deferred = 1`) and the `Application` configuration API. +- `Deferred` is the runtime default when the API is never called; `Classic` restores + pre-.NET 11 behavior. +- DWM cloaking at top-level form handle creation; uncloak per the timing strategy the + approved proposal settled on. +- Must be inert / safe when the OS does not support the relevant DWM attributes. + +## Engineering requirements + +- Target the C# language version and runtime of the current dotnet/winforms `main`. +- NRTs enabled; assume the repo's global usings. +- Match dotnet/winforms code style, P/Invoke conventions (CsWin32-generated `PInvoke` + surface), and the existing interop patterns — do not hand-roll `DllImport` if a + generated entry point exists. +- All public API gets XML docs. For `FormAppearanceMode.Deferred`, the flash-elimination + benefit goes in ``; the "background only, deep child trees may still update" + caveat goes in ``. +- Public API additions require matching entries in the `*.cs` reference-assembly / + public-API-baseline files the repo uses. +- Thread affinity: all of this assumes the UI thread; add debug assertions where the + repo already does, and do not let them affect release behavior. + +## Tests + +- Unit tests for refcount balance, including nesting and unbalanced-`End`. +- Tests that `ListView` et al. route through their native path and do not double-suspend. +- Tests for `DeferLocationChange` correctness including the `NULL`-HDWP fallback and + exception-unwind path. +- For `FormAppearanceMode`, tests for `Classic` (no behavior change) and `Deferred` + (cloak/uncloak lifecycle), plus the OS-unsupported fallback. + +## Deliverable + +A pull request (or a clear set of commits) implementing the above, with a PR description +that summarizes the change, links the API suggestion, and calls out any place the +implementation revealed a problem with the approved design that review should revisit. diff --git a/docs/Net11Api_03_ImproveControlRendering.HighRiskReview.md b/docs/Net11Api_03_ImproveControlRendering.HighRiskReview.md new file mode 100644 index 00000000000..0150a26b9a5 --- /dev/null +++ b/docs/Net11Api_03_ImproveControlRendering.HighRiskReview.md @@ -0,0 +1,23 @@ +# Net11Api_03 ImproveControlRendering High-Risk Review + +This document records a review finding that needs native lifecycle investigation before it can be patched safely. + +## Deferred reveal is not activated during initial display + +Reviewed merge base: `8b618e7f5` +Reviewed branch tip: `1ee2b58a6` + +The deferred-reveal implementation attempts to cloak a form from `OnHandleCreated`, while `ShouldUseDeferredAppearanceCloak` requires both `IsHandleCreated` and `Visible`. During `Show`, `ShowDialog`, and `Application.Run(form)`, the handle is created while evaluating `HWND`, before `ShowWindow` makes the form visible. The visibility state changes later while processing `WM_SHOWWINDOW`, and there is no subsequent cloak attempt. The intended initial-display cloak therefore does not activate on the normal display path. + +A safe correction requires selecting a one-shot point after handle creation but before the first compositor presentation. Moving the operation into `WM_SHOWWINDOW` could cloak later hide/show cycles, while removing the visibility check could cloak hidden forms whose handles are created for unrelated reasons. + +Before implementation, document and verify the state timeline for: + +- `Show`, `ShowDialog`, and `Application.Run`; +- hidden forms with pre-created handles; +- handle recreation; +- hide/show cycles; +- owned forms and splash screens; +- DWM composition or cloak-call failure. + +The selected design should define a one-shot invariant and explicitly reject stale state after handle recreation. Native verification should inspect the DWM cloak state before first paint and after reveal. Automated tests should cover all display paths and preserve a clear rollback path if compositor timing differs across supported Windows versions. diff --git a/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md b/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md new file mode 100644 index 00000000000..b3cb075a1a1 --- /dev/null +++ b/docs/Net11Api_04_KioskManagerComponent.HighRiskReview.md @@ -0,0 +1,38 @@ +# Net11Api_04 KioskModeManager High-Risk Review + +This document records review findings that require an API or lifecycle design decision before they can be patched safely. + +## FullScreen requested state and actual state differ + +`KioskModeManager.FullScreen` currently returns `true` when fullscreen has been requested but no target form is available. `FullScreenChanged`, however, is raised only when the resolved form actually enters or exits fullscreen. This creates two observable state models: + +- The property reports requested state through `_pendingFullScreen || _isFullScreen`. +- The event reports actual window state through changes to `_isFullScreen`. + +Consequently, setting `FullScreen = true` without a resolved form changes the property without raising the event. When a form later becomes available, the event is raised even though the property value remains `true`. + +Before changing this behavior, decide whether `FullScreen` represents requested state or actual window state. The decision must cover: + +- initialization and delayed parenting; +- missing, replaced, reparented, and disposed container controls; +- event ordering and two-way data binding; +- `ToggleFullScreen` behavior while a request is pending; +- disposal and failed fullscreen transitions; +- compatibility for applications already observing the property or event. + +Tests should define the complete transition matrix for requested, pending, entered, exited, reparented, and disposed states. + +## Session-notification registration is not retried + +`KioskModeFormObserver.RegisterSessionNotifications` records a failed `WTSRegisterSessionNotification` call as `false` and retries only after form handle recreation. During Windows startup, registration can fail with `RPC_S_INVALID_BINDING` before `Global\TermSrvReadyEvent` is signaled. A kiosk application started during that interval can therefore miss session notifications for the lifetime of its form handle. + +A safe fix needs a non-blocking retry design that specifies: + +- which Win32 errors are retryable and which are terminal; +- how waiting for `Global\TermSrvReadyEvent` is cancelled; +- how completion is marshalled to the form's UI thread; +- how stale callbacks are rejected after handle recreation; +- how registration and unregistration remain paired; +- how disposal races with an outstanding wait. + +Tests should cover service-not-ready startup, successful retry, handle recreation during the wait, and disposal before the wait completes. diff --git a/docs/Net11Api_05_VisualStylesMode.HighRiskReview.md b/docs/Net11Api_05_VisualStylesMode.HighRiskReview.md new file mode 100644 index 00000000000..8293c03922d --- /dev/null +++ b/docs/Net11Api_05_VisualStylesMode.HighRiskReview.md @@ -0,0 +1,29 @@ +# Net11Api_05 VisualStylesMode High-Risk Review + +This document records animation infrastructure findings that require architecture and lifecycle decisions before they can be patched safely. + +## Animation timer remains active and can consume a CPU core + +`AnimationManager` retains its high-precision timer registration after the first animation. The timer uses a 14 ms `PeriodicTimer` cadence with a 16.667 ms absolute frame target and then actively spins to that target. Once the periodic schedule falls behind, ticks can complete immediately and the loop can spend nearly an entire frame spinning. The process can therefore retain 1 ms timer resolution and consume approximately one logical processor after animations have settled. + +A safe redesign must establish: + +- lazy timer ownership based on the number of running animations; +- an idle-shutdown invariant after the final animation settles; +- balanced `timeBeginPeriod` and `timeEndPeriod` calls; +- atomic registration, unregistration, start, and stop transitions; +- a scheduling algorithm that does not accumulate backlog or busy-wait indefinitely; +- disposal behavior during application and message-loop shutdown. + +Before implementation, capture CPU and timer-resolution measurements, analyze current register/unregister races, and add tests proving that the timer parks after all animations stop. + +## Process-wide animation dispatch targets the first UI thread + +The process-wide `AnimationManager` singleton captures the `SynchronizationContext` of the first UI thread that initializes it. Renderers created by another supported WinForms UI thread are then ticked on the first thread. Renderer callbacks invalidate their controls directly, even though `Control.Invalidate` is not a cross-thread-safe API. The singleton's unsynchronized initialization also permits competing manager construction. + +A safe redesign must choose between: + +- one animation manager per UI thread or synchronization context; or +- per-renderer dispatch to each control's owning context. + +The selected ownership model must cover message-loop teardown, renderer disposal, concurrent registration, and controls moving through handle recreation. Validation should include two independent `Application.Run` UI threads and prove that every animation callback executes on its control's owning thread. diff --git a/docs/building.md b/docs/building.md index 25f26541301..da5a785bb75 100644 --- a/docs/building.md +++ b/docs/building.md @@ -10,6 +10,7 @@ Follow the prerequisites listed at [Developer Guide](developer-guide.md). * Run `.\build.cmd` from the repository root. This builds the `Winforms.sln` using the default config (Debug|Any CPU). * To specify a build configuration, add `-configuration` followed by the config such as `.\build -configuration Release`. +* To build on Windows ARM64, use `.\build -platform arm64`. This maps the solution platform to `TargetArchitecture=arm64` without promoting local Visual Studio native tool paths. Note that this does **not** build using your machine-wide installed version of the dotnet sdk. It builds using the repo-local .NET SDK specified in the global.json in the repository root. diff --git a/eng/build.cmd.ps1 b/eng/build.cmd.ps1 new file mode 100644 index 00000000000..3bfc5c85aff --- /dev/null +++ b/eng/build.cmd.ps1 @@ -0,0 +1,65 @@ +[CmdletBinding(PositionalBinding = $false)] +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $BuildArgs +) + +$forwardArgs = [System.Collections.Generic.List[string]]::new() +$useNativeTools = $true +$isArm64Platform = $false +$hasTargetArchitecture = $false + +for ($i = 0; $i -lt $BuildArgs.Length; $i++) { + $arg = $BuildArgs[$i] + + if ($arg.StartsWith('/p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase) -or + $arg.StartsWith('-p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase)) { + $hasTargetArchitecture = $true + } + + if ($arg.Equals('-platform', [StringComparison]::OrdinalIgnoreCase) -and + $i + 1 -lt $BuildArgs.Length -and + $BuildArgs[$i + 1].Equals('arm64', [StringComparison]::OrdinalIgnoreCase)) { + $isArm64Platform = $true + $i++ + continue + } + + if ($arg.Equals('/p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase) -or + $arg.Equals('-p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase)) { + $isArm64Platform = $true + continue + } + + $forwardArgs.Add($arg) +} + +if ($isArm64Platform) { + $useNativeTools = $false + + if (!$hasTargetArchitecture) { + $forwardArgs.Add('/p:TargetArchitecture=arm64') + } +} + +$buildScript = Join-Path $PSScriptRoot 'common\build.ps1' +$baseArgs = @() + +if ($useNativeTools) { + $baseArgs += '-NativeToolsOnMachine' +} + +$processArgs = @( + '-ExecutionPolicy' + 'ByPass' + '-NoProfile' + '-File' + $buildScript +) + $baseArgs + @( + '-restore' + '-build' + '-bl' +) + $forwardArgs + +& powershell @processArgs +exit $LASTEXITCODE diff --git a/src/BuildAssist/BuildAssist.msbuildproj b/src/BuildAssist/BuildAssist.msbuildproj index 2e188f76ff6..400c78f44e6 100644 --- a/src/BuildAssist/BuildAssist.msbuildproj +++ b/src/BuildAssist/BuildAssist.msbuildproj @@ -37,7 +37,7 @@ @@ -32,6 +36,18 @@ Namespace Microsoft.VisualBasic.ApplicationServices ''' Public Property ColorMode As SystemColorMode + ''' + ''' Setting this property inside the event handler determines the default + ''' for newly created top-level forms. + ''' + Public Property FormRevealMode As FormRevealMode + + ''' + ''' Setting this property inside the event handler determines the + ''' for the application. + ''' + Public Property VisualStylesMode As VisualStylesMode + ''' ''' Setting this property inside the event handler causes a ''' new default for Forms and UserControls to be set. @@ -59,5 +75,33 @@ Namespace Microsoft.VisualBasic.ApplicationServices Public Property MinimumSplashScreenDisplayTime As Integer = WindowsFormsApplicationBase.MinimumSplashExposureDefault + Friend Sub New(minimumSplashScreenDisplayTime As Integer, + highDpiMode As HighDpiMode, + colorMode As SystemColorMode, + formRevealMode As FormRevealMode) + + Me.MinimumSplashScreenDisplayTime = minimumSplashScreenDisplayTime + Me.HighDpiMode = highDpiMode + Me.ColorMode = colorMode + Me.FormRevealMode = formRevealMode + End Sub + + ''' + ''' Setting this property inside the event handler determines the default + ''' for newly created top-level forms. + ''' + Public Property FormRevealMode As FormRevealMode + + Friend Sub New(minimumSplashScreenDisplayTime As Integer, + highDpiMode As HighDpiMode, + colorMode As SystemColorMode, + visualStylesMode As VisualStylesMode) + + Me.MinimumSplashScreenDisplayTime = minimumSplashScreenDisplayTime + Me.HighDpiMode = highDpiMode + Me.ColorMode = colorMode + Me.VisualStylesMode = visualStylesMode + End Sub + End Class End Namespace diff --git a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb index 6e14d471534..2bd03879ed0 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb @@ -1,9 +1,8 @@ -' Licensed to the .NET Foundation under one or more agreements. +' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. Imports System.Collections.ObjectModel Imports System.ComponentModel -Imports System.Diagnostics.CodeAnalysis Imports System.IO.Pipes Imports System.Reflection Imports System.Runtime.CompilerServices @@ -11,7 +10,6 @@ Imports System.Runtime.InteropServices Imports System.Security Imports System.Threading Imports System.Windows.Forms -Imports System.Windows.Forms.Analyzers.Diagnostics Imports VbUtils = Microsoft.VisualBasic.CompilerServices.ExceptionUtils @@ -69,6 +67,12 @@ Namespace Microsoft.VisualBasic.ApplicationServices ' Note: We aim to expose this to the App Designer in later runtime/VS versions. Private _colorMode As SystemColorMode = SystemColorMode.Classic + ' The FormRevealMode the user assigned to the ApplyApplicationsDefault event. + Private _formRevealMode As FormRevealMode = FormRevealMode.Classic + + ' The VisualStylesMode (renderer version) the user assigned to the ApplyApplicationDefaults event. + Private _visualStylesMode As VisualStylesMode = VisualStylesMode.Classic + ' We only need to show the splash screen once. ' Protect the user from himself if they are overriding our app model. Private _didSplashScreen As Boolean @@ -200,6 +204,39 @@ Namespace Microsoft.VisualBasic.ApplicationServices End Set End Property + ''' + ''' Gets or sets the for the Application. + ''' + ''' + ''' The that newly created top-level forms use by + ''' default. + ''' + + Protected Property FormRevealMode As FormRevealMode + Get + Return _formRevealMode + End Get + Set(value As FormRevealMode) + _formRevealMode = value + End Set + End Property + + ''' + ''' Gets or sets the (renderer version) for the application. + ''' + ''' + ''' The that the application uses to render its controls. + ''' + + Protected Property VisualStylesMode As VisualStylesMode + Get + Return _visualStylesMode + End Get + Set(value As VisualStylesMode) + _visualStylesMode = value + End Set + End Property + ''' ''' Determines whether this application will use the XP Windows styles for windows, controls, etc. ''' @@ -734,7 +771,7 @@ Namespace Microsoft.VisualBasic.ApplicationServices ' in a derived class and setting `MyBase.MinimumSplashScreenDisplayTime` there. ' We are picking this (probably) changed value up, and pass it to the ApplyDefaultsEvents ' where it could be modified (again). So event wins over Override over default value (2 seconds). - ' b) We feed the defaults for HighDpiMode, ColorMode, VisualStylesMode to the EventArgs. + ' b) We feed the defaults for HighDpiMode, ColorMode, FormRevealMode, and VisualStylesMode to the EventArgs. ' With the introduction of the HighDpiMode property, we changed Project System the chance to reflect ' those default values in the App Designer UI and have it code-generated based on a modified ' Application.myapp, which would result it to be set in the derived constructor. @@ -746,11 +783,35 @@ Namespace Microsoft.VisualBasic.ApplicationServices Dim applicationDefaultsEventArgs As New ApplyApplicationDefaultsEventArgs( MinimumSplashScreenDisplayTime, HighDpiMode, - ColorMode) With + ColorMode, + FormRevealMode, + VisualStylesMode) With { .MinimumSplashScreenDisplayTime = MinimumSplashScreenDisplayTime } + ' Rationale for how we process the default values and how we let the user modify + ' them on demand via the ApplyApplicationDefaults event. + ' =========================================================================================== + ' a) Users used to be able to set MinimumSplashScreenDisplayTime _only_ by overriding OnInitialize + ' in a derived class and setting `MyBase.MinimumSplashScreenDisplayTime` there. + ' We are picking this (probably) changed value up, and pass it to the ApplyDefaultsEvents + ' where it could be modified (again). So event wins over Override over default value (2 seconds). + ' b) We feed the defaults for HighDpiMode, ColorMode, VisualStylesMode to the EventArgs. + ' With the introduction of the HighDpiMode property, we changed Project System the chance to reflect + ' those default values in the App Designer UI and have it code-generated based on a modified + ' Application.myapp, which would result it to be set in the derived constructor. + ' (See the hidden file in the Solution Explorer "My Project\Application.myapp\Application.Designer.vb + ' for how those UI-set values get applied.) + ' Once all this is done, we give the User another chance to change the value by code through + ' the ApplyDefaults event. + ' Note: Overriding MinimumSplashScreenDisplayTime needs still to keep working! + Dim applicationDefaultsEventArgs As New ApplyApplicationDefaultsEventArgs( + MinimumSplashScreenDisplayTime, + HighDpiMode, + ColorMode, + VisualStylesMode) + RaiseEvent ApplyApplicationDefaults(Me, applicationDefaultsEventArgs) If applicationDefaultsEventArgs.Font IsNot Nothing Then @@ -765,6 +826,8 @@ Namespace Microsoft.VisualBasic.ApplicationServices _highDpiMode = applicationDefaultsEventArgs.HighDpiMode _colorMode = applicationDefaultsEventArgs.ColorMode + _formRevealMode = applicationDefaultsEventArgs.FormRevealMode + _visualStylesMode = applicationDefaultsEventArgs.VisualStylesMode ' Then, it's applying what we got back as HighDpiMode. Dim dpiSetResult As Boolean = Application.SetHighDpiMode(_highDpiMode) @@ -780,7 +843,10 @@ Namespace Microsoft.VisualBasic.ApplicationServices Application.EnableVisualStyles() End If + Application.SetDefaultVisualStylesMode(_visualStylesMode) + Application.SetColorMode(_colorMode) + Application.SetDefaultFormRevealMode(_formRevealMode) ' We'll handle "/nosplash" for you. If Not (commandLineArgs.Contains("/nosplash") OrElse Me.CommandLineArgs.Contains("-nosplash")) Then @@ -1079,5 +1145,44 @@ Namespace Microsoft.VisualBasic.ApplicationServices End If ' Single-Instance application End Sub + ' The FormRevealMode the user assigned to the ApplyApplicationsDefault event. + Private _formRevealMode As FormRevealMode = FormRevealMode.Classic + + ''' + ''' Gets or sets the for the Application. + ''' + ''' + ''' The that newly created top-level forms use by + ''' default. + ''' + + Protected Property FormRevealMode As FormRevealMode + Get + Return _formRevealMode + End Get + Set(value As FormRevealMode) + _formRevealMode = value + End Set + End Property + + ' The VisualStylesMode (renderer version) the user assigned to the ApplyApplicationDefaults event. + Private _visualStylesMode As VisualStylesMode = VisualStylesMode.Classic + + ''' + ''' Gets or sets the (renderer version) for the application. + ''' + ''' + ''' The that the application uses to render its controls. + ''' + + Protected Property VisualStylesMode As VisualStylesMode + Get + Return _visualStylesMode + End Get + Set(value As VisualStylesMode) + _visualStylesMode = value + End Set + End Property + End Class End Namespace diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index e69de29bb2d..9cd44c0af70 100644 --- a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.FormRevealMode() -> System.Windows.Forms.FormRevealMode +Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.FormRevealMode(AutoPropertyValue As System.Windows.Forms.FormRevealMode) -> Void +Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.VisualStylesMode() -> System.Windows.Forms.VisualStylesMode +Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.VisualStylesMode(AutoPropertyValue As System.Windows.Forms.VisualStylesMode) -> Void +Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.FormRevealMode() -> System.Windows.Forms.FormRevealMode +Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.FormRevealMode(value As System.Windows.Forms.FormRevealMode) -> Void +Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.VisualStylesMode() -> System.Windows.Forms.VisualStylesMode +Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.VisualStylesMode(value As System.Windows.Forms.VisualStylesMode) -> Void diff --git a/src/Microsoft.VisualBasic.Forms/tests/UnitTests/System/Windows/Forms/WindowsFormsApplicationBaseTests.vb b/src/Microsoft.VisualBasic.Forms/tests/UnitTests/System/Windows/Forms/WindowsFormsApplicationBaseTests.vb index a03679253d7..701b5d4ca82 100644 --- a/src/Microsoft.VisualBasic.Forms/tests/UnitTests/System/Windows/Forms/WindowsFormsApplicationBaseTests.vb +++ b/src/Microsoft.VisualBasic.Forms/tests/UnitTests/System/Windows/Forms/WindowsFormsApplicationBaseTests.vb @@ -21,6 +21,8 @@ Namespace Microsoft.VisualBasic.Forms.Tests ColorMode = SystemColorMode.Dark ColorMode.Should.Be(SystemColorMode.Dark) + VisualStylesMode.Should.Be(VisualStylesMode.Classic) + EnableVisualStyles.Should.Be(False) EnableVisualStyles = True EnableVisualStyles.Should.Be(True) @@ -113,6 +115,31 @@ Namespace Microsoft.VisualBasic.Forms.Tests End If End Sub + + Public Sub OnInitialize_ApplyApplicationDefaults_VisualStylesModeFlowsToApplication() + If RemoteExecutor.IsSupported Then + Dim test As Action = + Sub() + Dim appModel As New SubWindowsFormsApplicationBase() With + { + .EnableVisualStylesCore = True + } + + AddHandler appModel.ApplyApplicationDefaults, + Sub(sender, e) + e.VisualStylesMode = VisualStylesMode.Latest + End Sub + + appModel.CallOnInitialize(Array.Empty(Of String)()).Should.BeTrue() + System.Windows.Forms.Application.DefaultVisualStylesMode.Should.Be(VisualStylesMode.Latest) + End Sub + + Using handle As RemoteInvokeHandle = RemoteExecutor.Invoke(test) + handle.ExitCode.Should.Be(RemoteExecutor.SuccessExitCode) + End Using + End If + End Sub + Public Sub ShowHideSplashScreenSuccess() Dim testCode As Action @@ -158,5 +185,19 @@ Namespace Microsoft.VisualBasic.Forms.Tests End If End Sub + Private NotInheritable Class SubWindowsFormsApplicationBase + Inherits WindowsFormsApplicationBase + + Public Function CallOnInitialize(commandLineArgs As String()) As Boolean + Return MyBase.OnInitialize(Array.AsReadOnly(commandLineArgs)) + End Function + + Public WriteOnly Property EnableVisualStylesCore As Boolean + Set(value As Boolean) + EnableVisualStyles = value + End Set + End Property + End Class + End Class End Namespace diff --git a/src/System.Private.Windows.Core/src/Microsoft.Private.Windows.Core.csproj b/src/System.Private.Windows.Core/src/Microsoft.Private.Windows.Core.csproj index 028ddf0be71..f63dbaa5496 100644 --- a/src/System.Private.Windows.Core/src/Microsoft.Private.Windows.Core.csproj +++ b/src/System.Private.Windows.Core/src/Microsoft.Private.Windows.Core.csproj @@ -58,6 +58,10 @@ + + + + diff --git a/src/System.Private.Windows.Core/src/NativeMethods.txt b/src/System.Private.Windows.Core/src/NativeMethods.txt index cc17c7c0a45..168abd11e0a 100644 --- a/src/System.Private.Windows.Core/src/NativeMethods.txt +++ b/src/System.Private.Windows.Core/src/NativeMethods.txt @@ -1,8 +1,7 @@ -BeginPaint BI_COMPRESSION -BitBlt BOOL -CallWindowProc +BeginPaint +BitBlt CFSTR_DROPDESCRIPTION CFSTR_FILENAME CFSTR_FILENAMEA @@ -10,12 +9,13 @@ CFSTR_INDRAGLOOP CLIPBOARD_FORMAT CLIPBRD_E_BAD_DATA CLIPBRD_E_CANT_OPEN -CloseEnhMetaFile CLR_* +CP_ACP +CallWindowProc +CloseEnhMetaFile CoCreateInstance CombineRgn CopyImage -CP_ACP CreateBitmap CreateCompatibleBitmap CreateCompatibleDC @@ -28,12 +28,8 @@ CreatePen CreateRectRgn CreateSolidBrush DATA_S_SAMEFORMATETC -DefWindowProc -DeleteDC -DeleteEnhMetaFile -DeleteObject -DestroyIcon DEVMODEW +DISPID_* DISP_E_ARRAYISLOCKED DISP_E_BADCALLEE DISP_E_BADINDEX @@ -52,23 +48,28 @@ DISP_E_TYPEMISMATCH DISP_E_UNKNOWNINTERFACE DISP_E_UNKNOWNLCID DISP_E_UNKNOWNNAME -DISPID_* DMORIENT_* -DoDragDrop -DragAcceptFiles DRAGDROP_E_ALREADYREGISTERED DRAGDROP_E_NOTREGISTERED DRAGDROP_S_CANCEL DRAGDROP_S_DROP DRAGDROP_S_USEDEFAULTCURSORS -DragQueryFile -DrawIconEx DROPDESCRIPTION DROPFILES DROPIMAGETYPE DSH_FLAGS DVASPECT DV_E_* +DefWindowProc +DeleteDC +DeleteEnhMetaFile +DeleteObject +DestroyIcon +DoDragDrop +DragAcceptFiles +DragQueryFile +DrawIconEx +EM_* E_ABORT E_ACCESSDENIED E_FAIL @@ -80,7 +81,6 @@ E_OUTOFMEMORY E_PENDING E_POINTER E_UNEXPECTED -EM_* EndPaint EnumChildWindows EnumDisplayMonitors @@ -88,15 +88,14 @@ EnumEnhMetaFile EnumThreadWindows EnumWindows FACILITY_CODE -fdex* FDEX_PROP_FLAGS FILETIME +GET_CLASS_LONG_INDEX GdiplusStartup GdiplusStartupInputEx -GET_CLASS_LONG_INDEX GetClientRect -GetClipboardFormatName GetClipRgn +GetClipboardFormatName GetCurrentThreadId GetDC GetDCEx @@ -119,6 +118,7 @@ GetSystemMetrics GetThreadLocale GetViewportExtEx GetViewportOrgEx +GetWindowDC GetWindowOrgEx GetWindowRect GetWindowText @@ -150,11 +150,12 @@ HINSTANCE HPEN HPROPSHEETPAGE HRGN +HSTRING HWND HWND_* +IDI_* IDataObject IDataObjectAsyncCapability -IDI_* IDispatchEx IDragSourceHelper2 IDropSource @@ -164,30 +165,32 @@ IDropTargetHelper IEnumFORMATETC IEnumUnknown IGlobalInterfaceTable -ImageFormat* -ImageLockMode +IInspectable INK_SERIALIZED_FORMAT INPLACE_E_NOTOOLSPACE -IntersectClipRect IPicture IPictureDisp IStream ITypeInfo ITypeLib IUnknown +ImageFormat* +ImageLockMode +IntersectClipRect LF_FACESIZE -LoadIcon -LoadRegTypeLib LPARAM LRESULT -MapWindowPoints +LoadIcon +LoadRegTypeLib MAX_PATH +MONITORINFOEXW +MONITORINFOF_* +MapWindowPoints MonitorFromPoint MonitorFromRect MonitorFromWindow -MONITORINFOEXW -MONITORINFOF_* MultiByteToWideChar +NCCALCSIZE_PARAMS NONCLIENTMETRICSW NS_E_WMP_CANNOT_FIND_FILE NS_E_WMP_DSHOW_UNSUPPORTED_FORMAT @@ -196,16 +199,16 @@ NS_E_WMP_LOGON_FAILURE NS_E_WMP_UNSUPPORTED_FORMAT NS_E_WMP_URLDOWNLOADFAILED NTSTATUS -OBJ_TYPE OBJECT_IDENTIFIER -OffsetViewportOrgEx +OBJ_TYPE +OLECMDERR_E_DISABLED +OLECMDERR_E_NOTSUPPORTED +OLECMDERR_E_UNKNOWNGROUP OLE_E_ADVISENOTSUPPORTED OLE_E_INVALIDRECT OLE_E_NOCONNECTION OLE_E_PROMPTSAVECANCELLED -OLECMDERR_E_DISABLED -OLECMDERR_E_NOTSUPPORTED -OLECMDERR_E_UNKNOWNGROUP +OffsetViewportOrgEx OleCreatePictureIndirect OleDuplicateData OleFlushClipboard @@ -213,37 +216,45 @@ OleGetClipboard OleInitialize OleSetClipboard OleUninitialize +POINTS +PRINTDLGEX_FLAGS +PWSTR PeekMessage PixelFormat* -POINTS PostMessage -PRINTDLGEX_FLAGS PropVariantClear -PWSTR -RealizePalette RECT -Rectangle REGDB_E_CLASSNOTREG +RPC_E_CHANGED_MODE +RPC_E_DISCONNECTED +RPC_E_SERVERFAULT +RPC_E_SYS_CALL_FAILED +RPC_STATUS +RealizePalette +Rectangle RegisterClipboardFormat RegisterDragDrop ReleaseDC ReleaseStgMedium RestoreDC RevokeDragDrop -RPC_E_CHANGED_MODE -RPC_E_DISCONNECTED -RPC_E_SERVERFAULT -RPC_E_SYS_CALL_FAILED -RPC_STATUS +RoActivateInstance +SAFEARRAY +START_PAGE_GENERAL +STATFLAG +STATUS_NO_MEMORY +STATUS_PENDING +STATUS_SUCCESS +STGTY +STG_E_* +STILL_ACTIVE S_FALSE S_OK -SAFEARRAY SafeArrayCreate SafeArrayCreateEx SafeArrayDestroy SafeArrayGetElement SafeArrayGetRecordInfo -SafeArrayGetRecordInfo SafeArrayGetVartype SafeArrayLock SafeArrayPutElement @@ -259,23 +270,18 @@ SetMapMode SetROP2 SetTextAlign SetTextColor -START_PAGE_GENERAL -STATFLAG -STATUS_NO_MEMORY -STATUS_PENDING -STATUS_SUCCESS -STG_E_* -STGTY -STILL_ACTIVE SystemParametersInfo SystemParametersInfoForDpi TYPE_E_BADMODULEKIND UNICODE_STRING_MAX_CHARS VIEW_E_DRAW -WideCharToMultiByte WIN32_ERROR WINCODEC_ERR_* WINDOW_LONG_PTR_INDEX -WindowFromDC WM_* -WPARAM \ No newline at end of file +WPARAM +WideCharToMultiByte +WindowFromDC +WindowsCreateString +WindowsDeleteString +fdex* diff --git a/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/IUISettings3.cs b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/IUISettings3.cs new file mode 100644 index 00000000000..15028e00045 --- /dev/null +++ b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/IUISettings3.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Windows.Win32.UI.ViewManagement; + +/// +/// WinRT ABI for Windows.UI.ViewManagement.IUISettings3. +/// +/// +/// +/// Manually defined as the type lives in WinRT metadata, not Win32 metadata, +/// and we do not want a CsWinRT projection dependency. Slots 3-5 are the +/// IInspectable methods. +/// +/// +internal unsafe struct IUISettings3 : IComIID +{ + private readonly void** _vtbl; + + // {03021BE4-5254-4781-8194-5168F7D06D7B} + public static Guid IID_Guid { get; } = new(0x03021be4, 0x5254, 0x4781, 0x81, 0x94, 0x51, 0x68, 0xf7, 0xd0, 0x6d, 0x7b); + + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + // 0x03021be4, 0x5254, 0x4781, 0x81, 0x94, 0x51, 0x68, 0xf7, 0xd0, 0x6d, 0x7b + 0xe4, 0x1b, 0x02, 0x03, 0x54, 0x52, 0x81, 0x47, 0x81, 0x94, 0x51, 0x68, 0xf7, 0xd0, 0x6d, 0x7b + ]; + + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IUISettings3* pThis = &this) + return ((delegate* unmanaged[Stdcall])_vtbl[0])(pThis, riid, ppvObject); + } + + public uint AddRef() + { + fixed (IUISettings3* pThis = &this) + return ((delegate* unmanaged[Stdcall])_vtbl[1])(pThis); + } + + public uint Release() + { + fixed (IUISettings3* pThis = &this) + return ((delegate* unmanaged[Stdcall])_vtbl[2])(pThis); + } + + // Slots 3-5: IInspectable::GetIids, GetRuntimeClassName, GetTrustLevel (unused). + + public HRESULT GetColorValue(UIColorType desiredColor, UIColor* value) + { + fixed (IUISettings3* pThis = &this) + return ((delegate* unmanaged[Stdcall])_vtbl[6])(pThis, desiredColor, value); + } +} diff --git a/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColor.cs b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColor.cs new file mode 100644 index 00000000000..94c3036ca5b --- /dev/null +++ b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColor.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Windows.Win32.UI.ViewManagement; + +/// +/// WinRT ABI for Windows.UI.Color, the value returned by +/// . +/// +/// +/// +/// Manually defined to mirror the WinRT ABI layout (four sequential bytes: alpha, red, green, blue) +/// without taking a CsWinRT projection dependency. +/// +/// +internal struct UIColor +{ + /// + /// The alpha channel of the color. + /// + public byte A; + + /// + /// The red channel of the color. + /// + public byte R; + + /// + /// The green channel of the color. + /// + public byte G; + + /// + /// The blue channel of the color. + /// + public byte B; +} diff --git a/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColorType.cs b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColorType.cs new file mode 100644 index 00000000000..9725700b888 --- /dev/null +++ b/src/System.Private.Windows.Core/src/Windows/Win32/UI/ViewManagement/UIColorType.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Windows.Win32.UI.ViewManagement; + +/// +/// WinRT ABI for Windows.UI.ViewManagement.UIColorType, identifying which system color to +/// retrieve through . +/// +/// +/// +/// Manually defined to mirror the WinRT enumeration without taking a CsWinRT projection dependency. +/// +/// +internal enum UIColorType +{ + /// + /// The background color. + /// + Background = 0, + + /// + /// The foreground color. + /// + Foreground = 1, + + /// + /// The darkest of the three accent shades. + /// + AccentDark3 = 2, + + /// + /// The second darkest accent shade. + /// + AccentDark2 = 3, + + /// + /// The lightest of the three dark accent shades. + /// + AccentDark1 = 4, + + /// + /// The base accent color. + /// + Accent = 5, + + /// + /// The darkest of the three light accent shades. + /// + AccentLight1 = 6, + + /// + /// The second lightest accent shade. + /// + AccentLight2 = 7, + + /// + /// The lightest of the three accent shades. + /// + AccentLight3 = 8, + + /// + /// The complement of the accent color. + /// + Complement = 9, +} diff --git a/src/System.Windows.Forms.Primitives/src/NativeMethods.txt b/src/System.Windows.Forms.Primitives/src/NativeMethods.txt index a56634792b1..af50671bda6 100644 --- a/src/System.Windows.Forms.Primitives/src/NativeMethods.txt +++ b/src/System.Windows.Forms.Primitives/src/NativeMethods.txt @@ -1,50 +1,52 @@ ACTCTX_FLAG_* +ADVF +ARW_* +AUTOCOMPLETEOPTIONS ActivateActCtx ActivateKeyboardLayout AdjustWindowRectEx AdjustWindowRectExForDpi -ADVF AreDpiAwarenessContextsEqual -ARW_* -AUTOCOMPLETEOPTIONS BFFM_* BIF_* BITMAP -BlockInput BM_* BN_* BROWSEINFOW BS_* -CallNextHookEx -CB_* +BeginDeferWindowPos +BlockInput CBN_* CBS_* +CB_* CCM_* CDM_GETSPEC CDRF_* CFE_EFFECTS CFM_MASK CHILDID_SELF -ChildWindowFromPointEx CHOOSECOLOR_FLAGS CHOOSEFONT_FLAGS CHOOSEFONT_FONT_TYPE CLIENTCREATESTRUCT -ClientToScreen CLIPBRD_E_BAD_DATA +CLSCTX +COLOR_* +COMDLG_FILTERSPEC +COPYDATASTRUCT +CSIDL_* +CW_USEDEFAULT +CallNextHookEx +ChildWindowFromPointEx +ClientToScreen ClipCursor CloseDesktop CloseHandle CloseThemeData -CLSCTX -cmb4 CoGetClassObject -COLOR_* -COMDLG_FILTERSPEC -CommandStateChangeConstants -CommDlgExtendedError -COPYDATASTRUCT CoRegisterMessageFilter +CommDlgExtendedError +CommandStateChangeConstants CreateAcceleratorTableW CreateActCtx CreateBrushIndirect @@ -56,31 +58,35 @@ CreatePatternBrush CreateRectRgn CreateStdAccessibleObject CreateWindowEx -CSIDL_* -CW_USEDEFAULT DATETIMEPICK_CLASS -DeactivateActCtx -DefFrameProc -DefMDIChildProc DESKTOP_ACCESS_FLAGS -DestroyAcceleratorTable -DestroyCursor -DestroyMenu -DestroyWindow -DFC_TYPE DFCS_STATE +DFC_TYPE DISPATCH_CONSTRUCT DISPATCH_FLAGS -DispatchMessage DISPPARAMS DLGC_* DOCHOSTUIINFO -DocumentProperties DPI_AWARENESS_CONTEXT_* DPI_HOSTING_BEHAVIOR +DRAWITEMSTRUCT +DTM_* +DTN_* +DTS_* +DWMWINDOWATTRIBUTE +DWM_WINDOW_CORNER_PREFERENCE +DeactivateActCtx +DefFrameProc +DefMDIChildProc +DeferWindowPos +DestroyAcceleratorTable +DestroyCursor +DestroyMenu +DestroyWindow +DispatchMessage +DocumentProperties DrawEdge DrawFrameControl -DRAWITEMSTRUCT DrawMenuBar DrawText DrawTextEx @@ -88,43 +94,44 @@ DrawThemeBackground DrawThemeEdge DrawThemeParentBackground DrawThemeText -DTM_* -DTN_* -DTS_* DuplicateHandle -DWM_WINDOW_CORNER_PREFERENCE DwmGetWindowAttribute DwmSetWindowAttribute -EC_* -ECO_* ECOOP_* +ECO_* +EC_* ELEMDESC -Ellipse +ENM_* EN_* +ES_* +EVENTMSG +EXTLOGFONTW +Ellipse EnableMenuItem EnableScrollBar EnableWindow +EndDeferWindowPos EndDialog -ENM_* EnumDisplaySettings -ES_* -EVENTMSG ExpandCollapseState -EXTLOGFONTW ExtTextOut FDAP -FillRect -FindExecutable FINDREPLACE_FLAGS -FindWindow FONTDESC -FormatMessage FUNCDESC FUNCFLAGS FUNCKIND +FillRect +FindExecutable +FindWindow +FormatMessage GDI_ERROR GDTR_* +GETPROPERTYSTOREFLAGS +GETTEXTEX_FLAGS +GETTEXTLENGTHEX_FLAGS GET_MODULE_HANDLE_EX_FLAG* +GMR_* GetActiveWindow GetAncestor GetAsyncKeyState @@ -160,10 +167,10 @@ GetErrorInfo GetExitCodeThread GetFocus GetHGlobalFromILockBytes +GetKeyState GetKeyboardLayout GetKeyboardLayoutList GetKeyboardState -GetKeyState GetLocaleInfoEx GetMapMode GetMenu @@ -180,9 +187,8 @@ GetPhysicalCursorPos GetProcAddress GetProcessDpiAwareness GetProcessWindowStation -GETPROPERTYSTOREFLAGS -GetRgnBox GetROP2 +GetRgnBox GetScrollInfo GetShortPathName GetStartupInfo @@ -192,9 +198,7 @@ GetSystemPaletteEntries GetSystemPowerStatus GetTextAlign GetTextColor -GETTEXTEX_FLAGS GetTextExtentPoint32W -GETTEXTLENGTHEX_FLAGS GetTextMetrics GetThemeAppProperties GetThemeBackgroundContentRect @@ -227,11 +231,10 @@ GetWindowDisplayAffinity GetWindowDpiAwarenessContext GetWindowPlacement GetWindowRgn -GMR_* HC_* HDHITTESTINFO -HDI_MASK HDITEMW +HDI_MASK HDLAYOUT HDM_* HDN_* @@ -241,21 +244,20 @@ HFONT HH_AKLINK HH_FTS_QUERY HH_POPUP -HideCaret -HitTestThemeBackground HT* HTML_HELP_COMMAND -HtmlHelp HTREEITEM +HideCaret +HitTestThemeBackground +HtmlHelp IAccessible IAccessibleEx IAutoComplete2 -IClassFactory -IClassFactory2 -IClassFactory2 ICM_* ICM_MODE ICON_* +IClassFactory +IClassFactory2 IConnectionPoint IConnectionPointContainer IDC_* @@ -287,40 +289,11 @@ IHTMLWindow4 IInvokeProvider ILegacyIAccessibleProvider IMAGE_LIST_WRITE_STREAM_FLAGS -ImageList_Add -ImageList_Create -ImageList_Draw -ImageList_DrawEx -ImageList_Duplicate -ImageList_GetIconSize -ImageList_GetImageCount -ImageList_GetImageInfo -ImageList_Read -ImageList_Remove -ImageList_Replace -ImageList_ReplaceIcon -ImageList_SetBkColor -ImageList_Write -ImageList_WriteEx IME_COMPOSITION_STRING -ImmAssociateContext -ImmCreateContext -ImmGetContext -ImmGetConversionStatus -ImmGetOpenStatus -ImmNotifyIME -ImmReleaseContext -ImmSetConversionStatus -ImmSetOpenStatus IMN_* IMPLTYPEFLAGS IMultipleViewProvider -InitCommonControls -InitCommonControlsEx INITCOMMONCONTROLSEX_ICC -IntersectClipRect -InvalidateRect -InvalidateRgn IOleCommandTarget IOleControl IOleControlSite @@ -346,28 +319,15 @@ IRawElementProviderHwndOverride IRecordInfo IRichEditOle IRichEditOleCallback -IsAccelerator -IsAppThemed -IsChild IScrollItemProvider IScrollProvider -IsDialogMessage ISelectionItemProvider ISelectionProvider IServiceProvider IShellItem ISimpleFrameSite ISpecifyPropertyPages -IsProcessDPIAware -IsThemeBackgroundPartiallyTransparent -IsThemePartDefined ISupportErrorInfo -IsValidDpiAwarenessContext -IsWindow -IsWindowEnabled -IsWindowUnicode -IsWindowVisible -IsZoomed ITableItemProvider ITableProvider ITextDocument @@ -377,18 +337,59 @@ ITextRangeProvider IToggleProvider IUIAutomation IUIAutomationElement -IValueProvider IVBFormat IVBGetControl +IValueProvider IViewObject IViewObject2 IWebBrowser2 +ImageList_Add +ImageList_Create +ImageList_Draw +ImageList_DrawEx +ImageList_Duplicate +ImageList_GetIconSize +ImageList_GetImageCount +ImageList_GetImageInfo +ImageList_Read +ImageList_Remove +ImageList_Replace +ImageList_ReplaceIcon +ImageList_SetBkColor +ImageList_Write +ImageList_WriteEx +ImmAssociateContext +ImmCreateContext +ImmGetContext +ImmGetConversionStatus +ImmGetOpenStatus +ImmNotifyIME +ImmReleaseContext +ImmSetConversionStatus +ImmSetOpenStatus +InitCommonControls +InitCommonControlsEx +IntersectClipRect +InvalidateRect +InvalidateRgn +IsAccelerator +IsAppThemed +IsChild +IsDialogMessage +IsProcessDPIAware +IsThemeBackgroundPartiallyTransparent +IsThemePartDefined +IsValidDpiAwarenessContext +IsWindow +IsWindowEnabled +IsWindowUnicode +IsWindowVisible +IsZoomed KF_* KillTimer -LB_* LBN_* LBS_* -LineTo +LB_* LIST_ITEM_FLAGS LIST_ITEM_STATE_FLAGS LIST_VIEW_BACKGROUND_IMAGE_FLAGS @@ -396,9 +397,6 @@ LIST_VIEW_GROUP_ALIGN_FLAGS LIST_VIEW_GROUP_STATE_FLAGS LIST_VIEW_ITEM_FLAGS LIST_VIEW_ITEM_STATE_FLAGS -LoadCursor -LoadLibraryEx -LoadTypeLib LOCALE_IMEASURE LOCALE_NAME_SYSTEM_DEFAULT LOCALE_TRANSIENT_KEYBOARD1 @@ -408,8 +406,6 @@ LOCALE_TRANSIENT_KEYBOARD4 LOGPALETTE LOGPEN LPtoDP -LresultFromObject -LV_VIEW_* LVA_* LVBKIMAGEW LVCOLUMNW @@ -423,17 +419,22 @@ LVHITTESTINFO LVINSERTMARK LVIR_* LVM_* -LVN_* LVNI_* -LVS_* +LVN_* LVSCW_* LVSIL_* +LVS_* LVTILEVIEWINFO LVTILEVIEWINFO_FLAGS LVTILEVIEWINFO_MASK -MA_* -MapVirtualKey +LV_VIEW_* +LineTo +LoadCursor +LoadLibraryEx +LoadTypeLib +LresultFromObject MAX_TAB_STOPS +MA_* MCGRIDINFO MCGRIDINFO_FLAGS MCGRIDINFO_PART @@ -441,27 +442,25 @@ MCHITTESTINFO MCHITTESTINFO_HIT_FLAGS MCM_* MCN_* -MCS_* MCSC_* +MCS_* MEASUREITEMSTRUCT MEMBERID_NIL -MessageBeep -MessageBox MESSAGEBOX_RESULT MINIMIZEDMETRICS MINMAXINFO MODIFY_WORLD_TRANSFORM_MODE -MONTH_CALDENDAR_MESSAGES_VIEW MONTHCAL_CLASS +MONTH_CALDENDAR_MESSAGES_VIEW MOUSEHOOKSTRUCT -MoveToEx MSFTEDIT_CLASS +MapVirtualKey +MessageBeep +MessageBox +MoveToEx MsgWaitForMultipleObjectsEx -NavigateDirection NFR_* NIN_* -NM_* -NM_TREEVIEW_ACTION NMCUSTOMDRAW NMDATETIMECHANGE NMDATETIMECHANGE_FLAGS @@ -474,8 +473,8 @@ NMLVCUSTOMDRAW NMLVCUSTOMDRAW_ITEM_TYPE NMLVDISPINFOW NMLVFINDITEMW -NMLVGETINFOTIP_FLAGS NMLVGETINFOTIPW +NMLVGETINFOTIP_FLAGS NMLVKEYDOWN NMLVLINK NMLVODSTATECHANGE @@ -485,55 +484,80 @@ NMTTDISPINFOW NMTVCUSTOMDRAW NMTVDISPINFOW NMVIEWCHANGE +NM_* +NM_TREEVIEW_ACTION NOTIFY_ICON_DATA_FLAGS NOTIFY_ICON_INFOTIP_FLAGS NOTIFY_ICON_MESSAGE +NavigateDirection NotifyWinEvent -OemKeyScan OLECLOSE OLECMDEXECOPT OLECMDF OLECMDID OLECONTF +OPEN_FILENAME_FLAGS +OPEN_FILENAME_FLAGS_EX +OemKeyScan OleCreateFontIndirect OleCreatePropertyFrame OleCreatePropertyFrameIndirect -OPEN_FILENAME_FLAGS -OPEN_FILENAME_FLAGS_EX OpenInputDesktop OpenThemeData PAGESETUPDLG_FLAGS PARAFORMAT_NUMBERING -PatBlt PBM_* -PBS_* PBST_* +PBS_* PD_RESULT_* -PostQuitMessage -PostQuitMessage -PostThreadMessage PRF_* PROGRESS_CLASS PROPERTYKEY +PatBlt +PostQuitMessage +PostThreadMessage +PowerClearRequest +PowerCreateRequest +PowerSetRequest ProviderOptions -ReadClassStg READYSTATE -RedrawWindow REGDB_E_CLASSNOTREG +RICH_EDIT_GET_CONTEXT_MENU_SEL_TYPE +ReadClassStg +RedrawWindow +RegLoadMUIString RegisterClass RegisterWindowMessage -RegLoadMUIString ReleaseCapture ReleaseDC -RICH_EDIT_GET_CONTEXT_MENU_SEL_TYPE RoundRect RowOrColumnMajor -SC_* -SC_* -SCF_* SCF_* -ScreenToClient SCROLLBAR_COMMAND +SC_* +SFF_* +SF_* +SHAutoComplete +SHBrowseForFolder +SHCreateItemFromParsingName +SHCreateShellItem +SHDRAGIMAGE +SHGetKnownFolderPath +SHGetPathFromIDListEx +SHGetSpecialFolderLocation +SHParseDisplayName +SIATTRIBFLAGS +SIGDN +SIZE_* +START_PAGE_GENERAL +STATIC_STYLES +STATUSCLASSNAME +STGC +STGMEDIUM +STGTY +SYSBUTTONSTATES +SYSTEMTIME +ScreenToClient ScrollWindow ScrollWindowEx SendDlgItemMessage @@ -562,60 +586,33 @@ SetThreadDpiHostingBehavior SetTimer SetViewportExtEx SetViewportOrgEx +SetWinEventHook SetWindowDisplayAffinity SetWindowExtEx SetWindowOrgEx SetWindowPlacement SetWindowPos SetWindowRgn -SetWindowsHookEx SetWindowText SetWindowTheme -SetWinEventHook -SF_* -SF_* -SFF_* -SFF_* -SHAutoComplete -SHBrowseForFolder -SHCreateItemFromParsingName -SHCreateShellItem -SHDRAGIMAGE +SetWindowsHookEx ShellExecute -SHGetKnownFolderPath -SHGetPathFromIDListEx -SHGetSpecialFolderLocation -SHGetSpecialFolderLocation ShowCaret ShowCursor ShowWindow -SHParseDisplayName -SIATTRIBFLAGS -SIGDN -SIZE_* -START_PAGE_GENERAL -STATIC_STYLES -STATUSCLASSNAME -stc4 -STGC StgCreateDocfileOnILockBytes -STGMEDIUM StgOpenStorageOnILockBytes -STGTY StretchDIBits StructureChangeType -SYSBUTTONSTATES -SYSTEMTIME TAB_CONTROL_ITEM_STATE TASKDIALOG_ELEMENTS TASKDIALOG_FLAGS TASKDIALOG_ICON_ELEMENTS TASKDIALOG_MESSAGES TASKDIALOG_NOTIFICATIONS -TaskDialogIndirect -TB_* TBM_* TBS_* +TB_* TCITEMHEADERA_MASK TCITEMW TCM_* @@ -623,14 +620,9 @@ TCN_* TCS_* THEME_PROPERTY_SYMBOL_ID TILE_WINDOWS_HOW -ToggleState -tomConstants -TOOLTIP_FLAGS TOOLTIPS_CLASS +TOOLTIP_FLAGS TRACKBAR_CLASS -TrackMouseEvent -TranslateMDISysAccel -TranslateMessage TREE_VIEW_ITEM_STATE_FLAGS TTDT_* TTM_* @@ -639,19 +631,28 @@ TTS_* TTTOOLINFOW TVGN_* TVHITTESTINFO -TVI_* TVINSERTSTRUCTW -TVITEM_MASK TVITEMW +TVITEM_MASK +TVI_* TVM_* TVN_* -TVS_* TVSIL_* +TVS_* TYPEATTR TYPEDESC TYPEKIND +TaskDialogIndirect +ToggleState +TrackMouseEvent +TranslateMDISysAccel +TranslateMessage UIA_CONTROLTYPE_ID UIA_EVENT_ID +UISF_* +UIS_* +USERCLASSTYPE +USEROBJECTFLAGS UiaAppendRuntimeId UiaClientsAreListening UiaDisconnectProvider @@ -663,24 +664,19 @@ UiaRaiseNotificationEvent UiaRaiseStructureChangedEvent UiaReturnRawElementProvider UiaRootObjectId -UIS_* -UISF_* UnhookWinEvent UpdateWindow -USERCLASSTYPE -USEROBJECTFLAGS -ValidateRect VARDESC VARENUM VARFLAGS -VarFormat VARIANT VARIANT_* VARKIND VIRTUAL_KEY +ValidateRect +VarFormat VkKeyScan WA_* -WaitMessage WC_BUTTON WC_COMBOBOX WC_EDIT @@ -691,10 +687,16 @@ WC_STATIC WC_TABCONTROL WC_TREEVIEW WHEEL_DELTA -WindowFromPoint WINDOWPOS WINEVENT_INCONTEXT WSF_VISIBLE +WTSRegisterSessionNotification +WTSUnRegisterSessionNotification +WaitMessage +WindowFromPoint XBUTTON1 XBUTTON2 -XFORMCOORDS \ No newline at end of file +XFORMCOORDS +cmb4 +stc4 +tomConstants diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimer.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimer.cs new file mode 100644 index 00000000000..6499e654a72 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimer.cs @@ -0,0 +1,335 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace System.Windows.Forms.Animation; + +/// +/// A high-precision static timer designed to trigger WinForms control animations +/// at 60 Hz (or 30 Hz on systems without high-resolution timer support). +/// Controls register callbacks that are marshalled back to the UI thread via +/// the captured . +/// +internal static partial class HighPrecisionTimer +{ + // Target frame intervals. + private const double TargetFrameTimeMs60Hz = 16.667; + private const double TargetFrameTimeMs30Hz = 33.333; + + // We aim the PeriodicTimer earlier than the target frame time to allow + // for spin-wait refinement to hit the target precisely. + private const double TimerTickMs60Hz = 14.0; + private const double TimerTickMs30Hz = 30.0; + + // Maximum drift before we assert (20% of target frame time sustained over 10 frames). + private const int MaxDriftFrames = 10; + private const double MaxDriftThresholdRatio = 0.20; + + private static readonly Lock s_lock = new(); + private static readonly ConcurrentDictionary s_registrations = new(); + private static long s_nextId; + private static CancellationTokenSource? s_cts; + private static Task? s_loopTask; + private static bool s_highResolutionAvailable; + private static double s_targetFrameTimeMs; + private static double s_timerTickMs; + + /// + /// Gets the current target frame time in milliseconds. + /// + internal static double TargetFrameTimeMs => s_targetFrameTimeMs; + + /// + /// Gets whether high-resolution timing (60 Hz) is available on this system. + /// + internal static bool IsHighResolutionAvailable => s_highResolutionAvailable; + + /// + /// Registers a callback to be invoked on each animation frame tick. + /// The current is captured and used + /// to marshal the callback to the appropriate thread. + /// + /// + /// The async callback invoked each frame. Receives timing information and a cancellation token. + /// + /// A that must be disposed to unregister. + /// + /// Thrown when no is available on the current thread. + /// + internal static TimerRegistration Register(Func callback) + { + ArgumentNullException.ThrowIfNull(callback); + + SynchronizationContext? syncContext = SynchronizationContext.Current + ?? throw new InvalidOperationException( + "A SynchronizationContext must be available on the calling thread. " + + "Ensure registration is performed from a UI thread."); + + long id = Interlocked.Increment(ref s_nextId); + Registration registration = new(id, callback, syncContext); + s_registrations.TryAdd(id, registration); + + EnsureRunning(); + + return new TimerRegistration(id); + } + + /// + /// Unregisters a previously registered callback. + /// + internal static void Unregister(long registrationId) + { + s_registrations.TryRemove(registrationId, out _); + + if (s_registrations.IsEmpty) + { + StopTimer(); + } + } + + private static void EnsureRunning() + { + lock (s_lock) + { + if (s_loopTask is not null) + { + return; + } + + s_highResolutionAvailable = TrySetHighResolutionTimerMode(); + s_targetFrameTimeMs = s_highResolutionAvailable ? TargetFrameTimeMs60Hz : TargetFrameTimeMs30Hz; + s_timerTickMs = s_highResolutionAvailable ? TimerTickMs60Hz : TimerTickMs30Hz; + + s_cts = new CancellationTokenSource(); + CancellationToken cancellationToken = s_cts.Token; + s_loopTask = Task.Factory.StartNew( + () => TimerLoopAsync(cancellationToken), + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default).Unwrap(); + } + } + + private static void StopTimer() + { + CancellationTokenSource? cts; + + lock (s_lock) + { + cts = s_cts; + s_cts = null; + s_loopTask = null; + } + + if (cts is not null) + { + cts.Cancel(); + cts.Dispose(); + } + + // Best-effort: restore timer resolution. + if (s_highResolutionAvailable) + { + ResetTimerResolution(); + } + } + + private static async Task TimerLoopAsync(CancellationToken cancellationToken) + { + using PeriodicTimer periodicTimer = new(TimeSpan.FromMilliseconds(s_timerTickMs)); + Stopwatch stopwatch = Stopwatch.StartNew(); + long lastTickTimestamp = 0; + int consecutiveDriftFrames = 0; + + try + { + while (await periodicTimer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + // Spin-wait refinement: if we woke up early, spin until target time. + double targetMs = lastTickTimestamp + s_targetFrameTimeMs; + SpinToTarget(stopwatch, targetMs); + + long currentTimestamp = stopwatch.ElapsedMilliseconds; + double elapsed = currentTimestamp - lastTickTimestamp; + + // Drift detection: check if we are consistently overshooting. + double drift = elapsed - s_targetFrameTimeMs; + if (Math.Abs(drift) > s_targetFrameTimeMs * MaxDriftThresholdRatio) + { + consecutiveDriftFrames++; + Debug.Assert( + consecutiveDriftFrames < MaxDriftFrames, + $"HighPrecisionTimer: Excessive drift detected. " + + $"Drift: {drift:F2}ms over {consecutiveDriftFrames} consecutive frames."); + } + else + { + consecutiveDriftFrames = 0; + } + + lastTickTimestamp = currentTimestamp; + + // Dispatch to all registered callbacks. + DispatchCallbacks( + TimeSpan.FromMilliseconds(currentTimestamp), + TimeSpan.FromMilliseconds(elapsed), + cancellationToken); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Normal shutdown. + } + } + + private static void SpinToTarget(Stopwatch stopwatch, double targetMs) + { + SpinWait spinner = default; + + while (stopwatch.Elapsed.TotalMilliseconds < targetMs) + { + // SpinOnce with sleep1Threshold=-1 ensures we stay in PAUSE/yield + // mode and never escalate to Thread.Sleep(1). + spinner.SpinOnce(sleep1Threshold: -1); + } + } + + private static void DispatchCallbacks( + TimeSpan timestamp, + TimeSpan elapsed, + CancellationToken cancellationToken) + { + foreach (KeyValuePair kvp in s_registrations) + { + Registration registration = kvp.Value; + + // Skip if previous callback is still in flight (frame coalescing). + if (Interlocked.CompareExchange(ref registration.InFlight, 1, 0) != 0) + { + Interlocked.Increment(ref registration.DroppedFrames); + continue; + } + + long frameIndex = Interlocked.Increment(ref registration.FrameIndex) - 1; + int dropped = Interlocked.Exchange(ref registration.DroppedFrames, 0); + + HighPrecisionTimerTick tick = new() + { + Timestamp = timestamp, + Elapsed = elapsed, + DroppedFrames = dropped, + FrameIndex = frameIndex + }; + + registration.SyncContext.Post( + _ => _ = InvokeCallbackAsync(registration, tick, cancellationToken), + null); + } + } + + private static async Task InvokeCallbackAsync( + Registration registration, + HighPrecisionTimerTick tick, + CancellationToken cancellationToken) + { + try + { + await registration.Callback(tick, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Cancellation while the timer is shutting down is a benign, expected outcome. Swallow it + // here so it does not fault this fire-and-forget task into an unobserved task exception. + } + catch (Exception ex) + { + Debug.Fail($"HighPrecisionTimer: Unhandled exception in callback: {ex.Message}"); + } + finally + { + Interlocked.Exchange(ref registration.InFlight, 0); + } + } + + [SupportedOSPlatform("windows10.0.17134.0")] + private static bool TrySetHighResolutionTimerMode() + { + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134)) + { + return false; + } + + try + { + // Use timeBeginPeriod for documented, reliable high-resolution timing. + return NativeMethods.TimeBeginPeriod(1) == 0; // TIMERR_NOERROR + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + return false; + } + } + + private static void ResetTimerResolution() + { + try + { + NativeMethods.TimeEndPeriod(1); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + // Best effort. + } + } + + private static partial class NativeMethods + { + [LibraryImport("winmm.dll")] + internal static partial int TimeBeginPeriod(int uPeriod); + + [LibraryImport("winmm.dll")] + internal static partial int TimeEndPeriod(int uPeriod); + } + + private sealed class Registration( + long id, + Func callback, + SynchronizationContext syncContext) + { + public long Id { get; } = id; + public Func Callback { get; } = callback; + public SynchronizationContext SyncContext { get; } = syncContext; + public int InFlight; + public int DroppedFrames; + public long FrameIndex; + } + + /// + /// Represents a timer registration. Dispose to unregister. + /// + internal readonly struct TimerRegistration : IDisposable + { + private readonly long _id; + + internal TimerRegistration(long id) => _id = id; + + /// Gets the registration identifier. + public long Id => _id; + + /// Unregisters this callback from the timer. + public void Dispose() => Unregister(_id); + } + + /// + /// Resets internal state. For testing purposes only. + /// + internal static void Reset() + { + StopTimer(); + s_registrations.Clear(); + s_nextId = 0; + } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimerTick.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimerTick.cs new file mode 100644 index 00000000000..3b6557e2643 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Animation/HighPrecisionTimerTick.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Windows.Forms.Animation; + +/// +/// Provides timing information for a single animation frame tick. +/// +internal readonly struct HighPrecisionTimerTick +{ + /// + /// The absolute timestamp of this tick from the timer's epoch. + /// + public TimeSpan Timestamp { get; init; } + + /// + /// The elapsed time since the last tick delivered to this registration. + /// + public TimeSpan Elapsed { get; init; } + + /// + /// The number of frames that were dropped (coalesced) since the last delivered tick. + /// + public int DroppedFrames { get; init; } + + /// + /// The zero-based frame index for this registration. + /// + public long FrameIndex { get; init; } +} diff --git a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs index 764a35ca3a1..be410c24340 100644 --- a/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs +++ b/src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/ScaleHelper.cs @@ -30,6 +30,12 @@ internal static partial class ScaleHelper private static bool s_processPerMonitorAware; private static Size? s_logicalSmallSystemIconSize; + private const string AccessibilityRegistryKeyPath = "Software\\Microsoft\\Accessibility"; + private const string TextScaleFactorValueName = "TextScaleFactor"; + private const int MinSystemTextScalePercent = 100; + private const int MaxSystemTextScalePercent = 225; + private const double DefaultSystemTextScaleFactor = 1.0; + /// /// The initial primary monitor DPI (logical pixels per inch) for the process. /// @@ -227,20 +233,50 @@ internal static Bitmap ScaleToDpi(Bitmap logicalBitmap, int dpi, bool disposeBit return null; } - // The default(100) and max(225) text scale factor is value what Settings display text scale - // applies and also clamps the text scale factor value between 100 and 225 value. - // See https://docs.microsoft.com/windows/uwp/design/input/text-scaling. - const int MinTextScaleValue = 100; - const int MaxTextScaleValue = 225; + if (!TryGetSystemTextScaleFactor(out double textScaleFactor) || textScaleFactor == DefaultSystemTextScaleFactor) + { + return null; + } + + return font.WithSize(font.Size * (float)textScaleFactor); + } + + /// + /// Gets the current Windows Accessibility text-scale factor as a multiplier. + /// + /// + /// + /// Returns 1.0 when text scaling is unsupported or when the setting cannot be read. + /// + /// + internal static double GetSystemTextScaleFactor() + => TryGetSystemTextScaleFactor(out double textScaleFactor) ? textScaleFactor : DefaultSystemTextScaleFactor; + + /// + /// Attempts to get the current Windows Accessibility text-scale factor as a multiplier. + /// + /// The current text-scale factor. + /// + /// if the value was read successfully; otherwise, . + /// + internal static bool TryGetSystemTextScaleFactor(out double textScaleFactor) + { + textScaleFactor = DefaultSystemTextScaleFactor; + + if (!OsVersion.IsWindows10_1507OrGreater()) + { + return false; + } try { - // Retrieve the text scale factor, which is set via Settings > Display > Make Text Bigger. - using RegistryKey? key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Accessibility"); - if (key is not null && key.GetValue("TextScaleFactor") is int textScale) + // Retrieve the text scale factor, which is set via Settings > Accessibility > Text size. + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(AccessibilityRegistryKeyPath); + if (key is not null && key.GetValue(TextScaleFactorValueName) is int textScale) { - textScale = Math.Clamp(textScale, MinTextScaleValue, MaxTextScaleValue); - return textScale == 100 ? null : font.WithSize(font.Size * (textScale / 100.0f)); + textScale = Math.Clamp(textScale, MinSystemTextScalePercent, MaxSystemTextScalePercent); + textScaleFactor = textScale / 100.0; + return true; } } catch @@ -251,7 +287,7 @@ internal static Bitmap ScaleToDpi(Bitmap logicalBitmap, int dpi, bool disposeBit #endif } - return null; + return false; } /// diff --git a/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Animation/HighPrecisionTimerTests.cs b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Animation/HighPrecisionTimerTests.cs new file mode 100644 index 00000000000..89d031dc1ca --- /dev/null +++ b/src/System.Windows.Forms.Primitives/tests/UnitTests/System/Windows/Forms/Animation/HighPrecisionTimerTests.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Windows.Forms.Animation; + +namespace System.Windows.Forms.Primitives.Tests.Animation; + +/// +/// A test synchronization context that executes posted callbacks immediately +/// on the thread pool, simulating a UI message pump for testing purposes. +/// +internal sealed class TestSynchronizationContext : SynchronizationContext +{ + public override void Post(SendOrPostCallback d, object? state) => ThreadPool.QueueUserWorkItem(_ => d(state)); + + public override void Send(SendOrPostCallback d, object? state) => d(state); +} + +// The timer is process-wide static; disable parallelization so timing-sensitive +// assertions are not perturbed by concurrently running tests. +[Collection(nameof(HighPrecisionTimerTests))] +[CollectionDefinition(nameof(HighPrecisionTimerTests), DisableParallelization = true)] +public sealed class HighPrecisionTimerTests : IDisposable +{ + private readonly SynchronizationContext? _originalContext; + + public HighPrecisionTimerTests() + { + _originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(new TestSynchronizationContext()); + } + + public void Dispose() + { + HighPrecisionTimer.Reset(); + SynchronizationContext.SetSynchronizationContext(_originalContext); + } + + [Fact] + public async Task SingleConsumer_ReceivesTicksAtApproximatelyExpectedRate() + { + ConcurrentBag intervals = []; + Stopwatch stopwatch = Stopwatch.StartNew(); + double lastTick = 0; + int tickCount = 0; + const int TargetTicks = 30; + + using HighPrecisionTimer.TimerRegistration registration = HighPrecisionTimer.Register( + (tick, ct) => + { + double now = stopwatch.Elapsed.TotalMilliseconds; + if (lastTick > 0) + { + intervals.Add(now - lastTick); + } + + lastTick = now; + Interlocked.Increment(ref tickCount); + return ValueTask.CompletedTask; + }); + + await WaitForAsync(() => tickCount >= TargetTicks); + + List sorted = [.. intervals.OrderBy(x => x)]; + double targetMs = HighPrecisionTimer.TargetFrameTimeMs; + + // Relaxed bounds to remain robust on loaded CI machines: the median must be in a + // sane band around the target frame time. + double median = Percentile(sorted, 0.50); + median.Should().BeLessThan(targetMs * 3.0, "the median frame interval should stay near the target"); + } + + [Fact] + public async Task MultipleConsumers_AllReceiveTicksIndependently() + { + const int ConsumerCount = 5; + const int TargetTicks = 15; + int[] tickCounts = new int[ConsumerCount]; + HighPrecisionTimer.TimerRegistration[] registrations = new HighPrecisionTimer.TimerRegistration[ConsumerCount]; + + for (int i = 0; i < ConsumerCount; i++) + { + int index = i; + registrations[i] = HighPrecisionTimer.Register( + (tick, ct) => + { + Interlocked.Increment(ref tickCounts[index]); + return ValueTask.CompletedTask; + }); + } + + await WaitForAsync(() => tickCounts.Min() >= TargetTicks); + + foreach (HighPrecisionTimer.TimerRegistration registration in registrations) + { + registration.Dispose(); + } + + tickCounts.Should().OnlyContain(count => count >= TargetTicks); + } + + [Fact] + public async Task SlowConsumer_DropsFramesInsteadOfQueuing() + { + ConcurrentBag ticks = []; + + using HighPrecisionTimer.TimerRegistration registration = HighPrecisionTimer.Register( + async (tick, ct) => + { + ticks.Add(tick); + // Simulate slow rendering (well over one frame time). + await Task.Delay((int)(HighPrecisionTimer.TargetFrameTimeMs * 3), ct).ConfigureAwait(false); + }); + + await WaitForAsync(() => ticks.Sum(t => t.DroppedFrames) > 0, timeoutMs: 4000); + + ticks.Sum(t => t.DroppedFrames).Should().BeGreaterThan(0, "a slow consumer should report dropped frames"); + } + + [Fact] + public async Task Registration_Disposal_StopsCallbacks() + { + int tickCount = 0; + + HighPrecisionTimer.TimerRegistration registration = HighPrecisionTimer.Register( + (tick, ct) => + { + Interlocked.Increment(ref tickCount); + return ValueTask.CompletedTask; + }); + + await WaitForAsync(() => tickCount > 0); + registration.Dispose(); + int ticksAfterDispose = Volatile.Read(ref tickCount); + + await Task.Delay(200, TestContext.Current.CancellationToken); + + // At most a couple of in-flight callbacks may land right after disposal. + (Volatile.Read(ref tickCount) - ticksAfterDispose).Should().BeLessThanOrEqualTo(2); + } + + [Fact] + public void Registration_WithoutSyncContext_Throws() + { + SynchronizationContext? original = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + + try + { + Action act = () => HighPrecisionTimer.Register((tick, ct) => ValueTask.CompletedTask); + act.Should().Throw(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(original); + } + } + + [Fact] + public async Task TimerTick_ProvidesElapsedAndIncreasingFrameIndex() + { + ConcurrentBag elapsedValues = []; + long lastFrameIndex = -1; + bool frameIndexMonotonic = true; + int tickCount = 0; + const int TargetTicks = 15; + + using HighPrecisionTimer.TimerRegistration registration = HighPrecisionTimer.Register( + (tick, ct) => + { + if (tick.FrameIndex <= lastFrameIndex) + { + frameIndexMonotonic = false; + } + + lastFrameIndex = tick.FrameIndex; + + if (tick.FrameIndex > 0) + { + elapsedValues.Add(tick.Elapsed.TotalMilliseconds); + } + + Interlocked.Increment(ref tickCount); + return ValueTask.CompletedTask; + }); + + await WaitForAsync(() => tickCount >= TargetTicks); + + frameIndexMonotonic.Should().BeTrue("frame indices should increase monotonically"); + elapsedValues.Should().NotBeEmpty(); + elapsedValues.Should().OnlyContain(value => value > 0, "elapsed time between ticks should be positive"); + } + + private static double Percentile(List sortedValues, double percentile) + { + if (sortedValues.Count == 0) + { + return 0; + } + + int index = (int)Math.Ceiling(percentile * sortedValues.Count) - 1; + return sortedValues[Math.Max(0, index)]; + } + + private static async Task WaitForAsync(Func condition, int timeoutMs = 5000) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!condition()) + { + if (stopwatch.ElapsedMilliseconds > timeoutMs) + { + throw new TimeoutException("Timed out waiting for the expected timer ticks."); + } + + await Task.Delay(25, TestContext.Current.CancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/System.Windows.Forms/GlobalSuppressions.cs b/src/System.Windows.Forms/GlobalSuppressions.cs index 74c366ba97c..b1ed827437e 100644 --- a/src/System.Windows.Forms/GlobalSuppressions.cs +++ b/src/System.Windows.Forms/GlobalSuppressions.cs @@ -269,3 +269,4 @@ [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Analyzer wrongly complains for new APIs - known issue.", Scope = "member", Target = "~M:System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.TaskDialogPage,System.Windows.Forms.TaskDialogStartupLocation)~System.Threading.Tasks.Task{System.Windows.Forms.TaskDialogButton}")] [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Analyzer wrongly complains for new APIs - known issue.", Scope = "member", Target = "~M:System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.IWin32Window,System.Windows.Forms.TaskDialogPage,System.Windows.Forms.TaskDialogStartupLocation)~System.Threading.Tasks.Task{System.Windows.Forms.TaskDialogButton}")] [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Analyzer wrongly complains for new APIs - known issue.", Scope = "member", Target = "~M:System.Windows.Forms.TaskDialog.ShowDialogAsync(System.IntPtr,System.Windows.Forms.TaskDialogPage,System.Windows.Forms.TaskDialogStartupLocation)~System.Threading.Tasks.Task{System.Windows.Forms.TaskDialogButton}")] +[assembly: SuppressMessage("DocumentationAnalyzers.StyleRules", "DOC107:Use 'see cref'", Justification = "Has been referenced already in the same paragraph.", Scope = "member", Target = "~P:System.Windows.Forms.TextBoxBase.Padding")] diff --git a/src/System.Windows.Forms/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/PublicAPI.Unshipped.txt index e69de29bb2d..04b79b26c32 100644 --- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt @@ -0,0 +1,132 @@ +#nullable enable +System.Windows.Forms.Appearance.ToggleSwitch = 2 -> System.Windows.Forms.Appearance +System.Windows.Forms.Control.VisualStylesModeChanged -> System.EventHandler? +System.Windows.Forms.Form.SystemTextSizeChanged -> System.EventHandler? +System.Windows.Forms.SystemTextSizeAwareness +System.Windows.Forms.SystemTextSizeAwareness.Notify = 1 -> System.Windows.Forms.SystemTextSizeAwareness +System.Windows.Forms.SystemTextSizeAwareness.Unaware = 0 -> System.Windows.Forms.SystemTextSizeAwareness +virtual System.Windows.Forms.Form.OnSystemTextSizeChanged(System.EventArgs! e) -> void +System.Windows.Forms.TreeView.NodeLeading.get -> float +System.Windows.Forms.TreeView.NodeLeading.set -> void +System.Windows.Forms.TreeView.NodeLeadingChanged -> System.EventHandler? +virtual System.Windows.Forms.TreeView.OnNodeLeadingChanged(System.EventArgs! e) -> void +System.Windows.Forms.ControlMutationExtensions +static System.Windows.Forms.ControlMutationExtensions.SuspendPainting(this System.Windows.Forms.ISupportSuspendPainting! target) -> System.Windows.Forms.SuspendPaintingScope! +static System.Windows.Forms.ControlMutationExtensions.SuspendRelocation(this System.Windows.Forms.ISupportSuspendRelocation! target) -> System.Windows.Forms.SuspendRelocationScope! +System.Windows.Forms.FormRevealMode +System.Windows.Forms.FormRevealMode.Classic = 0 -> System.Windows.Forms.FormRevealMode +System.Windows.Forms.FormRevealMode.Deferred = 1 -> System.Windows.Forms.FormRevealMode +System.Windows.Forms.FormRevealMode.Inherit = -1 -> System.Windows.Forms.FormRevealMode +System.Windows.Forms.ISupportSuspendPainting +System.Windows.Forms.ISupportSuspendPainting.BeginSuspendPainting() -> void +System.Windows.Forms.ISupportSuspendPainting.EndSuspendPainting() -> void +System.Windows.Forms.ISupportSuspendRelocation +System.Windows.Forms.ISupportSuspendRelocation.BeginSuspendRelocation() -> void +System.Windows.Forms.ISupportSuspendRelocation.EndSuspendRelocation() -> void +System.Windows.Forms.SuspendPaintingScope +System.Windows.Forms.SuspendPaintingScope.Dispose() -> void +System.Windows.Forms.SuspendPaintingScope.SuspendPaintingScope(System.Windows.Forms.ISupportSuspendPainting? target) -> void +System.Windows.Forms.SuspendRelocationScope +System.Windows.Forms.SuspendRelocationScope.Dispose() -> void +System.Windows.Forms.SuspendRelocationScope.SuspendRelocationScope(System.Windows.Forms.ISupportSuspendRelocation? target) -> void +static System.Windows.Forms.Application.DefaultFormRevealMode.get -> System.Windows.Forms.FormRevealMode +static System.Windows.Forms.Application.IsFormRevealDeferred.get -> bool +static System.Windows.Forms.Application.SetDefaultFormRevealMode(System.Windows.Forms.FormRevealMode mode) -> void +virtual System.Windows.Forms.Control.BeginSuspendPaintingCore() -> void +virtual System.Windows.Forms.Control.BeginSuspendRelocationCore() -> void +virtual System.Windows.Forms.Control.EndSuspendPaintingCore() -> void +virtual System.Windows.Forms.Control.EndSuspendRelocationCore() -> void +virtual System.Windows.Forms.Form.FormRevealMode.get -> System.Windows.Forms.FormRevealMode +virtual System.Windows.Forms.Form.FormRevealMode.set -> void +override System.Windows.Forms.ComboBox.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.ComboBox.EndSuspendPaintingCore() -> void +override System.Windows.Forms.ListBox.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.ListBox.EndSuspendPaintingCore() -> void +override System.Windows.Forms.ListView.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.ListView.EndSuspendPaintingCore() -> void +override System.Windows.Forms.RichTextBox.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.RichTextBox.EndSuspendPaintingCore() -> void +override System.Windows.Forms.TreeView.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.TreeView.EndSuspendPaintingCore() -> void +System.Windows.Forms.KioskModeManager +System.Windows.Forms.KioskModeManager.ContainerControl.get -> System.Windows.Forms.ContainerControl? +System.Windows.Forms.KioskModeManager.ContainerControl.set -> void +System.Windows.Forms.KioskModeManager.ContainerControlChanged -> System.EventHandler? +System.Windows.Forms.KioskModeManager.EscapeExitsFullScreen.get -> bool +System.Windows.Forms.KioskModeManager.EscapeExitsFullScreen.set -> void +System.Windows.Forms.KioskModeManager.FullScreen.get -> bool +System.Windows.Forms.KioskModeManager.FullScreen.set -> void +System.Windows.Forms.KioskModeManager.FullScreenChanged -> System.EventHandler? +System.Windows.Forms.KioskModeManager.HideTaskbar.get -> bool +System.Windows.Forms.KioskModeManager.HideTaskbar.set -> void +System.Windows.Forms.KioskModeManager.KioskModeManager() -> void +System.Windows.Forms.KioskModeManager.KioskModeManager(System.ComponentModel.IContainer! container) -> void +System.Windows.Forms.KioskModeManager.MousePointerAutoHideDelay.get -> int +System.Windows.Forms.KioskModeManager.MousePointerAutoHideDelay.set -> void +System.Windows.Forms.KioskModeManager.SuppressPowerSaving.get -> bool +System.Windows.Forms.KioskModeManager.SuppressPowerSaving.set -> void +System.Windows.Forms.KioskModeManager.ToggleFullScreen() -> void +System.Windows.Forms.KioskModeManager.ToggleFullScreenKey.get -> System.Windows.Forms.Keys +System.Windows.Forms.KioskModeManager.ToggleFullScreenKey.set -> void +System.Windows.Forms.KioskModeManager.TopMostInFullScreen.get -> bool +System.Windows.Forms.KioskModeManager.TopMostInFullScreen.set -> void +System.Windows.Forms.KioskModeManager.WakeUpCommand.get -> System.Windows.Input.ICommand? +System.Windows.Forms.KioskModeManager.WakeUpCommand.set -> void +System.Windows.Forms.KioskModeManager.Wakeup -> System.Windows.Forms.KioskModeWakeupEventHandler? +System.Windows.Forms.KioskModeWakeupEventArgs +System.Windows.Forms.KioskModeWakeupEventArgs.KioskModeWakeupEventArgs(System.Windows.Forms.KioskModeWakeupSource source) -> void +System.Windows.Forms.KioskModeWakeupEventArgs.Source.get -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupEventHandler +System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Keyboard = 0 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Mouse = 1 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.PowerResume = 2 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.KioskModeWakeupSource.Session = 3 -> System.Windows.Forms.KioskModeWakeupSource +System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.Classic = 0 -> System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.Disabled = 1 -> System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.Inherit = -1 -> System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.Latest = 32767 -> System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.LatestPreview = 32766 -> System.Windows.Forms.VisualStylesMode +System.Windows.Forms.VisualStylesMode.Net11 = 2 -> System.Windows.Forms.VisualStylesMode +override System.Windows.Forms.ButtonBase.OnSystemColorsChanged(System.EventArgs! e) -> void +override System.Windows.Forms.ButtonBase.OnVisualStylesModeChanged(System.EventArgs! e) -> void +override System.Windows.Forms.CheckBox.Dispose(bool disposing) -> void +override System.Windows.Forms.CheckBox.OnPaint(System.Windows.Forms.PaintEventArgs! pevent) -> void +override System.Windows.Forms.CheckBox.OnSystemColorsChanged(System.EventArgs! e) -> void +override System.Windows.Forms.CheckBox.OnVisualStylesModeChanged(System.EventArgs! e) -> void +override System.Windows.Forms.Form.OnSystemColorsChanged(System.EventArgs! e) -> void +override System.Windows.Forms.PropertyGrid.VisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +override System.Windows.Forms.PropertyGrid.VisualStylesMode.set -> void +override System.Windows.Forms.RadioButton.Dispose(bool disposing) -> void +override System.Windows.Forms.RadioButton.OnPaint(System.Windows.Forms.PaintEventArgs! pevent) -> void +override System.Windows.Forms.RadioButton.OnSystemColorsChanged(System.EventArgs! e) -> void +override System.Windows.Forms.RadioButton.OnVisualStylesModeChanged(System.EventArgs! e) -> void +override System.Windows.Forms.UpDownBase.OnVisualStylesModeChanged(System.EventArgs! e) -> void +static System.Windows.Forms.Application.DefaultVisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +static System.Windows.Forms.Application.GetWindowsAccentColor() -> System.Drawing.Color +static System.Windows.Forms.Application.SetDefaultVisualStylesMode(System.Windows.Forms.VisualStylesMode styleSetting) -> void +static System.Windows.Forms.Application.SetSystemTextSizeAwareness(System.Windows.Forms.SystemTextSizeAwareness awareness) -> void +static System.Windows.Forms.Application.SystemTextSize.get -> double +static System.Windows.Forms.Application.SystemTextSizeAwareness.get -> System.Windows.Forms.SystemTextSizeAwareness +static System.Windows.Forms.Application.SystemTextSizeChanged -> System.EventHandler? +virtual System.Windows.Forms.Control.DefaultVisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +virtual System.Windows.Forms.Control.EndSuspendPaintingCore() -> void +virtual System.Windows.Forms.Control.EndSuspendRelocationCore() -> void +virtual System.Windows.Forms.Control.OnParentVisualStylesModeChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.Control.OnVisualStylesModeChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.Control.VisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +virtual System.Windows.Forms.Control.VisualStylesMode.set -> void +virtual System.Windows.Forms.Form.OnSystemTextSizeChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.KioskModeManager.OnContainerControlChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.KioskModeManager.OnFullScreenChanged(System.EventArgs! e) -> void +virtual System.Windows.Forms.KioskModeManager.OnWakeup(System.Windows.Forms.KioskModeWakeupEventArgs! e) -> void +virtual System.Windows.Forms.KioskModeWakeupEventHandler.Invoke(object? sender, System.Windows.Forms.KioskModeWakeupEventArgs! e) -> void +virtual System.Windows.Forms.TreeView.OnNodeLeadingChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnContainerControlChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnEscapeExitsFullScreenChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnFullScreenChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnHideTaskbarChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnSuppressPowerSavingChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnToggleFullScreenKeyChanged(System.EventArgs! e) -> void +virtual WinFormsBoards.Controls.KioskModeManager.OnTopMostInFullScreenChanged(System.EventArgs! e) -> void diff --git a/src/System.Windows.Forms/Resources/SR.resx b/src/System.Windows.Forms/Resources/SR.resx index 53d5073b748..c158bb6b495 100644 --- a/src/System.Windows.Forms/Resources/SR.resx +++ b/src/System.Windows.Forms/Resources/SR.resx @@ -1,4 +1,3 @@ -