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.',
);
}
}
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
horizontalDetailsin theTableView.builderto:Expected results
The TableSpan (row) border should be at the bottom since it's defined as trailing border in
Actual results
The border placement is switched. From leading to trailing and vice-versa.
Code sample
Code sample
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