Skip to content

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

Closed
flutter/packages
#11334

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

Activity

  1. changed the title [-]TableSpan vertical borders are flipped when ScrollableDetails.horizontal is reversed[/-] [+][two_dimensional_scrollables] TableSpan vertical borders are flipped when ScrollableDetails.horizontal is reversed[/+] on Oct 16, 2025
  2. tirth-patel-nc commented on Oct 17, 2025

    @tirth-patel-nc
    Member

    @DMouayad Thanks for the report. Seeing the same behaviour with latest SDK versions.

    stable : 3.35.6
    master : 3.37.0-1.0.pre-566
    
  3. added
    packageflutter/packages repository. See also p: labels.
    p: two_dimensional_scrollablesIssues pertaining to the two_dimensional_scrollables package
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: scrollingViewports, list views, slivers, etc.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Oct 17, 2025
  4. added
    P2Important issues not at the top of the work list
    on Oct 21, 2025
  5. self-assigned this
    on Mar 23, 2026
  6. github-actions commented on Apr 8, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  7. locked as resolved and limited conversation to collaborators on Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions