Skip to content

Bind drawing primitives: Canvas, Modifier.drawBehind/drawWithContent, DrawScope, Brush, Path, Shape factories #64

@jonathanpeppers

Description

@jonathanpeppers

No drawing primitives are wrapped today. Without these, all rendering has to compose into existing widgets — there's no way to draw custom shapes, charts, signature pads, sparklines, progress arcs, custom dividers, gradients, or anything else off the Material-component grid.

Scope

Canvas composable + drawing modifiers

API Kt class
Canvas(modifier) { DrawScope -> ... } CanvasKt
Modifier.drawBehind { DrawScope -> ... } DrawModifierKt
Modifier.drawWithContent { DrawScope -> drawContent(); ... } DrawModifierKt
Modifier.drawWithCache { CacheDrawScope -> onDrawBehind { ... } } DrawModifierKt

DrawScope

The receiver passed to every draw lambda. Has ~30 methods:

Drawing: drawRect, drawCircle, drawArc, drawLine, drawOval, drawRoundRect, drawPath, drawPoints, drawImage, drawText

State stack: clipRect, clipPath, translate, rotate, scale, inset, withTransform

Properties: size, center, layoutDirection, drawContext

On the C# side this needs a managed wrapper class (DrawScope) that proxies into the underlying androidx.compose.ui.graphics.drawscope.DrawScope JNI handle. Each method becomes a [ComposeBridge] taking the scope handle + draw parameters.

Brush

What you fill / stroke with — used by DrawScope methods, Modifier.Background(brush), Modifier.Border(width, brush).

API Kt class
Brush.linearGradient(colors, start, end, tileMode) BrushKt
Brush.horizontalGradient(colors, startX, endX, tileMode) BrushKt
Brush.verticalGradient(colors, startY, endY, tileMode) BrushKt
Brush.radialGradient(colors, center, radius, tileMode) BrushKt
Brush.sweepGradient(colors, center) BrushKt
SolidColor(color) SolidColor data class

Path

Mutable geometry builder for DrawScope.drawPath and Modifier.clip(GenericShape { ... }).

API Kt class
Path() (constructor) Path (Android shim around androidx.compose.ui.graphics.Path)
moveTo, lineTo, relativeLineTo, quadraticBezierTo, cubicTo, relativeCubicTo, arcTo, close, reset, rewind methods on Path
addRect, addOval, addRoundRect, addArc, addPath methods on Path
PathMeasure for "draw N% of this path" animations
PathOperation (Union, Intersect, Difference, ReverseDifference, Xor) + Path.op(p1, p2, op) boolean path combinations

Shape

Used today by Modifier.background(brush, shape), Border(width, brush, shape), Surface(shape), Card(shape), Modifier.clip(shape), etc. — already referenced in several existing bridges (e.g. ModifierBackgroundDefault has a shape slot) but never actually exposed in the C# facade.

API Kt class
RoundedCornerShape(corner: Dp) / RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart) RoundedCornerShapeKt
RoundedCornerShape(percent: Int) / per-corner percent RoundedCornerShapeKt
CircleShape (singleton) RoundedCornerShape instance in ShapesKt
CutCornerShape(corner) / per-corner variant CutCornerShapeKt
RectangleShape (singleton) RectangleShapeKt
GenericShape { size, layoutDirection -> path } GenericShapeKt — arbitrary path-defined shape
AbsoluteRoundedCornerShape / AbsoluteCutCornerShape LTR-fixed variants (ignore layout direction)

Shape on the C# side

new Canvas(Modifier.Companion.Size(200)) {
    Draw = scope => {
        scope.DrawCircle(Color.Red, radius: 50f, center: scope.Center);
        scope.DrawLine(
            color: Color.Blue,
            start: scope.TopLeft,
            end:   new Offset(scope.Size.Width, scope.Size.Height),
            strokeWidth: 4f);
        scope.DrawPath(myPath, brush: Brush.LinearGradient(
            new[] { Color.Cyan, Color.Magenta },
            start: scope.TopLeft,
            end:   scope.BottomRight));
    }
}

// Or just a background:
new Box {
    Modifier.Companion
        .Size(64)
        .Background(Brush.RadialGradient(new[] { Color.White, Color.Black }))
        .Clip(new RoundedCornerShape(corner: 12)),
}

Effort estimate

Medium-high. Breakdown:

  1. Shape factories — small, can land first. Each is a static call on a *Kt class returning a Shape JNI handle. RoundedCornerShape/CircleShape likely hash-mangled because of Dp inline-class corner params. Once these exist, all the existing bridges with shape slots become user-callable (background-with-shape, border-with-shape, clip).
  2. Brush factories — small, same shape as Shape. Returns a Brush JNI handle that flows into draw calls.
  3. Modifier.Clip(shape) — fold into the modifier expansion issue (Add CompositionLocal + CompositionLocalProvider (LocalContext, LocalDensity, LocalContentColor, LocalTextStyle, ...) #59); needs Shape factories to be useful.
  4. Path builder — medium. Mutable state holder; needs [ComposeBridge] partials for each builder method, plus a managed Path class wrapping the underlying IntPtr.
  5. Canvas composable + Modifier.drawBehind / drawWithContent — medium. The tricky part is the lambda shape: the body takes a DrawScope receiver, so we need a DrawScope managed wrapper that holds the underlying IntPtr and forwards every draw call through a [ComposeBridge].
  6. DrawScope wrapper — medium-high. ~30 methods, each a [ComposeBridge] on DrawScope.${methodName} taking the scope handle as the first param. Most of these are hash-mangled because of Color/Offset/Size/Dp inline-class params.

Dependencies

Sample

A "Drawing" tab in the sample with a sparkline + a custom gradient-backed circle that pulses (paired with the animation primitives issue, if/when that lands).

Tracking

Long-term dotnet/java-interop#1440.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions