Skip to content

feat: make the accumulators barriers draggable - #500

Open
behnam-deriv wants to merge 10 commits into
masterfrom
feat/draggable-accumulators-barriers
Open

behnam-deriv wants to merge 10 commits into
masterfrom
feat/draggable-accumulators-barriers

Conversation

@behnam-deriv

@behnam-deriv behnam-deriv commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

What

Lets consumer apps offer growth-rate adjustment by dragging either accumulators barrier, rather than only through a separate trade-params control. Opt-in: without a controller the barriers render exactly as they do today.

Design: whole flow · barrier component

How it's used

// Created once by the consumer and kept alive — the annotation around it is
// rebuilt on every tick.
final controller = AccumulatorBarrierDragController(
  onDragEnd: (step) => commitGrowthRate(step.growthRate),
)..steps = ladderOfSelectableGrowthRates;

AccumulatorIndicator(tick, ..., dragController: controller);

The chart snaps the band to the nearest rung as the user drags and reports it back. Committing the value and pushing the real barriers back down stays the consumer's job.

Design notes

  • The controller is consumer-owned because it has to outlive the annotation. It also carries the grip style — ChartTheme is a pure interface, so adding a getter there would break any app that implements it.
  • The painter publishes the geometry it resolved for the frame it drew, so hit-testing matches the pixels even during the post-tick lerp. Recomputing positions in the overlay would disagree with the painter mid-animation.
  • The preview latches past drag end until the model actually moves. Without it the band rubber-bands to its pre-drag width while the consumer's commit is in flight, then animates forward again. A timeout drops the latch if the commit never lands.
  • recalculateMinMax follows the preview, so a wider preview cannot paint outside the visible quote range.
  • The gesture recognizer claims the pointer only over a grip or its barrier line, which beats the chart's pan/scale recognizer; the quote-label strip is left to the Y-axis gesture. It passes the hit transform to startTrackingPointer, without which every drag position is global rather than local.
  • Grips are hidden while a contract is active, and the overlay mounts independently of useDrawingToolsV2.

Public API

New exports: AccumulatorBarrierDragController, AccumulatorGrowthRateStep, AccumulatorBarrierGripStyle, AccumulatorBarrierSide, plus the dragController parameter on AccumulatorIndicator. All additive — nothing existing changes shape.

Testing

  • flutter analyze --no-fatal-infos — clean (8 pre-existing infos in unrelated files)
  • flutter test — 211 passed, 26 of them new: snapping and hysteresis, hit-testing, the commit latch, recalculateMinMax under a preview, and widget tests covering the drag winning the gesture arena over a competing pan.

🤖 Generated with Claude Code

Summary by Sourcery

Enable consumers to make Accumulators barriers draggable while preserving existing rendering and behavior when the feature is not configured.

New Features:

  • Add opt-in draggable Accumulators barriers with consumer-configurable growth-rate ladders and drag callbacks.
  • Expose configurable barrier grip styling and public controller, step, and barrier-side APIs.

Bug Fixes:

  • Keep drag hit-testing aligned with the geometry rendered during barrier animations.
  • Prevent preview bands from exceeding the visible quote range and avoid rubber-banding while consumer commits are applied.

Enhancements:

  • Integrate barrier dragging with gesture arbitration, hover highlighting, snapping hysteresis, preview animation, and active-contract behavior.
  • Allow vertical chart scale configuration to be applied and clamped without overriding unrelated user zoom gestures.

Tests:

  • Add unit and widget coverage for snapping, hysteresis, hit-testing, gesture precedence, preview latching, ladder updates, quote-range recalculation, and vertical scaling.

Adds an opt-in way for consumer apps to let users adjust the growth rate by
dragging either accumulators barrier, instead of only through a separate
trade-params control.

A consumer creates an `AccumulatorBarrierDragController`, feeds it the ladder
of selectable growth rates, and passes it to `AccumulatorIndicator`. The chart
snaps the band to the nearest rung as the user drags and reports the rung back;
committing the value and pushing the real barriers down stays the consumer's
job. Without a controller the barriers render exactly as before.

Notable details:

- The controller is owned by the consumer because it has to outlive the
  annotation, which is rebuilt on every tick. It also carries the grip style:
  `ChartTheme` is a pure interface, so adding a getter there would break any
  app that implements it.
- The painter publishes the geometry it resolved for the frame it drew, so
  hit-testing matches the pixels even during the post-tick lerp.
- A drag preview latches past drag end until the model moves, otherwise the
  band rubber-bands back to its pre-drag width while the consumer's commit is
  in flight. A timeout drops the latch if that commit never lands.
- `recalculateMinMax` follows the preview, so a wider band cannot paint outside
  the visible quote range.
- The gesture recognizer claims the pointer only over a grip or its barrier
  line, which beats the chart's pan/scale recognizer, and passes the hit
  transform so drag positions stay local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds an opt-in, consumer-controlled draggable Accumulators barrier experience: the painter supplies frame-accurate geometry, an overlay eagerly captures only valid barrier targets, movement snaps to configured growth-rate steps, and a commit latch keeps previews stable until the model catches up or times out, with comprehensive controller and widget coverage.

Sequence diagram for dragging and committing Accumulators barriers

sequenceDiagram
    participant User
    participant Overlay as AccumulatorBarrierDragOverlay
    participant Recognizer as AccumulatorBarrierGestureRecognizer
    participant Controller as AccumulatorBarrierDragController
    participant Chart as MainChart
    participant Consumer

    User->>Recognizer: Pointer down on grip or barrier line
    Recognizer->>Controller: geometry.hitTest()
    Recognizer->>Recognizer: startTrackingPointer(event.pointer, event.transform)
    Recognizer->>Controller: beginDrag(side)
    Controller-->>Consumer: onDragStart()
    User->>Recognizer: Pointer move
    Recognizer->>Overlay: onBarrierDragUpdate(local)
    Overlay->>Controller: nearestStep(distance, hysteresis)
    Overlay->>Controller: updateDrag(step)
    Controller-->>Chart: notifyListeners()
    Chart->>Chart: recalculateMinMax() using preview
    User->>Recognizer: Pointer up
    Recognizer->>Overlay: onBarrierDragEnd()
    Overlay->>Controller: endDrag(commit: true)
    Controller-->>Consumer: onDragEnd(step)
    Controller->>Controller: latch preview and start commitTimeout
    Consumer->>Chart: Push committed barriers
    Chart->>Controller: releaseLatchIfModelMoved(highBarrier, lowBarrier)
    Controller->>Controller: clearPreview()
Loading

State diagram for the Accumulators barrier drag controller

stateDiagram-v2
    [*] --> Idle
    Idle --> Hovered: setHovered(side)
    Hovered --> Idle: setHovered(null)
    Idle --> Dragging: beginDrag(side)
    Hovered --> Dragging: beginDrag(side)
    Dragging --> Dragging: updateDrag(step)
    Dragging --> PreviewLatched: endDrag(commit: true)
    Dragging --> Idle: endDrag(commit: false)
    PreviewLatched --> Idle: releaseLatchIfModelMoved()
    PreviewLatched --> Idle: clearPreview()
    PreviewLatched --> Idle: commitTimeout
    Idle --> Disabled: enabled = false
    Hovered --> Disabled: enabled = false
    Dragging --> Disabled: enabled = false
    PreviewLatched --> Disabled: enabled = false
    Disabled --> Idle: enabled = true
Loading

File-Level Changes

Change Details Files
Add a consumer-owned controller API for configuring and observing draggable barrier interactions.
  • Introduce growth-rate ladder steps, barrier-side enum, and configurable grip styling.
  • Track enabled, hover, drag, preview, callbacks, geometry, and commit-timeout latch state.
  • Export the new public types and add the optional controller to AccumulatorIndicator.
lib/deriv_chart.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_drag_controller.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_grip_style.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_side.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_growth_rate_step.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulators_indicator.dart
Implement pixel-accurate barrier painting, preview rendering, and geometry publication for interaction hit-testing.
  • Render configurable grips and highlighted barrier/band states only when dragging is enabled and no contract is active.
  • Use preview distances for barrier positions, labels, and visible quote-range recalculation.
  • Publish resolved painter geometry so overlay hit-testing follows animated pixels and support model-movement/timeout release of committed previews.
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulators_indicator_painter.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_geometry.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_drag_controller.dart
Add an independent transparent drag overlay and eager gesture recognizer that competes correctly with chart interactions.
  • Hit-test grips and barrier lines with device-specific tolerances while excluding the quote-label strip.
  • Snap pointer movement to the nearest growth-rate rung with pixel-derived hysteresis.
  • Pass transformed local pointer coordinates and eagerly claim valid barrier gestures over pan/scale.
  • Mount the overlay independently of drawing tools and preserve/restore X-axis scroll blocking during a drag.
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_drag_overlay.dart
lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_gesture_recognizer.dart
lib/src/deriv_chart/chart/main_chart.dart
Add coverage for snapping, gesture ownership, preview lifecycle, hit-testing, and range recalculation.
  • Test nearest-step snapping and hysteresis plus drag callbacks and cancellation.
  • Test commit latching, model-movement release, timeout behavior, disabled/active-contract behavior, and geometry hit targets.
  • Test widget gesture-arena precedence, pan pass-through, label-strip exclusion, and controller opt-in behavior.
test/deriv_chart/chart/data_visualization/accumulator_barrier_drag_overlay_test.dart
test/deriv_chart/chart/data_visualization/accumulator_barrier_drag_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Manifest Files

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 4m 49s —— View job


I'll analyze this and get back to you.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/src/deriv_chart/chart/main_chart.dart" line_range="192-194" />
<code_context>
+  /// The accumulators annotation the user is allowed to drag, if any.
+  AccumulatorIndicator? get _draggableAccumulator =>
+      widget.annotations?.whereType<AccumulatorIndicator>().firstWhereOrNull(
+            (AccumulatorIndicator indicator) =>
+                (indicator.dragController?.enabled ?? false) &&
+                indicator.activeContract == null,
+          );
+
</code_context>
<issue_to_address>
**issue (bug_risk):** An enabled controller with an empty `steps` ladder still mounts the overlay and claims pointer events over the barrier lines or grips, but `nearestStep` returns null, so the drag produces no preview or callback while preventing the chart's pan/scale gesture.

**Triggers:** When a consumer constructs the controller before populating `steps`, or intentionally supplies an empty ladder.

**Suggested fix:** Only mount and hit-test the overlay when `controller.steps` is non-empty, or reject the gesture when the ladder is empty.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/src/deriv_chart/chart/main_chart.dart
Four refinements from testing the drag end to end.

Scale the drag to the ladder. Real barrier offsets are nearly flat across
growth rates — the ladder spans ~26% from its tightest rung to its widest — so
mapping the pointer straight onto the barrier put every growth rate within a
few pixels of travel. The gesture is now scaled so the whole ladder takes
`ladderTravel` pixels, leaving the band on true barrier positions at the cost
of the grip no longer tracking the finger exactly. A ladder already wider than
that target is left at 1:1, since reducing sensitivity would only hurt.

Hold ladder updates back for the duration of a drag. Barrier distances move
with the spot, so a ladder refreshing on every tick shifted the snap targets
under the user's finger — and the rungs sit close enough together that a rung
could change with the pointer completely still.

Stop hover from rebuilding the chart. Every controller notification drove a
setState on MainChart, so crossing a barrier re-ran updateVisibleData twice,
recomputed min/max across every series and annotation, and rebuilt the whole
stack — to change a cursor and a stroke width, which showed up as a CPU spike
on a profiler. Hover now repaints only the annotation layer and rebuilds only
the overlay; the expensive path is reserved for a moved band, which is the one
case that genuinely needs the quote bounds re-measured.

Glide between rungs instead of stepping. The band now animates on each snap,
and on the commit, matching what already happens when the growth rate is
changed from the trade params. It cannot share that animation controller —
playNewTickAnimation is a no-op while an animation is in flight and rungs are
crossed far faster than it runs — so it reuses the duration and curve through
its own. The transition starts from the distance last painted, so changing
rung mid-glide carries on from where the band actually is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 1m 39s —— View job


I'll analyze this and get back to you.

… Y axis

The grips were small and sat in the middle of the band, so a finger dragging
one covered the barrier value it was changing — the one number the gesture
exists to adjust.

They are now 56x18 rather than 31x12, which with the existing 44px minimum
touch target makes the tap area 56x44, and they are pinned to the right of the
plotting area instead of centred. The Y-axis label strip is excluded from that
edge, so the grips sit beside the axis rather than under its labels; the
painter had no access to the plotting width, so the overlay publishes it to the
controller the same way the painter publishes geometry back. Right-aligning is
clamped so a band narrower than the grip still holds it inside.

The grip fill now defaults to the chart's own background instead of a
hardcoded near-white, so it stops being a white block on a dark chart. In the
default themes that resolves to exactly the colour the designs use in dark
mode.

Placement moved into `AccumulatorBarrierGeometry.gripCenterX` so it can be
tested without a golden — buried in `onPaint` it could only have been verified
by eye.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 1m 38s —— View job


I'll analyze this and get back to you.

The consumer needs to know which barrier the user has hold of: which grip is
in hand decides which way they must drag to leave an end of the ladder — at
the tightest band the top grip goes up and the bottom one goes down. Carry
the side through onDragStart/onDragUpdate/onDragEnd rather than making the
consumer infer it.

Also fold the "at least two rungs" rule into `enabled`. A one-rung ladder has
nowhere to drag to, and grips the user cannot move are worse than no grips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 1m 27s —— View job


I'll analyze this and get back to you.

`verticalPaddingFraction` was read once in initState, so a consumer could
choose the scale the chart opened at but never change it afterwards. It now
re-applies when the value changes, which is what lets the scale follow
something like a trade-type switch without the chart being recreated.

Only on a change: the value the user drags to lives in the same field, so
re-applying on every rebuild would fight their gesture.

Also names the clamp bounds the quote-label drag already used, and clamps the
incoming value to them, so a consumer cannot ask for a scale the user would be
unable to drag back to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 1m 52s —— View job


I'll analyze this and get back to you.

Tapping a grip and letting go without moving left the consumer's readout on
screen for good. `endDrag` returned early when nothing had been previewed, so
`onDragEnd` never fired — but `onDragStart` had already fired on touch-down,
and that is what puts the readout up.

It now reports on every release: the settled step when there is one, and
otherwise the rung the band is already on, which is where the user let go.
That also covers a cancelled gesture, which had the same bug in a worse form —
the previewed rate stayed on the readout with nothing to clear it.

Also stops a release from re-highlighting the barrier it just ended on.
Letting go does not move the pointer, so the first hover afterwards lands back
on the same grip — on touch as well, via the synthetic mouse event a browser
fires after a tap. Hover is ignored at that spot until the pointer genuinely
moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 2m 31s —— View job


I'll analyze this and get back to you.

Coming back from the background, the Accumulators band slid across the chart
to catch up with the spot instead of simply being there.

An annotation eases from `previousObject`, and frames stop while the app is
away — so the position it was left in is as old as the absence, and the whole
backlog lands in one rebuild. Easing from a position that has since scrolled
out of view is a sweep across the chart, not a step.

Dropped in `onUpdate`, which already asks the same question of the annotation
itself one line above. Annotations whose object leaves an epoch bound null — a
plain horizontal barrier spans the chart rather than sitting at a point in
time — are unaffected, since they can never scroll out of view.

Follows the same shape as the disjoint-range and stale-prevLastEntry fixes:
decide from the data whether the new position continues the old one, rather
than from an out-of-band signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 3m 29s —— View job


I'll analyze this and get back to you.

The barriers are no longer dragged directly. The whole band is a tap target
instead, and the host puts its own control on screen when the tap is reported,
moving the band from there with previewGrowthRate. Dragging stays implemented
and tested, but is off unless a consumer opts in, so by default there are no
grips and a press that turns into a pan still pans.

The press is reported as well as the tap, because a host that decides what a
gesture means from a document listener in the capture phase has already acted
by the time the tap is known.

While a rate is being chosen the two barrier values are the thing the user is
reading, so they grow and thicken, then settle back. They grow away from their
own barrier rather than outward from their middle: anchored on the centre, half
of every pixel of growth walked the text onto the line it labels. The + label
pins its baseline and the - label its top edge, so whichever edge faces the
barrier is the one that stays put, and both grow right into the empty chart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 9m 13s —— View job


I'll analyze this and get back to you.

A hand on a disc inside the band's top-left corner, pulsing until the band is
tapped. Off unless the consumer asks for it: the chart draws and animates the
hint, and remembering whether this user has already been shown it is the
consumer's.

Two halo rings travel out from the disc half a cycle apart, easing out so they
read as emitted rather than as circles that grow, and the disc swells as each
one leaves so the ping looks like it comes off the badge. One ping a second.
The loop only runs while a hint is actually up — otherwise it would repaint the
annotation layer every frame, all session, for nothing.

Anchored to the band's top-left rather than centred, so it stays put as the
band widens and narrows with the growth rate, and centred instead when a band
is too short to hold it without crossing the lower barrier.

The tap that answers the hint retires it in the chart there and then, and
latches it. A consumer goes on sending the old flag for the frame or two before
its own state catches up, and the band republishes every tick, so without the
latch the hint would flick back on over the control the tap just opened.
Sending false releases it, which is also how a consumer asks for the hint back.

The hand is transcribed from the design's SVG into a Path. One icon does not
justify a dependency on every host app bundling a font, and a path scales with
the swell for free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 5m 1s —— View job


I'll analyze this and get back to you.

A previewed band was drawn around the incoming tick's centre outright, with no
ease from the previous one, while barrierX was interpolated right beside it. So
on every tick the band arrived at the new price on the first frame and then
slid horizontally into place: a jump, and only vertically.

The preview owns the band's width, not its position. The centre belongs to the
spot, which keeps moving for as long as the picker is open, so it now eases
whether or not a rung is being previewed and only the two barrier quotes stay
the preview's. The comment that drove this conflated the two.

The geometry still publishes the committed centre: that is the quote-space
reference the drag measures its pointer distance against, so it has to stay
still even as the rendered band moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 3m 39s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

🤖 Claude PR Review Complete

Model: claude-sonnet-5 | Review Type: 🔄 Follow-up Review

Summary

0 of 3 issues from the previous review have been resolved — but this push isn't trying to fix any of them. Commit 7f35ee0 ("fix: glide the accumulators band between ticks while a rate is previewed") is a narrowly-scoped animation fix to AccumulatorIndicatorPainter: previously, a previewed band was drawn around the incoming tick's centre outright with no ease, while barrierX interpolated smoothly beside it — producing a horizontal glide paired with a vertical jump every tick. The fix introduces animatedCenterQuote, lerped between the previous and committed band centres exactly like barrierX already was, and uses it (instead of the static committedCenterQuote) as the anchor for the two preview-owned barrier quotes.

I traced this change end-to-end against the drag math it could have broken: AccumulatorBarrierGeometry.bandCenterQuote is still published as committedCenterQuote (unchanged), which is what the drag overlay anchors its pointer-to-distance math to (accumulator_barrier_drag_overlay.dart:251) — so the fix only changes what's painted, not what the drag measures against. That separation is correct and avoids a subtle regression where the drag's reference point could have drifted mid-animation. The new test file, accumulator_barrier_tick_glide_test.dart, directly reads back the painter's published geometry at several currentTickPercent values and asserts the regression (vertical jump) no longer happens, while a fourth test confirms the band's width still eases correctly when the model itself moves the barriers without any preview active. This is good, targeted coverage for exactly the bug being fixed.

None of this push touches main_chart.dart, x_axis_model.dart, or the gesture recognizer — confirmed directly against the commit's diff, which only modifies the painter and adds the one test file. All three previously-flagged findings carry forward unchanged, in files this push never touched.

Recommendation: APPROVE (this push's own change is a correct, well-tested bug fix; the three carried-over findings are refinements, not blockers — same as prior review)


🟡 Medium Priority Issues (carried over, unresolved)

🟡 1. Pinch-zoom isn't blocked while a barrier is being dragged — lib/src/deriv_chart/chart/main_chart.dart:734-743

Details

Severity File Lines
MEDIUM lib/src/deriv_chart/chart/main_chart.dart 734-743

❌ Problematic Code:

onDragBegin: () {
  crosshairController.onExit(const PointerExitEvent());
  _xScrollBlockedBeforeBarrierDrag = xAxis.isScrollBlocked;
  // A second finger opens a new gesture arena the barrier recognizer
  // does not join, so the chart's scale recognizer could still pan.
  xAxis.isScrollBlocked = true;
  completeCurrentTickAnimation();
},
onDragFinish: () =>
    xAxis.isScrollBlocked = _xScrollBlockedBeforeBarrierDrag,

📋 Issue: isScrollBlocked only guards XAxisModel.onPanUpdate and the momentum path — it does not guard onScaleUpdate, which is what a second finger's pinch actually drives. While one finger drags a barrier grip, a second finger can still zoom the time axis. Still unaddressed; isScaleBlocked does not exist anywhere in lib/.

⚠️ Impact: On touch devices, an incidental second touch during a drag zooms the chart horizontally out from under the user mid-gesture — a jarring glitch during the exact interaction this PR is built around.

✅ Fix:

// x_axis_model.dart
bool _isScaleBlocked = false;
bool get isScaleBlocked => _isScaleBlocked;
set isScaleBlocked(bool value) => _isScaleBlocked = value;

void onScaleUpdate(ScaleUpdateDetails details) {
  if (_isScaleBlocked) {
    return;
  }
  if (_currentViewingMode == ViewingMode.followCurrentTick) {
    _scaleWithNowFixed(details);
  } else {
    _scaleWithFocalPointFixed(details);
  }
}
// main_chart.dart
onDragBegin: () {
  crosshairController.onExit(const PointerExitEvent());
  _xScrollBlockedBeforeBarrierDrag = xAxis.isScrollBlocked;
  xAxis.isScrollBlocked = true;
  xAxis.isScaleBlocked = true;
  completeCurrentTickAnimation();
},
onDragFinish: () {
  xAxis.isScrollBlocked = _xScrollBlockedBeforeBarrierDrag;
  xAxis.isScaleBlocked = false;
},

💡 Explanation: Mirrors the existing isScrollBlocked toggle for scale, closing the gap the code's own comment already identifies for pan.

🟡 2. New MainChart wiring has no test coverage — lib/src/deriv_chart/chart/main_chart.dart

Details

Severity File Lines
MEDIUM lib/src/deriv_chart/chart/main_chart.dart _draggableAccumulator getter, didUpdateChartData release/glide block, animation-controller lifecycle

❌ Problematic Code:

AccumulatorIndicator? get _draggableAccumulator =>
    widget.annotations?.whereType<AccumulatorIndicator>().firstWhereOrNull(
          (AccumulatorIndicator indicator) =>
              (indicator.dragController?.enabled ?? false) &&
              indicator.activeContract == null,
        );

📋 Issue: Same gap as the previous two reviews. This push's new test (accumulator_barrier_tick_glide_test.dart) exercises AccumulatorIndicatorPainter and AccumulatorBarrierDragController directly, which is exactly the right level for the animation bug it fixes — but, like the prior pushes' new tests, it still doesn't reach anything inside MainChart. _draggableAccumulator selection, releaseLatchIfModelMoved inside didUpdateChartData, the previousObject glide-source reconstruction, and the animation controllers' forward/reverse/repeat decisions remain untested at the integration level.

⚠️ Impact: Remains the code most likely to regress silently as more controller-facing state gets threaded through MainChart.

✅ Fix:

Add a MainChart widget test covering:

testWidgets('drag overlay is not mounted once a contract becomes active', ...);
testWidgets('releaseLatchIfModelMoved glides from the rendered preview once the model barriers move', ...);

💡 Explanation: Closes the gap where the previously-tested controller/overlay/painter logic actually gets exercised end-to-end. vertical_padding_fraction_test.dart shows the team already has the harness needed to write this as a MainChart widget test.


🟢 Low Priority Issues (carried over, unresolved)

🟢 3. Gesture recognizer can leave the controller stuck mid-drag if pointer tracking stops abnormally — lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_gesture_recognizer.dart:182-186

Details

Severity File Lines
LOW lib/src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulator_barrier_gesture_recognizer.dart 182-186

❌ Problematic Code:

@override
void didStopTrackingLastPointer(int pointer) {
  _isBarrierHit = false;
  _downPosition = null;
}

📋 Issue: Unchanged — this file isn't touched by this push either. If tracking ever stops through a route other than the explicit up/cancel handling in handleEvent, controller.isDragging can stay true with no further pointer events to clear it, since this override never calls onBarrierDragCancel.

⚠️ Impact: Low likelihood given the overlay's dispose() already force-ends the drag on unmount, but if triggered, barriers appear stuck in the dragged/highlighted state until another drag starts and finishes.

✅ Fix:

@override
void didStopTrackingLastPointer(int pointer) {
  if (_isBarrierHit && !tapOnly) {
    onBarrierDragCancel();
  }
  _isBarrierHit = false;
  _downPosition = null;
}

💡 Explanation: Makes the recognizer self-defensive so controller.isDragging can never outlive the pointer it was tracking, in both the drag and tap modes.


Summary Table

Priority Count Categories
🔴 Critical 0 —
🟠 High 0 —
🟡 Medium 2 Multi-touch interaction gap, missing integration tests
🟢 Low 1 Defensive gesture-recognizer cleanup

Recommendations

  • Add an isScaleBlocked guard (or equivalent) alongside isScrollBlocked so a second-finger pinch can't rescale the chart mid-drag.
  • Add a MainChart-level widget test for _draggableAccumulator selection, releaseLatchIfModelMoved, and the animation controllers' start/stop/forward/reverse transitions.
  • Optionally harden AccumulatorBarrierGestureRecognizer.didStopTrackingLastPointer to fire onBarrierDragCancel when a real drag (not a tap) is interrupted.

None of these block the PR. This push's own change — correctly keeping the drag's quote-space anchor (bandCenterQuote, still committedCenterQuote) stable while fixing only the rendered band centre to glide with the tick animation — is a clean, well-targeted fix with a new test that reproduces the exact regression it closes.


Auto Fix Claude Reviews

Action Open Dashboard

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.

1 participant