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/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 @@ ''' Setting this property inside the event handler determines the ''' for the application. ''' Public Property ColorMode As SystemColorMode + ''' + ''' 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 +56,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..cb59f1b43a4 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 @@ -734,7 +732,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 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 +744,34 @@ Namespace Microsoft.VisualBasic.ApplicationServices Dim applicationDefaultsEventArgs As New ApplyApplicationDefaultsEventArgs( MinimumSplashScreenDisplayTime, HighDpiMode, - ColorMode) With + ColorMode, + FormRevealMode) 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 +786,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 +803,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 +1105,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..12b0e2b41ae 100644 --- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt @@ -0,0 +1,145 @@ +#nullable enable +System.Windows.Forms.Appearance.ToggleSwitch = 2 -> System.Windows.Forms.Appearance +System.Windows.Forms.Control.VisualStylesModeChanged -> System.EventHandler? +System.Windows.Forms.ControlMutationExtensions +System.Windows.Forms.Form.SystemTextSizeChanged -> System.EventHandler? +System.Windows.Forms.FormAppearanceMode +System.Windows.Forms.FormAppearanceMode.Classic = 0 -> System.Windows.Forms.FormAppearanceMode +System.Windows.Forms.FormAppearanceMode.Deferred = 1 -> System.Windows.Forms.FormAppearanceMode +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.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.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 +System.Windows.Forms.SystemTextSizeAwareness +System.Windows.Forms.SystemTextSizeAwareness.Notify = 1 -> System.Windows.Forms.SystemTextSizeAwareness +System.Windows.Forms.SystemTextSizeAwareness.Unaware = 0 -> System.Windows.Forms.SystemTextSizeAwareness +System.Windows.Forms.TreeView.NodeLeading.get -> float +System.Windows.Forms.TreeView.NodeLeading.set -> void +System.Windows.Forms.TreeView.NodeLeadingChanged -> System.EventHandler? +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.Net11 = 2 -> System.Windows.Forms.VisualStylesMode +WinFormsBoards.Controls.KioskModeManager +WinFormsBoards.Controls.KioskModeManager.ContainerControl.get -> System.Windows.Forms.ContainerControl? +WinFormsBoards.Controls.KioskModeManager.ContainerControl.set -> void +WinFormsBoards.Controls.KioskModeManager.ContainerControlChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.EnterFullScreen() -> void +WinFormsBoards.Controls.KioskModeManager.EscapeExitsFullScreen.get -> bool +WinFormsBoards.Controls.KioskModeManager.EscapeExitsFullScreen.set -> void +WinFormsBoards.Controls.KioskModeManager.EscapeExitsFullScreenChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.ExitFullScreen() -> void +WinFormsBoards.Controls.KioskModeManager.FullScreenChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.HideTaskbar.get -> bool +WinFormsBoards.Controls.KioskModeManager.HideTaskbar.set -> void +WinFormsBoards.Controls.KioskModeManager.HideTaskbarChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.IsFullScreen.get -> bool +WinFormsBoards.Controls.KioskModeManager.KioskModeManager() -> void +WinFormsBoards.Controls.KioskModeManager.KioskModeManager(System.ComponentModel.IContainer! container) -> void +WinFormsBoards.Controls.KioskModeManager.SuppressPowerSaving.get -> bool +WinFormsBoards.Controls.KioskModeManager.SuppressPowerSaving.set -> void +WinFormsBoards.Controls.KioskModeManager.SuppressPowerSavingChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.ToggleFullScreen() -> void +WinFormsBoards.Controls.KioskModeManager.ToggleFullScreenKey.get -> System.Windows.Forms.Keys +WinFormsBoards.Controls.KioskModeManager.ToggleFullScreenKey.set -> void +WinFormsBoards.Controls.KioskModeManager.ToggleFullScreenKeyChanged -> System.EventHandler? +WinFormsBoards.Controls.KioskModeManager.TopMostInFullScreen.get -> bool +WinFormsBoards.Controls.KioskModeManager.TopMostInFullScreen.set -> void +WinFormsBoards.Controls.KioskModeManager.TopMostInFullScreenChanged -> System.EventHandler? +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.OnVisualStylesModeChanged(System.EventArgs! e) -> void +override System.Windows.Forms.ComboBox.BeginSuspendPaintingCore() -> void +override System.Windows.Forms.ComboBox.EndSuspendPaintingCore() -> void +override System.Windows.Forms.Form.OnSystemColorsChanged(System.EventArgs! e) -> void +override System.Windows.Forms.KioskModeManager.Dispose(bool disposing) -> void +override System.Windows.Forms.KioskModeManager.Site.get -> System.ComponentModel.ISite? +override System.Windows.Forms.KioskModeManager.Site.set -> 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.PropertyGrid.VisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +override System.Windows.Forms.PropertyGrid.VisualStylesMode.set -> 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 +static System.Windows.Forms.Application.DefaultVisualStylesMode.get -> System.Windows.Forms.VisualStylesMode +static System.Windows.Forms.Application.FormAppearanceMode.get -> System.Windows.Forms.FormAppearanceMode +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.SetFormAppearanceMode(System.Windows.Forms.FormAppearanceMode mode) -> 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? +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! +virtual System.Windows.Forms.Control.BeginSuspendPaintingCore() -> void +virtual System.Windows.Forms.Control.BeginSuspendRelocationCore() -> void +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..999bed6d05b 100644 --- a/src/System.Windows.Forms/Resources/SR.resx +++ b/src/System.Windows.Forms/Resources/SR.resx @@ -1,4 +1,3 @@ -