Skip to content

Add extensible Paint and gradient system - #3

Merged
ProGraMajster merged 9 commits into
masterfrom
feature/paint-and-gradients
Jul 19, 2026
Merged

Add extensible Paint and gradient system#3
ProGraMajster merged 9 commits into
masterfrom
feature/paint-and-gradients

Conversation

@ProGraMajster

Copy link
Copy Markdown
Owner

Problem

ModernFormsNext already exposed several brush and gradient types, but mutation, invalidation, resource sharing, coordinate semantics, and Skia shader creation were inconsistent. That made the existing surface difficult to reuse safely for dynamic themes and the planned Shape controls.

Current state

The framework used the Brush hierarchy directly and already supported solid, linear, radial, sweep, and glass rendering. Several public properties exposed SkiaSharp types, gradient stops were plain mutable data, shader construction was distributed through rendering extensions, and controls did not observe internal brush mutations.

Solution

  • Keep the existing Brush hierarchy as the single canonical model instead of introducing a parallel Paint hierarchy.
  • Make brushes, gradient stops, and the typed stop collection observable and shareable.
  • Add opacity, platform-neutral transforms, relative gradient geometry, focal radial origins, pad/repeat/reflect spread modes, and NoBrush.
  • Centralize bounds-aware Skia shader construction in SkiaBrushFactory.
  • Observe mutable brushes from controls with weak, reference-counted subscriptions and render-only invalidation.
  • Extend Designer serialization and editing so the new values survive .Designer.cs round trips.
  • Add a focused ControlGallery page and user/architecture documentation.

Public API

The preferred platform-neutral API uses System.Drawing.Color, PointF, and System.Numerics.Matrix3x2. The extended surface includes Brush.Opacity, Brush.Transform, GradientSpreadMode, GradientStopCollection, relative geometry on linear/radial/sweep brushes, RadialGradientBrush.GradientOrigin, and NoBrush.

Existing SKColor and SKPoint members remain as compatibility views over the same backing values. They are documented as legacy integration surfaces; new application code does not need SkiaSharp types.

Compatibility

Existing BackColor, ForeColor, and legacy brush constructors continue to work. BackgroundBrush takes precedence when assigned; clearing it restores the color fallback, so there are not two independent rendering sources of truth.

The main compatibility risk is that GradientStops now returns GradientStopCollection rather than List<GradientStop>, and invalid offsets are rejected instead of flowing into rendering. Existing source that only uses normal collection operations remains straightforward to migrate, but already compiled consumers must rebuild.

Dynamic resources

Brush values participate in the existing application/window/control resource hierarchy. Tests cover hierarchy overrides, object replacement, in-place gradient mutation, removal and fallback, duplicate-subscription prevention, and release of controls without a durable brush subscription.

SkiaSharp integration

SkiaBrushFactory is the single conversion boundary. It maps spread modes to Skia tile modes, applies relative bounds and transforms once, and creates short-lived shaders owned by the drawing operation. Bounds-dependent native shaders are deliberately not cached, avoiding stale-size entries and unclear native lifetime.

Validation

  • dotnet restore ModernFormsNext.slnx: passed.
  • Debug and Release solution builds: passed.
  • Full solution tests: 536/536 passed.
  • Focused brush/gradient tests: 32/32 passed.
  • Dynamic-resource and brush-resource tests: 20/20 passed.
  • Designer serialization tests: 3/3 passed.
  • Windows target build: passed.
  • Android backend, cross-platform Android sample, and Android smoke-test builds: passed.
  • Designer, VSIX Debug/Release, ControlGallery, and template/reference app builds: passed.
  • Local package validation: eight 1.8.0 .nupkg files, seven expected .snupkg files, README and XML documentation present.

Known warnings are pre-existing dependency/security advisories in the VSIX graph, .NET Framework compatibility warnings for older Visual Studio SDK packages, nullable/generated interop warnings, obsolete Skia text/filter APIs, Windows platform annotations, and Android 16 KB page-size warnings for the bundled HarfBuzz native library. No warning was suppressed.

Windows impact

The primary Windows rendering and Designer paths use the shared brush factory. Designer code generation now preserves opacity, transform, spread mode, and radial focal origin while legacy brush expressions continue to deserialize.

Android impact

The Android backend consumes the same shared brush and Skia conversion layers. Its target builds and backend tests pass. Android remains experimental; GPU behavior, performance, lifecycle interactions, and rendering on physical devices still require manual validation.

ControlGallery

A dedicated Paint and Gradients panel demonstrates solid, linear, radial/focal, sweep, spread, transform, opacity, resource replacement, and in-place resource mutation. No screenshot is attached because an interactive visual session was not performed in this environment.

Limitations

  • Gradient coordinates are relative-only in this stage.
  • Advanced linear-light/color-space interpolation is not exposed yet.
  • JSON theme serialization is designed and documented but intentionally not implemented without ThemeManager.
  • There is no native shader cache; render-time allocation is preferred over unsafe bounds-dependent caching for now.
  • Legacy public SkiaSharp compatibility members remain to avoid a broad breaking change.
  • No animation scheduler or Shape controls are included.

Next steps

  1. Harden the shared UI animation scheduler so mutable brush values can be animated without a second timing system.
  2. Add the ThemeManager and its renderer-neutral JSON brush converter.
  3. Build Shape controls on the common brush contract after the animation and theme foundations are stable.

Document why the established Brush hierarchy remains the canonical paint model instead of introducing a parallel Paint abstraction.

Record the current Skia coupling, mutation and allocation problems, the platform-neutral preferred API, dynamic-resource invalidation model, shader ownership rules, compatibility plan, performance risks, and completion criteria.

The ADR preserves existing Color/SKColor and SKPoint compatibility adapters while keeping Windows and experimental Android on one shared rendering path. Theme JSON, absolute coordinates, Shape controls, and the animation scheduler remain explicitly deferred.
Make the existing Brush hierarchy the mutable, observable visual-value foundation required by dynamic themes and future shapes, instead of adding a competing Paint hierarchy.

Add opacity, a platform-neutral Matrix3x2 transform, explicit no-fill semantics, strict finite-value validation, pad/repeat/reflect spread modes, and an observable GradientStopCollection with stable cached ordering. Add System.Drawing.Color and PointF properties while preserving the published SKColor/SKPoint members as adapters to the same backing data.

All behavior remains in the shared framework project for identical Windows and experimental Android semantics. GradientStops changes from List<T> to the typed collection, and invalid offsets now fail explicitly; absolute coordinates, JSON loading, advanced color spaces, and animation batching remain future work.
Move linear, radial, focal-radial, and sweep shader construction into one shared Skia adapter so control renderers no longer duplicate coordinate, tile-mode, alpha, transform, and ownership logic.

Render pad/repeat/reflect spread modes, brush opacity, explicit no-fill, one-stop gradients, zero-radius radial brushes, and platform-neutral transforms. Bounds are consumed exactly once, preventing a second DPI scale, while every bounds-specific SKShader remains scoped and disposed after drawing; only managed stop ordering is cached.

Windows, experimental Android, the Designer, and off-screen surfaces use the same code. Native shader caching and advanced color-space interpolation are intentionally deferred until measurements justify a bounded policy.
Wire Control, GroupBox, and Switch brush properties to a shared reference-counted invalidation helper so changing a brush or an individual gradient stop repaints every current consumer without requiring reassignment.

Use weak control references in brush event handlers, detach on property replacement and disposal, and share one subscription when multiple properties use the same brush. Dynamic-resource replacement, hierarchy fallback, and ordinary CLR setter behavior remain unchanged, preserving BackColor/ForeColor fallback precedence and targeted invalidation.

The logic is platform-neutral and runs synchronously with the existing UI-thread resource contract on Windows and experimental Android. It does not introduce layout invalidation, a global resource broadcast, or an animation scheduler.
Add focused tests for neutral and SKColor compatibility views, opacity and transform validation, observable stops, stable equal-offset ordering, collection membership changes, relative coordinates, radial origins, zero radius, spread mapping, bounds-specific shaders, and scoped disposal.

Cover application, window, and control resource precedence; Solid-to-gradient replacement; nested stop mutation; fallback restoration; shared-property reference counting; disposal; and garbage collection of a forgotten control bound to a long-lived brush.

The rendering tests execute the same shared Skia path used by Windows and experimental Android and verify that bounds are resolved without a second DPI conversion. Physical-device GPU rendering and animation stress remain manual or future validation.
Round-trip brush opacity, transforms, gradient spread modes, and radial focal origins through designer documents and generated C# initializers.

Keep legacy documents compatible by applying the original defaults when the new fields are absent. Preserve common and type-specific values when Property Grid or BrushEditDialog edits recreate a brush.

Add focused serialization and code-generation coverage for the new data and backward-compatible defaults.
Assert that BackgroundBrush takes precedence over the compatibility BackColor facade and that assigning null restores the style color fallback.
Add a dedicated ControlGallery surface for solid, linear, radial, focal, sweep, spread, opacity, and transformed brushes.

Include interactive dynamic-resource examples that exercise both in-place brush mutation and resource replacement without reassigning the target property.
Document the canonical Brush API, relative coordinates, gradient stops, spread, transforms, color fallback, no-fill semantics, runtime mutation, and dynamic-resource lifetime behavior.

Record a renderer-neutral JSON schema direction without introducing a partial ThemeManager serializer. Refresh public XML examples to use the preferred System.Drawing and Matrix3x2 surface while retaining published Skia adapters for compatibility.

Mark only the paint/gradient foundation as implemented in the roadmap and recommend UI animation scheduler hardening next. Full ThemeManager, Shape, absolute mapping, and advanced color interpolation remain deferred; Android remains experimental.
@ProGraMajster
ProGraMajster marked this pull request as ready for review July 19, 2026 15:45
@ProGraMajster
ProGraMajster merged commit a8fe289 into master Jul 19, 2026
1 check passed
@ProGraMajster
ProGraMajster deleted the feature/paint-and-gradients branch July 19, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant