Skip to content

[syncfusion_flutter_gauges] SfLinearGauge drag offset issue inside custom RenderObjects with Paint Offset #2486

@scalz

Description

@scalz

Bug description

I have identified a bug in SfLinearGauge where dragging the marker is desynchronized (offset) when the gauge is hosted inside a custom RenderObject that applies a paint offset (drawing the child at a position different from (0,0)) without creating a new transformation layer.

This architecture is common in efficient grid layouts (like custom Slivers) where children are positioned via paint offsets rather than layout positioning.

The issue stems from _handleDragUpdate using details.localPosition. In this specific scenario, the GestureRecognizer may report a local position relative to the parent's origin if the transform isn't handled via a standard RenderTransform. Using globalToLocal(details.globalPosition) fixes this reliably.

Fix:

The issue lies in gauge/linear_gauge_render_widget.dart, specifically in the _handleDragUpdate method.

You should explicitly convert the global position to the local coordinate system using the standard RenderBox.globalToLocal method. This ensures the transformation matrix (including Paint Transforms from Slivers..) is correctly applied.

// In linear_gauge_render_widget.dart

void _handleDragUpdate(DragUpdateDetails details) {
  // OLD CODE (Buggy in Custom Slivers / Paint Transforms):
  // final double currentValue = _getValueFromPosition(details.localPosition);

  // FIX:
  // Convert global touch position to local coordinates explicitly using the RenderBox matrix.
  // This ensures compatibility with parents applying paint transforms (like RenderSlivers).
  final Offset localPos = globalToLocal(details.globalPosition);
  final double currentValue = _getValueFromPosition(localPos);
  
  // ... rest of the logic
}

Steps to reproduce

  1. Run the code.
  2. The Gauge appears shifted to the right (grey background).
  3. Try to drag the marker.
  4. Result: The marker jumps or is offset by ~100px relative to the cursor.

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';

void main() {
  runApp(const MaterialApp(home: App()));
}

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  double _value = 50;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Syncfusion Offset Bug')),
      body: Center(
        // This custom widget paints the child at (100, 0)
        // simulating a slot in a grid/sliver.
        child: OffsetPainterWidget(
          offset: const Offset(100, 0),
          child: Container(
            width: 300,
            height: 100,
            color: Colors.grey.shade200,
            child: SfLinearGauge(
              minimum: 0,
              maximum: 100,
              markerPointers: [
                LinearShapePointer(value: _value, onChanged: (v) => setState(() => _value = v)),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// A minimal RenderObject that applies a Paint Offset
// This mimics the behavior of RenderSliverDashboard or other optimized layouts.
class OffsetPainterWidget extends SingleChildRenderObjectWidget {
  final Offset offset;
  const OffsetPainterWidget({super.key, required Widget child, required this.offset})
    : super(child: child);

  @override
  RenderObject createRenderObject(BuildContext context) => _RenderOffsetPainter(offset);
}

class _RenderOffsetPainter extends RenderProxyBox {
  final Offset offset;
  _RenderOffsetPainter(this.offset);

  @override
  void paint(PaintingContext context, Offset paintOffset) {
    // We paint the child shifted by 'offset'
    if (child != null) {
      context.paintChild(child!, paintOffset + offset);
    }
  }

  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    // We must report this shift to the transformation matrix
    transform.translateByDouble(offset.dx, offset.dy, 0, 1);
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    // We must shift the hit-test check accordingly
    if (child != null) {
      return child!.hitTest(result, position: position - offset);
    }
    return false;
  }
}

Screenshots or Video

Screenshots / Video demonstration

Image

Stack Traces

Stack Traces
no stack trace

On which target platforms have you observed this bug?

Android

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.38.4, on Microsoft Windows [version 10.0.26100.7171], locale fr-FR)
[√] Windows Version (11 Professionnel 64-bit, 24H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.18 (October 2025))
[√] Connected device (4 available)
[√] Network resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gaugesGauges componentopenOpen

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions