Implement Border dashed-stroke geometry#278
Open
jonathanpeppers wants to merge 1 commit into
Open
Conversation
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>
There was a problem hiding this comment.
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]forDrawModifierKt.drawBehindand hand-written DrawScope JNI helpers (DrawScopeGetSize,UnpackSizeWidth/Height,DrawScopeGetNativeCanvas) inComposeBridges.cs, plus a publicModifier.DrawBehindextension. BorderStrokeDrawCallbackJCW (IFunction1<DrawScope, Unit>) in the MAUI project that paints the border stroke via nativePaint/Canvas, supporting RoundRectangle, Ellipse, and Rectangle shapes with dash effects. Allocated once per handler instance for stable JNI identity.BorderHandlergains five mappers (all funnelled through a single_strokeGeometryVersionMutableState slot) and aHasCustomStrokeGeometrydiscriminator that selects theDrawBehindpath only when needed, drivingBorderHandlerto 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wires up the five MAUI dashed-stroke properties that PR #273 deferred:
StrokeDashPatternStrokeDashOffsetStrokeLineCapStrokeLineJoinStrokeMiterLimitApproach
Compose's
Strokector anddashPathEffectare stripped by the@JvmInline value classmangling, andModifier.borderis solid-only. Instead of building a partial bridge per primitive, this PR keepsModifier.Border()as the solid fast path and switches toModifier.DrawBehind()+ nativeAndroid.Graphics.Paint+DashPathEffectwhenever any of the five knobs diverges from defaults.AndroidCanvas_androidKt.getNativeCanvas(ICanvas)is directly bound, so the route is short.New plumbing
[ComposeBridge]forDrawModifierKt.drawBehind+Modifier.DrawBehind(IFunction1)public extension.ComposeBridges.cs:DrawScopeGetSize,UnpackSizeWidth/Height,DrawScopeGetNativeCanvas(walksDrawScope→DrawContext→ICanvas→Android.Graphics.Canvas).Platform/BorderStrokeDrawCallback.cs(net/compose/maui/BorderStrokeDrawCallback) implementingIFunction1<DrawScope, Unit>with a cachedPaintand per-instance mutable state. WalksIBorderStroke.Shape:RoundRectangleuniform →Canvas.DrawRoundRectRoundRectangleper-corner →Path.AddRoundRect(radii[8])Ellipse→Canvas.DrawOvalCanvas.DrawRectstrokeWidth / 2to matchModifier.bordercentring.[assembly: InternalsVisibleTo("Microsoft.AndroidX.Compose.Maui")]so the JCW can call the internalComposeBridgeshelpers.ColorMapping.ToArgb(MauiColor?)helper for nativePaint.Color.BorderHandler_strokeGeometryVersionMutableStateslot.readonly _strokeDrawCallbackper handler instance — keeps JCW identity stable across recompositions (Compose'sDrawBehindElement.equalsis reference-based).HasCustomStrokeGeometry()discriminator picks theDrawBehindpath only when needed; otherwise we stay onModifier.Border(), so the solid case is unchanged.Coverage
BorderHandler35 / 40 (88 %) → 40 / 40 (100 %) in regenerateddocs/maui-coverage.md.Sample
VisualsPage.xamlgains five dashed/cap/join variants:4,26,3,2,3with offset 28,4dashVerification
dotnet build src/Microsoft.AndroidX.Compose.Maui— succeeds.dotnet build src/Microsoft.AndroidX.Compose.Maui.Sample— succeeds.dotnet run scripts/maui-coverage.cs—BorderHandlerrow flips to ✅ 100 %.