Repository navigation
feat: composable/lambda extension points for tooltip, connector, and cutout rendering - #53
Merged
Merged
Conversation
…r rendering Adds three ways for app code to customize rendering without a new enum case or config field in :lumen: - CoachmarkConfig.progressIndicator: optional @composable slot replacing the built-in progress dots. - CoachmarkConfig.customConnector: optional DrawScope lambda taking over the entire connector (line + endpoint) given the two anchors and reveal progress. ConnectorStyle/ConnectorEndStyle are ignored while set. - CutoutShape.Custom: pathBuilder case for cutout outlines that aren't one of the built-ins. Draws through the same BlendMode.Clear offscreen-layer path as every built-in shape (including the PULSE/GLOW/BOUNCE stroke scale animation) so a custom shape isn't a second-class citizen. All three default to null/absent, so every existing caller is unaffected. Sample: new "Custom Rendering" example rebuilding a pill progress indicator, a teardrop speech-bubble connector, and a diamond cutout entirely in app code, to demonstrate the extension points replace per-feature library additions. Bump to 1.0.0-beta16.
Adds ProgressIndicatorStyle (DOTS default / PILL) so the current step in a multi-step tooltip's progress indicator can render as an elongated pill instead of a same-size dot. Kept as a built-in convenience layered on top of the new CoachmarkConfig.progressIndicator slot: PILL covers the common case out of the box, the slot covers everything else. New CoachmarkConfig.progressIndicatorStyle and CoachmarkConfig.progressActivePillWidth (default 20.dp) control it. No new color config needed - reuses existing CoachmarkColors.progressActiveColor / progressInactiveColor. DOTS behavior is unchanged. Added a "Start Tour (Pill)" button to the Multi-Step Sequence sample.
aldefy
added a commit
that referenced
this pull request
Aug 20, 2026
…theme contrast fix (#54) * feat: per-target ConnectorStyle.CUSTOM, sample teardrop tail demo Dry-run for re-scoping #51 (ConnectorStyle.TEARDROP) onto #53's customConnector extension point. Finding: config.customConnector from #53 is sequence-wide only, with no per-target override. Paul's TEARDROP could mix a teardrop connector with an elbow connector in the same sequence (per-target CoachmarkTarget field); customConnector alone could not. This fixes that gap: - ConnectorStyle.CUSTOM: new enum case, the generic gate for full-control connector rendering (mirrors the existing ConnectorEndStyle.CUSTOM pattern). resolveConnectorStyle() resolves it through the same heuristic as AUTO for geometry purposes, so the anchors/fallback path stay correct. - CoachmarkTarget.customConnector: per-target override, takes priority over CoachmarkConfig.customConnector when both are set. - CUSTOM with no lambda supplied anywhere falls back to drawing a normal connector line rather than nothing, verified via a new screenshot test. Sample: added a "Teardrop Tail" step to ConnectorsExample using ConnectorStyle.CUSTOM, and a drawTeardropTail() helper — Paul's original "hill" silhouette (compared against a classic pointed teardrop and a true geometric pear/raindrop shape; the hill was the right call for a tail meant to read as continuous with a rounded card, not a separate sharp shape). Verified on-device end to end. Two new CoachmarkScrimScreenshotTest cases cover priority (target lambda wins over config default) and fallback (CUSTOM with nothing supplied still draws a line) behavior. * feat: speech-bubble tooltip shape with tail carved into the card outline Adds CoachmarkConfig.tooltipShape + tooltipTailInset: the tail is one continuous outline with the card (via a Shape), not a separately-drawn nub next to it — matches a real speech-bubble reference design where the tail reads as part of the card, no seam. - shapes/SpeechBubblePath.kt: SpeechBubbleShape + speechBubblePath(), following the existing StarPath/SquirclePath convention. The card body is sized to (measured size - tailInset) so the tail draws inside the Shape's own bounds rather than bumping past them, since clip() always bounds to the actual layout size. - CoachmarkTooltip gains tailAnchorX/tooltipShape/tooltipTailInset params. A Spacer inside the Column reserves tooltipTailInset on the tail's side so the Column's measured size includes room for the shape's tail. - Scrim computes tailAnchorX the same way the existing inline-dot-X code does: target center X relative to the tooltip's left edge. Sample: CustomRenderingExample's step 2 now uses tooltipShape instead of customConnector for the speech-bubble tail (customConnector kept only as a no-op to suppress the built-in line, since ConnectorStyle has no NONE case today - a real gap worth a follow-up). Two bugs found and fixed against on-device verification, not just visual inspection: 1. Tail direction was inverted (isTooltipBelow=true was mapped to tailOnBottom=true; it should be the opposite - tooltip below the target means the tail points UP, on the top edge, toward it). 2. tooltipCardColor in DarkCoachmarkColors and the default CoachmarkColors failed WCAG contrast against scrimColor: DarkCoachmarkColors measured 1.19:1, default measured 1.38:1 (need >=3:1 for a UI boundary), because both defaulted to a near-black card over a near-black scrim. The default's titleColor = Color.Black on a dark card additionally failed text contrast outright (1.38:1, needs >=4.5:1) - that one predates this change and was previously undetected because the sample never exercised showTooltipCard=true with the plain default colors. Fixed by moving both dark-mode-adjacent card colors to #666666 and fixing the default's title/description colors to white/near-white to match its (now-legible) dark card. Verified: card-vs-scrim >=3.1:1, title-vs-card >=6.2:1, description-vs-card >=3.86:1 in both palettes. LightCoachmarkColors was already fine (17.26:1) and is untouched. Re-recorded the 12 tooltip_* goldens affected by the card color change. Full suite green: 232 Android + 201 JVM tests, apiCheck passes. * feat(sample): speech-bubble tail on the LazyColumn whole-row target Adds CoachmarkConfig.tooltipShape to the LazyColumn example's existing "Item Cards" step (list_card), which already targets a whole row via CutoutShape.RoundedRect. This is the closest existing target to the reference speech-bubble tooltip design - a whole list row, tooltip above it, tail carved into the card's own bottom edge pointing down at the row, no connector line. Verified on-device against a real scrollable list. Also confirmed and documented a real gap while building this: tooltipShape only supports a top or bottom tail (SpeechBubbleShape has no left/right case), and CoachmarkScrim always computes tailAnchorX from target.bounds.center.x regardless of connector direction. Verified live with a temporary HORIZONTAL-connector target: the line correctly points sideways at the target while the tail sits meaninglessly on the bottom edge, disconnected from what the line points at. Left as a known limitation for a future PR rather than silently working around it - tooltipShape should not be combined with HORIZONTAL-style targets today. * chore: bump to 1.0.0-beta17 CHANGELOG entry for the tooltipShape / per-target CUSTOM connector / contrast-fix work in this branch.
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.
Summary
Adds three general extension points so app- and brand-specific rendering doesn't need a new enum case or config field added to
:lumenevery time:CoachmarkConfig.progressIndicator— optional@Composableslot replacing the built-in progress dots entirely. Receives(currentStep, totalSteps).null(default) keeps existing behavior.CoachmarkConfig.customConnector— optionalDrawScopelambda that takes over the whole connector (line + endpoint), given the cutout-edge anchor, tooltip-edge anchor, and reveal progress (0f..1f).ConnectorStyle/ConnectorEndStyleare ignored while set.CutoutShape.Custom— apathBuildercase for cutout outlines that aren't Circle/RoundedRect/Rect/Squircle/Star. Draws through the sameBlendMode.Clearoffscreen-layer path as every built-in shape, including the PULSE/GLOW/BOUNCE stroke-scale animation.Also keeps
ProgressIndicatorStyle.PILL(from #52) as a built-in convenience layered on top of the new slot — it's a common enough idiom (iOS-style page indicator) to ship directly, while the slot covers everything else.Why
Reviewing #51 (TEARDROP connector) and #52 (PILL indicator) against the actual design screens driving them showed a pattern: each new visual requirement was landing as a new sealed enum case + several
CoachmarkConfigfields in the library, for something entirely expressible in app code.CoachmarkConfigwas already at 40 fields before those two PRs.CutoutShapeissealedwith no escape hatch.CoachmarkTooltiphas 34 params, all primitives, zero@Composableslots.This PR adds the missing general escape hatches instead. Verified all three on-device (Pixel 9 Pro Fold) via a new "Custom Rendering" sample rebuilding a pill indicator, a teardrop speech-bubble connector, and a diamond cutout — none of it touching
:lumen's enum surface.Compatibility
All three new fields default to
null/absent. Every existing caller is unaffected — confirmed by the full test suite (230 Android/Robolectric + 201 JVM, all passing, including every existing Roborazzi golden byte-for-byte unchanged).Test plan
./gradlew :lumen:apiCheck— passes, dumps regenerated./gradlew :lumen:testDebugUnitTest :lumen:jvmTest— 230 + 201 tests, 0 failures./gradlew :lumen:compileKotlinMetadata :lumen:compileDebugKotlinAndroid :lumen:compileKotlinJvm :sample:compileDebugKotlinAndroid :sample:compileKotlinWasmJs— all green