Skip to content

[two_dimensional_scrollables] TableSpan vertical borders are flipped when ScrollableDetails.horizontal is reversed #177117

Description

@DMouayad

What package does this bug report belong to?

two_dimensional_scrollables

What target platforms are you seeing this bug on?

Windows

Have you already upgraded your packages?

Yes

Dependency versions

pubspec.lock

Steps to reproduce

Using the package example app

  1. Set horizontalDetails in the TableView.builder to:
 horizontalDetails: ScrollableDetails.horizontal(
  reverse: true,
),

Expected results

The TableSpan (row) border should be at the bottom since it's defined as trailing border in

TableSpan _buildRowSpan(int index) {
    final TableSpanDecoration decoration = TableSpanDecoration(
      color: index.isEven ? Colors.purple[100] : null,
      border: const TableSpanBorder(trailing: BorderSide(width: 3)),
    );
// rest of code
}

Actual results

The border placement is switched. From leading to trailing and vice-versa.

Code sample

Code sample
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

void main() {
  runApp(MaterialApp(home: const TableExample()));
}
class TableExample extends StatefulWidget {
  /// Creates a screen that demonstrates the TableView widget.
  const TableExample({super.key});

  @override
  State<TableExample> createState() => _TableExampleState();
}

class _TableExampleState extends State<TableExample> {
  late final ScrollController _verticalController = ScrollController();
  final int _rowCount = 20;

  @override
  void dispose() {
    _verticalController.dispose();
    super.dispose();
  }

  bool reversed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 10),
        child: TableView.builder(
          verticalDetails: ScrollableDetails.vertical(
            controller: _verticalController,
          ),
          horizontalDetails: ScrollableDetails.horizontal(
            reverse: reversed,
          ),
          cellBuilder: _buildCell,
          columnCount: 20,
          columnBuilder: _buildColumnSpan,
          rowCount: _rowCount,
          rowBuilder: _buildRowSpan,
        ),
      ),
      persistentFooterButtons: <Widget>[
        OverflowBar(
          alignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                SegmentedButton<bool>(
                  segments: const <ButtonSegment<bool>>[
                    ButtonSegment<bool>(
                      value: true,
                      label: Text('Reversed'),
                      icon: Icon(Icons.keyboard_double_arrow_right),
                    ),
                    ButtonSegment<bool>(
                      value: false,
                      label: Text('Normal'),
                      icon: Icon(Icons.keyboard_double_arrow_left),
                    ),
                  ],
                  selected: <bool>{reversed},
                  onSelectionChanged: (Set<bool> newValue) {
                    print(newValue.toList());
                    setState(() {
                      // By default there is only a single segment that can be
                      // selected at one time, so its value is always the first
                      // item in the selected set.
                      reversed = newValue.first;
                    });
                  },
                ),
              ],
            ),
          ],
        ),
      ],
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    Widget result = Center(
      child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'),
    );

    return TableViewCell(child: result);
  }

  TableSpan _buildColumnSpan(int index) {
    const TableSpanDecoration decoration = TableSpanDecoration(
      border: TableSpanBorder(trailing: BorderSide()),
    );

    switch (index % 5) {
      case 0:
        return TableSpan(
          foregroundDecoration: decoration,
          extent: const FixedTableSpanExtent(100),
          onEnter: (_) => print('Entered column $index'),
          recognizerFactories: <Type, GestureRecognizerFactory>{
            TapGestureRecognizer:
                GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
              () => TapGestureRecognizer(),
              (TapGestureRecognizer t) =>
                  t.onTap = () => print('Tap column $index'),
            ),
          },
        );
      case 1:
        return TableSpan(
          foregroundDecoration: decoration,
          extent: const FractionalTableSpanExtent(0.5),
          onEnter: (_) => print('Entered column $index'),
          cursor: SystemMouseCursors.contextMenu,
        );
      case 2:
        return TableSpan(
          foregroundDecoration: decoration,
          extent: const FixedTableSpanExtent(120),
          onEnter: (_) => print('Entered column $index'),
        );
      case 3:
        return TableSpan(
          foregroundDecoration: decoration,
          extent: const FixedTableSpanExtent(145),
          onEnter: (_) => print('Entered column $index'),
        );
      case 4:
        return TableSpan(
          foregroundDecoration: decoration,
          extent: const FixedTableSpanExtent(200),
          onEnter: (_) => print('Entered column $index'),
        );
    }
    throw AssertionError(
      'This should be unreachable, as every index is accounted for in the '
      'switch clauses.',
    );
  }

  TableSpan _buildRowSpan(int index) {
    final TableSpanDecoration decoration = TableSpanDecoration(
      color: index.isEven ? Colors.purple[100] : null,
      border: const TableSpanBorder(trailing: BorderSide(width: 3)),
    );

    switch (index % 3) {
      case 0:
        return TableSpan(
          backgroundDecoration: decoration,
          extent: const FixedTableSpanExtent(50),
          recognizerFactories: <Type, GestureRecognizerFactory>{
            TapGestureRecognizer:
                GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
              () => TapGestureRecognizer(),
              (TapGestureRecognizer t) =>
                  t.onTap = () => print('Tap row $index'),
            ),
          },
        );
      case 1:
        return TableSpan(
          backgroundDecoration: decoration,
          extent: const FixedTableSpanExtent(65),
          cursor: SystemMouseCursors.click,
        );
      case 2:
        return TableSpan(
          backgroundDecoration: decoration,
          extent: const FractionalTableSpanExtent(0.15),
        );
    }
    throw AssertionError(
      'This should be unreachable, as every index is accounted for in the '
      'switch clauses.',
    );
  }
}

Screenshots or Videos

Screenshots / Video demonstration
bandicam.2025-10-16.21-21-28-612.mp4

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
 Flutter (Channel stable, 3.35.3, on Microsoft Windows [Version 10.0.22631.2070], locale en-US) [522ms]
    • Flutter version 3.35.3 on channel stable at C:\Users\Mouayad\dev\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision a402d9a437 (6 weeks ago), 2025-09-03 14:54:31 -0700
    • Engine revision ddf47dd3ff
    • Dart version 3.9.2
    • DevTools version 2.48.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations,
      enable-lldb-debugging

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listf: scrollingViewports, list views, slivers, etc.found in release: 3.35Found to occur in 3.35found in release: 3.37Found to occur in 3.37frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: two_dimensional_scrollablesIssues pertaining to the two_dimensional_scrollables packagepackageflutter/packages repository. See also p: labels.team-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework teamwaiting for PR to land (fixed)A fix is in flight

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions