Skip to content

Fill missing MAUI property-mapper keys#273

Merged
jonathanpeppers merged 7 commits into
mainfrom
jonathanpeppers/fill-maui-mapper-keys
Jun 15, 2026
Merged

Fill missing MAUI property-mapper keys#273
jonathanpeppers merged 7 commits into
mainfrom
jonathanpeppers/fill-maui-mapper-keys

Conversation

@jonathanpeppers

Copy link
Copy Markdown
Owner

Drives every Compose-backed MAUI handler in docs/maui-coverage.md toward
100% property-mapper key coverage. Stacked on PR #269 originally; rebased
onto main after that PR merged.

Headline numbers

Metric Before After
Handlers we override 24 / 43 (55.8%) 24 / 43 (55.8%)
Property-mapper keys covered 830 / 1224 (67.8%) 883 / 1224 (72.1%)
Leaves category coverage 92% 98%
Containers category coverage 89% 95%
Pages / Navigation 86% (yellow) 100% (green)

Handler count is unchanged on purpose — opening new red ❌ handlers
(FlyoutViewHandler, NavigationViewHandler, ShapeViewHandler,
WebViewHandler, …) is explicitly out of scope per the request.

Per-handler — what got wired

Pages / Navigation

  • PageHandlerTitle ✅ via Activity title forwarding (with TODO
    for Shell-owned case).

Containers

  • LayoutHandlerClipsToBounds ✅ via Modifier.Clip.
  • RefreshViewHandlerIsRefreshEnabled ✅ (gates the
    onRefresh callback + visually dims when disabled).
  • BorderHandlerShape ✅ (alias mapping to the same slot
    StrokeShape already wires).

Leaves

  • DatePickerHandlerCharacterSpacing ✅, IsOpen
    (two-way bind to the modal dialog).
  • TimePickerHandlerCharacterSpacing ✅, IsOpen ✅.
  • IndicatorViewHandlerHideSingle ✅, MaximumVisible ✅.
  • LabelHandlerCharacterSpacing, LineHeight, Padding,
    TextDecorations ✅ (4 keys).
  • PickerHandlerCharacterSpacing, HorizontalTextAlignment,
    IsOpen, Items ✅ (4 keys).
  • ButtonHandlerCharacterSpacing, CornerRadius, Font,
    Padding, Source (image inside button), StrokeColor,
    StrokeThickness ✅ — 7 new keys, 100%.
  • EntryHandler / EditorHandler / SearchBarHandler — refactored
    onto Compose's OutlinedTextField(MutableState<TextFieldValue>)
    overload via a TextFieldValueState : MutableState<TextFieldValue>
    subclass that intercepts every write to enforce
    ITextInput.MaxLength and mirror text + caret/selection back to
    the MAUI virtual view. New keys wired across the three:
    CharacterSpacing, HorizontalTextAlignment,
    IsSpellCheckEnabled, IsTextPredictionEnabled (collapsed →
    autoCorrectEnabled), MaxLength, PlaceholderColor,
    ReturnType (Entry/SearchBar), CursorPosition,
    SelectionLength, ClearButtonVisibility (Entry trailing X),
    SearchIconColor + CancelButtonColor (SearchBar trailing X).

Deliberate TODOs (left unmapped on purpose)

These remain yellow in the coverage report instead of being wired to
no-op mappers — per the request, partial coverage is fine, lying
coverage is not
. Each carries a // TODO: comment in source naming
the blocker:

  • BorderHandler (88%) — StrokeDashOffset, StrokeDashPattern,
    StrokeLineCap, StrokeLineJoin, StrokeMiterLimit. Dashed strokes
    need a custom DrawScope-based modifier; Compose's stock
    Modifier.Border doesn't expose dash configuration.
  • RadioButtonHandler (92%) — CornerRadius, StrokeColor,
    StrokeThickness. Compose Material 3 RadioButton has no border
    or corner slot at all — needs a custom radio composable.
  • ScrollViewHandler (94%) — HorizontalScrollBarVisibility,
    VerticalScrollBarVisibility. Modifier.verticalScroll doesn't
    expose scrollbar visibility (Android shows them implicitly on touch).
  • ImageHandler / ImageButtonHandler (97%) — IsAnimationPlaying.
    Compose has no built-in animated image; needs coil-compose or
    similar.
  • RefreshViewHandler (97%) — RefreshColor. Theming the spinner
    needs PullToRefreshDefaults.Indicator slot override.
  • SliderHandler (97%) — ThumbImageSource. Compose Slider has
    no thumb-image slot short of writing a custom thumb composable.
  • LabelHandler / PickerHandler / EntryHandler / EditorHandler
    / SearchBarHandler
    (each 98%) — VerticalTextAlignment. Compose's
    Box facade in this repo doesn't yet expose the contentAlignment
    slot (always passes null), so we can't wrap the text-field /
    label and align it. The mapper key is intentionally absent so it
    shows up in the gap analysis until the facade gains the slot.

Verification

  • dotnet build src/Microsoft.AndroidX.Compose.Maui — clean.
  • dotnet run scripts/maui-coverage.cs — regenerated
    docs/maui-coverage.md; numbers above match the report.
  • Spot-checked Entry text-field key behaviour by reading the
    RenderWithSelection interception path; _suppressMirror guards
    the bidirectional Compose ⇄ MAUI loop and the
    ApplySelection/MapText paths skip writes that wouldn't change
    Compose's actual TextRange, so the mirror is stable across
    recompositions.

Out of scope (deliberately not touched)

  • New red ❌ handlers (FlyoutViewHandler, NavigationViewHandler,
    ShapeViewHandler, WebViewHandler, menus, App / Window) — those
    remain larger separate PRs.
  • The scripts/maui-coverage.cs parser — no false-positive key
    categories surfaced.
  • Anything under src/Microsoft.AndroidX.Compose/ (facade); only the
    MAUI backend handlers under src/Microsoft.AndroidX.Compose.Maui/
    changed.

jonathanpeppers and others added 4 commits June 12, 2026 17:59
…icker + TODOs

Wires the trivial single-key gaps from `docs/maui-coverage.md`'s
"Partial handlers (gap analysis)" section:

* `PageHandler.Title`: registered as a no-op (matches stock MAUI's
  empty `MapTitle`).
* `LayoutHandler.ClipsToBounds`: `Modifier.Clip(RectangleShape)` on
  the outer container when true.
* `IndicatorViewHandler.HideSingle` / `MaximumVisible`: collapse the
  dot strip when count <= 1 and cap rendered dots respectively.
* `DatePickerHandler.IsOpen` (two-way) + `CharacterSpacing` on the
  trigger label.
* `TimePickerHandler.IsOpen` (two-way) + `CharacterSpacing`.
* `RefreshViewHandler.IsRefreshEnabled`: swallow the pull-down
  gesture when false.
* `ScrollViewHandler.Content`: version-bump slot so content swaps
  re-walk the child tree.
* `RadioButtonHandler.CharacterSpacing`: TextStyle letter-spacing
  on the sibling label.

Explicit `// TODO` hold-backs (left unmapped so the coverage report
flags the gap instead of claiming a no-op wire):

* `ImageHandler` / `ImageButtonHandler.IsAnimationPlaying` — needs
  coil-compose.
* `SliderHandler.ThumbImageSource` — needs custom-thumb composable.
* `RefreshViewHandler.RefreshColor` — needs PullToRefreshBox color
  facade extension.
* `ScrollViewHandler.{Horizontal,Vertical}ScrollBarVisibility` —
  Compose `Modifier.{vertical,horizontal}Scroll` has no scrollbar
  visibility hook.
* `RadioButtonHandler.{CornerRadius,StrokeColor,StrokeThickness}` —
  Material 3 RadioButton chrome is fixed.

Coverage moves 24/43 -> 29/43 handlers (67.4%), 830/1224 -> 841/1224
keys (68.7%). Five handlers go green (Page, Layout, IndicatorView,
DatePicker, TimePicker).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Label: CharacterSpacing, LineHeight, Padding, TextDecorations.

Picker: CharacterSpacing, HorizontalTextAlignment, IsOpen (two-way), Items alias.

Both still hold back VerticalTextAlignment (TODO; needs Box contentAlignment).

Coverage: 841 -> 849 / 1224 keys (68.7%% -> 69.4%%).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Button: CharacterSpacing, CornerRadius, Font, Padding, Source (via

ImageSourceLoader), StrokeColor, StrokeThickness. Now 100%% (40/40).

Border: Shape alias for the abstract IBorderStroke.Shape key. The

5 dashed-stroke / cap / join / miter keys stay TODO — Compose's

Modifier.Border is solid-only and faithful dashed strokes need a

custom drawWithCache shader (much larger refactor).

Coverage: 849 -> 857 / 1224 keys (69.4%% -> 70.0%%).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor the three text-input handlers (Entry, Editor, SearchBar) onto Compose's MutableState<TextFieldValue> overload via a TextFieldValueState : MutableState<TextFieldValue> subclass that intercepts writes to enforce ITextInput.MaxLength and mirror text + caret/selection back to the MAUI virtual view.

New keys wired: CharacterSpacing, HorizontalTextAlignment, IsSpellCheckEnabled, IsTextPredictionEnabled, MaxLength, PlaceholderColor, ReturnType (Entry/SearchBar), CursorPosition, SelectionLength, ClearButtonVisibility (Entry trailing X), SearchIconColor + CancelButtonColor (SearchBar).

Deliberate TODOs (left unmapped to surface in the coverage report):

- VerticalTextAlignment on Entry/Editor/SearchBar/Label/Picker: Box facade doesn't yet expose contentAlignment slot, so we can't wrap and align.

Coverage: 857 -> 883 / 1224 keys (70.0%% -> 72.1%%).

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

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 significantly improves property-mapper key coverage across the Compose-backed MAUI handlers, raising overall coverage from 67.8% to 72.1% (830 → 883 keys). No new handler registrations are added — the focus is exclusively on wiring missing mapper keys in the 24 existing handlers. The bulk of the work centers on the text-input handlers (Entry, Editor, SearchBar), which are refactored to use Compose's OutlinedTextField(MutableState<TextFieldValue>) overload via a custom TextFieldValueState subclass that enforces MaxLength and mirrors cursor/selection state bidirectionally with MAUI.

Changes:

  • Wired ~53 new property-mapper keys across 15 handlers (CharacterSpacing, CursorPosition, SelectionLength, IsOpen, HideSingle, MaximumVisible, ClipsToBounds, PlaceholderColor, StrokeColor/Thickness, CornerRadius, Font, Padding, Content, Title, etc.), bringing multiple handlers to 100% coverage.
  • Refactored Entry/Editor/SearchBar onto Compose's TextFieldValue-based OutlinedTextField overload with a TextFieldValueState : MutableState<TextFieldValue> subclass that intercepts writes for MaxLength enforcement and two-way MAUI synchronization of text, cursor, and selection.
  • Added TODO comments documenting deliberate gaps (dashed strokes, scrollbar visibility, animated images, thumb images, vertical text alignment) so the coverage report flags them honestly rather than hiding behind no-op mappers.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
ButtonHandler.cs 7 new keys: CharacterSpacing, Font, CornerRadius, StrokeColor/Thickness, Padding, Source (image via ImageSourceLoader)
EntryHandler.cs Refactored to TextFieldValueState; added CursorPosition, SelectionLength, MaxLength, ClearButton, ReturnType, PlaceholderColor, CharacterSpacing, AutoCorrect, HorizontalTextAlignment
EditorHandler.cs Parallel refactor to TextFieldValueState; added CursorPosition, SelectionLength, CharacterSpacing, AutoCorrect, PlaceholderColor, HorizontalTextAlignment
SearchBarHandler.cs Parallel refactor to TextFieldValueState; added SearchIconColor, CancelButtonColor, ReturnType, MaxLength, CursorPosition, SelectionLength, CharacterSpacing, AutoCorrect, PlaceholderColor, HorizontalTextAlignment
LabelHandler.cs CharacterSpacing, LineHeight, TextDecorations, Padding (via version-counter live-read)
PickerHandler.cs CharacterSpacing, HorizontalTextAlignment, IsOpen (two-way), Items alias
DatePickerHandler.cs CharacterSpacing, IsOpen (two-way via SetOpen helper)
TimePickerHandler.cs CharacterSpacing, IsOpen (two-way via SetOpen helper)
IndicatorViewHandler.cs HideSingle, MaximumVisible
RadioButtonHandler.cs CharacterSpacing
LayoutHandler.cs ClipsToBounds via Modifier.Clip(RectangleShape)
ScrollViewHandler.cs Content mapper + version-counter for child-tree swaps
RefreshViewHandler.cs IsRefreshEnabled (gates onRefresh callback)
PageHandler.cs Title (deliberate no-op for mapper parity)
BorderHandler.cs Shape alias key → MapShape
ImageHandler.cs TODO comment for IsAnimationPlaying
ImageButtonHandler.cs TODO comment for IsAnimationPlaying
SliderHandler.cs TODO comment for ThumbImageSource
docs/maui-coverage.md Regenerated coverage report reflecting new numbers

Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/LabelHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/TimePickerHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/DatePickerHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/RadioButtonHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/EntryHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/EditorHandler.cs Outdated
Comment thread src/Microsoft.AndroidX.Compose.Maui/Handlers/SearchBarHandler.cs Outdated
jonathanpeppers and others added 3 commits June 15, 2026 11:04
For each of the new keys wired in this PR, add a XAML section to the existing demo page that exercises the mapper visibly:

- EntriesPage: CharacterSpacing, HorizontalTextAlignment (Start/Center/End), MaxLength (with overflow-rejection echo), ClearButtonVisibility, ReturnType (Done/Go/Next/Search/Send), IsSpellCheckEnabled+IsTextPredictionEnabled, PlaceholderColor, CursorPosition+SelectionLength (button-driven caret control).

- EditorPage: CharacterSpacing, HorizontalTextAlignment Center+End, IsSpellCheckEnabled+IsTextPredictionEnabled, PlaceholderColor, CursorPosition+SelectionLength.

- SearchPage: CharacterSpacing, HorizontalTextAlignment, MaxLength, IsSpellCheckEnabled+IsTextPredictionEnabled, PlaceholderColor, SearchIconColor, CancelButtonColor, ReturnType.

- ButtonsPage: CharacterSpacing, CornerRadius (Pill / Sharp), Font (large bold), Padding, StrokeColor+StrokeThickness (outlined / heavy), ImageSource (dotnet_bot).

- LabelsPage: CharacterSpacing, LineHeight (vs default), Padding chip, TextDecorations (Underline / Strikethrough / both).

- PickersPage: Picker.CharacterSpacing, Picker.HorizontalTextAlignment, DatePicker.CharacterSpacing, TimePicker.CharacterSpacing.

- IndicatorPage: HideSingle (True hides single dot, False keeps it), MaximumVisible=3 with Count=10.

- RefreshPage: IsRefreshEnabled toggle that disables the pull gesture.

Spot-checked on a Pixel device: the app starts cleanly, Buttons + Labels demos render the new mapper effects visibly (CornerRadius pill/sharp, StrokeColor outlined/heavy, ImageSource leading icon, CharacterSpacing wide letters, LineHeight 2x vs default, TextDecorations underline/strikethrough).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- LabelHandler: reorder modifier chain so view properties (background/shadow/opacity) sit on the outermost modifier and Padding is innermost. Matches the ModifierBridge convention and LayoutHandler's documented ordering, so MAUI's Label.BackgroundColor now covers the full label rectangle including padding instead of only the inner content area.

- TimePickerHandler / DatePickerHandler / RadioButtonHandler: gate CharacterSpacing on '!= 0' instead of '> 0'. MAUI's CharacterSpacing is a double that can be negative (tighter kerning); using '> 0' silently dropped negative values and was inconsistent with the six other handlers in this PR.

- EntryHandler / EditorHandler / SearchBarHandler TextFieldValueState.Value setter: replace null-forgiving 'value!' with '?? throw new InvalidOperationException(...)' carrying a message that names the offending state-holder. The '!' operator is prohibited by the coding guidelines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers merged commit 8811a63 into main Jun 15, 2026
1 check passed
@jonathanpeppers jonathanpeppers deleted the jonathanpeppers/fill-maui-mapper-keys branch June 15, 2026 19:03
jonathanpeppers added a commit that referenced this pull request Jun 16, 2026
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 `DrawScope` → `DrawContext` → `ICanvas` → `Android.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])`
  - `Ellipse` → `Canvas.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.cs` — `BorderHandler` row flips to ✅ 100 %.
- On-device: not run in this session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants