Skip to content

Implement Border dashed-stroke geometry#278

Open
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers/border-dashed-strokes
Open

Implement Border dashed-stroke geometry#278
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers/border-dashed-strokes

Conversation

@jonathanpeppers

Copy link
Copy Markdown
Owner

Wires up the five MAUI dashed-stroke properties that PR #273 deferred:

  • StrokeDashPattern
  • StrokeDashOffset
  • StrokeLineCap
  • StrokeLineJoin
  • StrokeMiterLimit

Approach

Compose's Stroke ctor and dashPathEffect are stripped by the @JvmInline value class mangling, and Modifier.border is solid-only. Instead of building a partial bridge per primitive, this PR keeps Modifier.Border() as the solid fast path and switches to Modifier.DrawBehind() + native Android.Graphics.Paint + DashPathEffect whenever any of the five knobs diverges from defaults. AndroidCanvas_androidKt.getNativeCanvas(ICanvas) is directly bound, so the route is short.

New plumbing

  • [ComposeBridge] for DrawModifierKt.drawBehind + Modifier.DrawBehind(IFunction1) public extension.
  • Hand-written DrawScope JNI helpers in ComposeBridges.cs: DrawScopeGetSize, UnpackSizeWidth/Height, DrawScopeGetNativeCanvas (walks DrawScopeDrawContextICanvasAndroid.Graphics.Canvas).
  • New JCW Platform/BorderStrokeDrawCallback.cs (net/compose/maui/BorderStrokeDrawCallback) implementing IFunction1<DrawScope, Unit> with a cached Paint and per-instance mutable state. Walks IBorderStroke.Shape:
    • RoundRectangle uniform → Canvas.DrawRoundRect
    • RoundRectangle per-corner → Path.AddRoundRect(radii[8])
    • EllipseCanvas.DrawOval
    • fallback → Canvas.DrawRect
    • Insets by strokeWidth / 2 to match Modifier.border centring.
  • [assembly: InternalsVisibleTo("Microsoft.AndroidX.Compose.Maui")] so the JCW can call the internal ComposeBridges helpers.
  • ColorMapping.ToArgb(MauiColor?) helper for native Paint.Color.

BorderHandler

  • Five new mappers, all folded into a single _strokeGeometryVersion MutableState slot.
  • One readonly _strokeDrawCallback per handler instance — keeps JCW identity stable across recompositions (Compose's DrawBehindElement.equals is reference-based).
  • HasCustomStrokeGeometry() discriminator picks the DrawBehind path only when needed; otherwise we stay on Modifier.Border(), so the solid case is unchanged.

Coverage

BorderHandler 35 / 40 (88 %) → 40 / 40 (100 %) in regenerated docs/maui-coverage.md.

Sample

VisualsPage.xaml gains five dashed/cap/join variants:

  • Dash pattern 4,2
  • Pattern 6,3,2,3 with offset 2
  • Round caps + dotted dash
  • Bevel join
  • Round join + 8,4 dash

Verification

  • dotnet build src/Microsoft.AndroidX.Compose.Maui — succeeds.
  • dotnet build src/Microsoft.AndroidX.Compose.Maui.Sample — succeeds.
  • dotnet run scripts/maui-coverage.csBorderHandler row flips to ✅ 100 %.
  • On-device: not run in this session.

Wires up the five MAUI dashed-stroke properties that PR #273 deferred:

- StrokeDashPattern

- StrokeDashOffset

- StrokeLineCap

- StrokeLineJoin

- StrokeMiterLimit

Approach: keep the existing Modifier.Border() solid fast path for the

common case, but switch to a Modifier.DrawBehind() + native

Android.Graphics.Paint + DashPathEffect path whenever any of the five

knobs diverges from defaults. Compose's Stroke ctor + dashPathEffect

are stripped by the inline-class JVM mangling, so going down to the

native canvas via the directly-bound AndroidCanvas_androidKt.getNativeCanvas

is the smallest path.

New plumbing:

- [ComposeBridge] for DrawModifierKt.drawBehind + Modifier.DrawBehind extension.

- Hand-written DrawScope JNI helpers (DrawScopeGetSize, DrawScopeGetNativeCanvas, UnpackSizeWidth/Height) that walk DrawScope -> DrawContext -> ICanvas -> Android.Graphics.Canvas.

- New JCW Platform/BorderStrokeDrawCallback (registered net/compose/maui/BorderStrokeDrawCallback) implementing IFunction1<DrawScope, Unit> with a cached Paint and per-instance mutable state. Walks the IBorderStroke shape switch — RoundRectangle uniform/per-corner, Ellipse, Rectangle fallback — and insets by strokeWidth/2 to match Modifier.border centring.

- InternalsVisibleTo for Microsoft.AndroidX.Compose.Maui so the JCW can call internal ComposeBridges helpers.

- ColorMapping.ToArgb helper for native Paint colour.

BorderHandler:

- 5 new mappers funneling into a single _strokeGeometryVersion MutableState slot.

- One readonly _strokeDrawCallback per handler instance to keep JCW identity stable across recompositions.

- HasCustomStrokeGeometry() discriminator picks the DrawBehind path only when needed; otherwise we stay on Modifier.Border() (no regression for the solid case).

Coverage: BorderHandler 35/40 (88%) -> 40/40 (100%). docs/maui-coverage.md regenerated.

Sample: extended VisualsPage with five dashed/cap/join variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 15, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR completes BorderHandler's property-mapper coverage by wiring up five MAUI dashed-stroke properties (StrokeDashPattern, StrokeDashOffset, StrokeLineCap, StrokeLineJoin, StrokeMiterLimit) that were explicitly deferred in PR #273. Since Compose's Modifier.border only supports solid strokes, the implementation introduces a Modifier.drawBehind code path that drops down to Android's native Canvas + Paint + DashPathEffect APIs when any of the five geometry knobs diverges from defaults. The solid Modifier.border fast path is preserved for the trivial case.

Changes:

  • New [ComposeBridge] for DrawModifierKt.drawBehind and hand-written DrawScope JNI helpers (DrawScopeGetSize, UnpackSizeWidth/Height, DrawScopeGetNativeCanvas) in ComposeBridges.cs, plus a public Modifier.DrawBehind extension.
  • BorderStrokeDrawCallback JCW (IFunction1<DrawScope, Unit>) in the MAUI project that paints the border stroke via native Paint/Canvas, supporting RoundRectangle, Ellipse, and Rectangle shapes with dash effects. Allocated once per handler instance for stable JNI identity.
  • BorderHandler gains five mappers (all funnelled through a single _strokeGeometryVersion MutableState slot) and a HasCustomStrokeGeometry discriminator that selects the DrawBehind path only when needed, driving BorderHandler to 100% MAUI coverage.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Microsoft.AndroidX.Compose/ComposeBridges.cs DrawScope JNI helpers (size unpack, native canvas walk) and ModifierDrawBehind [ComposeBridge]
src/Microsoft.AndroidX.Compose/ModifierExtensions.cs Public Modifier.DrawBehind(IFunction1) extension
src/Microsoft.AndroidX.Compose/AssemblyInfo.cs New [InternalsVisibleTo("Microsoft.AndroidX.Compose.Maui")]
src/Microsoft.AndroidX.Compose/PublicAPI.Unshipped.txt New DrawBehind entry (out of sort order)
src/Microsoft.AndroidX.Compose.Maui/Platform/BorderStrokeDrawCallback.cs JCW that paints dashed/styled border strokes via Paint + DashPathEffect
src/Microsoft.AndroidX.Compose.Maui/Handlers/BorderHandler.cs Five new mapper registrations, HasCustomStrokeGeometry discriminator, ConfigureStrokeDrawCallback wiring
src/Microsoft.AndroidX.Compose.Maui/ColorMapping.cs ToArgb(MauiColor?) helper for native Paint.Color
src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/VisualsPage.xaml Five dashed-stroke demo variants
docs/maui-coverage.md BorderHandler 88% → 100%, overall containers 95% → 98%

static AndroidX.Compose.ModifierExtensions.DefaultMinSize(this AndroidX.Compose.Modifier! modifier, AndroidX.Compose.Dp? minWidth = null, AndroidX.Compose.Dp? minHeight = null) -> AndroidX.Compose.Modifier!
static AndroidX.Compose.ModifierExtensions.DetectDragGestures(this AndroidX.Compose.Modifier! modifier, System.Action<AndroidX.Compose.Offset>! onDrag, System.Action<AndroidX.Compose.Offset>? onDragStart = null, System.Action? onDragEnd = null, System.Action? onDragCancel = null, object? key = null) -> AndroidX.Compose.Modifier!
static AndroidX.Compose.ModifierExtensions.DetectTapGestures(this AndroidX.Compose.Modifier! modifier, System.Action<AndroidX.Compose.Offset>? onTap = null, System.Action<AndroidX.Compose.Offset>? onPress = null, System.Action<AndroidX.Compose.Offset>? onLongPress = null, System.Action<AndroidX.Compose.Offset>? onDoubleTap = null, object? key = null) -> AndroidX.Compose.Modifier!
static AndroidX.Compose.ModifierExtensions.DrawBehind(this AndroidX.Compose.Modifier! modifier, Kotlin.Jvm.Functions.IFunction1! onDraw) -> AndroidX.Compose.Modifier!
Comment on lines +222 to +248
void ConfigureStrokeDrawCallback(MauiBorder border, long strokeColor, float strokeThickness)
{
// ARGB int for native Paint — derived directly from the live
// SolidPaint Color so the dashed path stays in sync with the
// brush even when MapStroke doesn't refire (e.g. opacity tweak
// bubbling through SolidPaint).
var solidColor = (border as IStroke)?.Stroke is SolidPaint solid
? solid.Color
: null;
var stroke = (IBorderStroke)border;
_strokeDrawCallback.StrokeArgb = ColorMapping.ToArgb(solidColor);
_strokeDrawCallback.StrokeThicknessDip = strokeThickness;
_strokeDrawCallback.StrokeDashPattern = stroke.StrokeDashPattern;
_strokeDrawCallback.StrokeDashOffset = stroke.StrokeDashOffset;
_strokeDrawCallback.StrokeLineCap = stroke.StrokeLineCap;
_strokeDrawCallback.StrokeLineJoin = stroke.StrokeLineJoin;
_strokeDrawCallback.StrokeMiterLimit = stroke.StrokeMiterLimit;
_strokeDrawCallback.Shape = stroke.Shape;
var metrics = global::Android.Content.Res.Resources.System?.DisplayMetrics
?? throw new InvalidOperationException("Resources.System.DisplayMetrics not available.");
_strokeDrawCallback.Density = metrics.Density;

// Silence "strokeColor is unused" — it's already derived from
// SolidPaint above, but kept on the signature so the caller
// can pass through the cached MutableState slot when MAUI's
// stroke property cycles through null between updates.
_ = strokeColor;
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.

3 participants