Skip to content

Conversation

@hm21
Copy link
Contributor

@hm21 hm21 commented Oct 16, 2025

Description

This PR adds support for colSpan and rowSpan to the TableCell widget.
With this change, each TableCell can now span multiple columns and/or rows, similar to how HTML tables work.

This allows developers to create more flexible and complex table layouts without manually merging cells or resorting to nested tables.

Examples

Example 1 image
Table(
  border: TableBorder.all(color: Colors.grey),
  columnWidths: const <int, TableColumnWidth>{
    0: FixedColumnWidth(120),
    1: FlexColumnWidth(),
    2: FlexColumnWidth(),
  },
  children: <TableRow>[
    const TableRow(
      decoration: BoxDecoration(color: Color(0xFFE0E0E0)),
      children: <Widget>[
        TableCell(
          colSpan: 3,
          child: Padding(
            padding: EdgeInsets.all(8),
            child: Text(
              'Invoice Summary',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
          ),
        ),
        TableCell.none,
        TableCell.none,
      ],
    ),
    const TableRow(
      decoration: BoxDecoration(color: Color(0xFFF5F5F5)),
      children: <Widget>[
        Padding(padding: EdgeInsets.all(8), child: Text('Customer')),
        TableCell(
          colSpan: 2,
          child: Padding(padding: EdgeInsets.all(8), child: Text('Alex Frei')),
        ),
        TableCell.none,
      ],
    ),
    const TableRow(
      children: <Widget>[
        Padding(padding: EdgeInsets.all(8), child: Text('Date')),
        Padding(padding: EdgeInsets.all(8), child: Text('16 Oct 2025')),
        Padding(padding: EdgeInsets.all(8), child: Text('Invoice #1024')),
      ],
    ),
    const TableRow(
      decoration: BoxDecoration(color: Color(0xFFE0E0E0)),
      children: <Widget>[
        TableCell(
          colSpan: 3,
          child: Padding(
            padding: EdgeInsets.all(8),
            child: Text('Items', style: TextStyle(fontWeight: FontWeight.bold)),
          ),
        ),
        TableCell.none,
        TableCell.none,
      ],
    ),
    const TableRow(
      children: <Widget>[
        Padding(padding: EdgeInsets.all(8), child: Text('Development Work')),
        Padding(padding: EdgeInsets.all(8), child: Text('10 hours')),
        Padding(padding: EdgeInsets.all(8), child: Text(r'$500')),
      ],
    ),
    const TableRow(
      children: <Widget>[
        Padding(padding: EdgeInsets.all(8), child: Text('Design Work')),
        Padding(padding: EdgeInsets.all(8), child: Text('5 hours')),
        Padding(padding: EdgeInsets.all(8), child: Text(r'$250')),
      ],
    ),
    TableRow(
      children: <Widget>[
        TableCell(
          rowSpan: 2,
          verticalAlignment: TableCellVerticalAlignment.fill,
          child: Container(
            color: const Color(0xFFF5F5F5),
            padding: const EdgeInsets.all(8),
            child: const Text('Notes'),
          ),
        ),
        const TableCell(
          colSpan: 2,
          child: Padding(
            padding: EdgeInsets.all(8),
            child: Text('Thank you for your business!'),
          ),
        ),
        TableCell.none,
      ],
    ),
    const TableRow(
      children: <Widget>[
        TableCell.none,
        TableCell(
          colSpan: 2,
          child: Padding(padding: EdgeInsets.all(8), child: Text('Payment due in 14 days.')),
        ),
        TableCell.none,
      ],
    ),
  ],
)
Example 2 image
Table(
  border: TableBorder.all(),
  children: <TableRow>[
    TableRow(
      children: <Widget>[
        TableCell(
          rowSpan: 3,
          verticalAlignment: TableCellVerticalAlignment.fill,
          child: Container(color: Colors.blue),
        ),
        TableCell(colSpan: 2, child: Container(color: Colors.pink, height: 50)),
        TableCell.none,
      ],
    ),
    TableRow(
      children: <Widget>[
        TableCell.none,
        TableCell(child: Container(color: Colors.green, height: 50)),
        TableCell(
          verticalAlignment: TableCellVerticalAlignment.bottom,
          rowSpan: 2,
          child: Container(color: Colors.orange, height: 75),
        ),
      ],
    ),
    TableRow(
      children: <Widget>[
        TableCell.none,
        TableCell(child: Container(color: Colors.purple, height: 50)),
        TableCell.none,
      ],
    ),
  ],
);

Issues Fixed

Fixes #21594

Questions for Reviewers

  1. This PR introduces the static constant TableCell.none to represent a skipped cell (for when another cell uses rowSpan or colSpan). I’m not sure if none is the best name, alternatives like placeholder or skipped might also fit. I avoided empty since DataCell.empty already exists with different semantics.
  2. Should this feature also be considered for DataTable? I initially didn’t include it cuz the Material Design table doesn’t normally support rowSpan or colSpan.

Pre-launch Checklist

@github-actions github-actions bot added the framework flutter/packages/flutter repository. See also f: labels. label Oct 16, 2025
@hm21 hm21 changed the title Feat/table feat(table): add support for colSpan and rowSpan in TableCell Oct 16, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-implemented feature, adding colSpan and rowSpan support to the Table widget. The changes are comprehensive, covering rendering logic, widget API, border painting, and extensive testing. The implementation includes thoughtful performance optimizations, such as caching span information and using typed data lists. Additionally, the new assertions and detailed error messages will greatly improve the developer experience when working with complex table layouts. Overall, this is a high-quality contribution.

@justinmc justinmc requested a review from Piinks October 21, 2025 22:23
- Enhanced TableCell to include colSpan and rowSpan properties, allowing cells to span multiple columns and rows.
- Updated Table widget to validate colSpan and rowSpan values, ensuring they do not exceed table dimensions.
- Improved error handling for irregular row lengths and empty TableRow scenarios.
- Added tests for colSpan and rowSpan functionality, including edge cases and layout validation.
- Updated documentation to reflect new features and usage guidelines for TableCell.

refactor(RenderTable): improve comments for column and row span logic
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Hey @hm21 thanks for contributing! This is a big change, to help facilitate review, would you be willing to draft an outline of your changes and the motivations behind the decisions you made here? We usually do these using this template: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md

@Piinks
Copy link
Contributor

Piinks commented Oct 24, 2025

I am not sure what the implications might be on performance here, have you see the TableView widget that supports cell merging with lazy loading for children?

@hm21
Copy link
Contributor Author

hm21 commented Oct 25, 2025

Hey @Piinks, thanks for the fast response to that PR, really appreciate it cuz I thought it would take way longer for someone to take a look, as my other older PR #176393 is still waiting, but maybe that one’s not very interesting for you guys😅🙈

...would you be willing to draft an outline of your changes and the motivations behind the decisions you made here?

Sure, I can fill it out but not sure I’ll have time this weekend, but definitely next week I should find a bit of time.

I am not sure what the implications might be on performance here, have you see the TableView widget that supports cell merging with lazy loading for children?

I'm not sure, but I guess you're referring to the TableView from the two_dimensional_scrollables package, right? If that's the case, then yes, I know that package. However, I still think there are a few reasons why it makes sense to implement it the way it's done in the PR.

Reasons for that PR
  1. Dependencies: two_dimensional_scrollables is a package on pub.dev and not directly part of Flutter. I think new Flutter devs will expect the Table widget to support that out of the box, without needing to search for an extra package. As seen in Table > TableRow > TableCell colspan #21594, a few hundred people gave a thumbs up to that feature, so it seems like expected behavior should be internal. Also, when we compare it to other popular frameworks like MAUI, Ionic, or NativeScript, that kind of functionality is built-in without any extra dependencies.
  2. Complexity: I think for simple tables, most devs will expect a structure like Table > TableRow > TableCell, since that’s exactly how it works in HTML. In contrast, TableView works differently, the rowBuilder is separated from the cells, which could makes the widget tree harder to understand. Also, in TableViewCell, you have to set both columnMergeSpan and columnMergeStart. That makes sense when using the builder, but for a simple table, it feels a bit off or unnecessarily complicated. In most other frameworks, and even in standard HTML tables, you just set colSpan and rowSpan without needing to define a start position.
  3. Built-in RTL: I'm not 100% sure, but it seems that TableView.builder doesn't handle RTL correctly, or at least in my example, switching to RTL still keeps cell 0,0 on the top-left. Btw the existing Table widget already supports RTL, and the PR just extends that to also respect it for cell spanning.
  4. TableCellVerticalAlignment: Also, not 100% sure, but it seems like verticalAlignment isn’t supported in TableView. In this PR, verticalAlignment still works as expected and it also works correctly with colSpan and rowSpan for each individual cell.
  5. Performance: That's a good point you mentioned. For large tables with many offscreen cells that can be lazy-loaded, TableView.builder will definitely perform better. But for small tables where all cells are visible, I’m pretty sure it’s faster than TableView, though I doubt users would notice any difference. Comparing the original Table code to my changes, there’s a minimal impact on performance, but I don’t think that’s something end users would feel, especially since the Table widget isn’t designed for massive cell counts anyway. I tested it with 10k cells, and it was about 16ms slower, details are in the “Compare-Performance” section below. FYI: if performance is your top priority and readability is less important, I could also rewrite _computeSpanInformation to run directly within the same loop in performLayout. And also the loop inside table_border.dart could I merge.

Compare-Performance

To measure the average render duration, I repeatedly called setState() 100 times.

Test 1: Very Large (10,000 cells: 500 rows × 20 columns)

Version Avg (ms) Min (ms) Max (ms)
Original 351.09 275.40 469.32
PR 367.22 289.07 452.91

Test 2: Large (500 cells: 50 rows × 10 columns)

Version Avg (ms) Min (ms) Max (ms)
Original 42.18 20.29 96.12
PR 49.89 21.50 80.72

Btw, I hope you didn’t get me wrong TableView from two_dimensional_scrollables is definitely an awesome package, and I saw in the commits that you’re also working on it. Still, as I mentioned above, I believe there are valid reasons why it could make sense to support colSpan/rowSpan in the default Table as well. Anyway, if the points I mentioned aren’t relevant for you and the way to go for devs is to use packages for this kind of core functionality, just let me know, then we can close the PR 😉 But in that case, I’d also suggest closing #21594 and mentioning that the recommended solution is to use the two_dimensional_scrollables package.

Thanks again for the fast response to that PR and I'll fill out the document you mentioned as soon as possible.

@Piinks
Copy link
Contributor

Piinks commented Oct 27, 2025

as my other older PR #176393 is still waiting, but maybe that one’s not very interesting for you guys

Actually, we use labels to route PRs to the right sub-team for review. I noticed that PR did not have a team label on it, so I applied one and reached out to the team to confirm. I hope you will hear back soon on it, and thanks again for contributing!
We have a Discord channel for reaching out to folks directly if a PR goes unattended, see here for docs:

Thanks as well for your detailed response. If you are able to fill out the design dic template (the various sections are suggestive, not all required) it would be helpful for review. Otherwise I can start digging into this, it just might take a minute to get through it all. :)

@hm21
Copy link
Contributor Author

hm21 commented Oct 28, 2025

Hey @Piinks, thanks for sharing those two links and my bad for not reading carefully enough that I was supposed to mention the open PR in the Discord channel after two weeks🙈 Still, really appreciate your help with tagging the team on the other PR.

The link to the design-document is https://flutter.dev/go/table-cell-span.

Also, sorry again, I missed that the usual process is to first create an issue referencing the design doc before opening a PR. If you'd still prefer I create a new issue for the design doc but I just thought it might be a bit pointless now that the PR is already there😅

sfshaza2 pushed a commit to flutter/website that referenced this pull request Oct 28, 2025
## Description of what this PR is changing or adding, and why:
Adds a new shortlink redirect for the Flutter design document describing
TableCell span support.

## Issues fixed by this PR (if any):


## PRs or commits this PR depends on (if any):
[flutter/flutter#177102](flutter/flutter#177102)
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Still working through, but a couple questions to start. Thanks for writing a doc! Very helpful!

}

// Convert logical span information to visual span information for RTL
if (textDirection == TextDirection.rtl) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I am really glad you included this. Does this mean that where developers put the TableCell.none must correlate with the text direction?

As in... if TextDirection.rtl:
[TableCell(colSpan: 2), TableCell.none]

But if TextDirection.ltr developers should do:
[TableCell.none, TableCell(colSpan: 2)]

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, just a nit, but best to use a switch statement here on the enum for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, it keeps the same logic in both RTL and LTR. In short [TableCell(colSpan: 2), TableCell.none] works the same way in both directions but will be reversed. I know it’s a bit confusing at first when reading it like you showed, but it actually makes sense cuz in LTR, the first cell starts at the top left, and in RTL, at the top right, so the first cell in the array should be the same. In that example, TableCell.none has to stay at the end.

It maybe feels confusing cuz in your example it's written in a single line, and since we both read from left to right, it looks for us reversed in the table. But if we were using an RTL IDE, the code itself would also appear reversed, and then it would look correct again.

For example, the code above in "Example 1" will look like this in RTL:
image

Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Thanks for the updates and your patience here. :)

Couple tests I want to make sure we've covered..

From your doc (thanks again for providing!):

If a developer forgets to include the required TableCell.none entries, assertions during layout report the conflict and specify which row and column overlap occurred.

Is that covered here? I might have missed it.

Also, what happens if the configuration of spans causes overlapping merged cells? How is that handled?

Comment on lines 1494 to 1498
pendingRowSpanHeights[futureY] -= rowHeight;
// Ensure it doesn't go negative
if (pendingRowSpanHeights[futureY] < 0) {
pendingRowSpanHeights[futureY] = 0.0;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and where this logic is repeated, instead of the additional case to ensure it does not go negative, can you use clampDouble?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also struggled to follow the logic here a bit, can you explain a bit more way we set the height, then subtract? Up above on 1475, it only sets the trailing row of a span's height for row spans, with the potential in-betweens set to zero, and we are still in the row loop, so for every row, we iterate through every following row to subtract the height for what purpose?

(Forgive me, when I did merged cell support in TableView is melted my brain 😆 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, let me try to explain it:

The key idea is that we don’t know the final row heights upfront, but we do know the full height of a spanning cell when it’s first laid out.

So instead of trying to split that height immediately:

We store the full required height only in the last row of the span.
All intermediate rows stay at 0 for now.

Each time a row gets its final height, we subtract that height from all future pending spans.
This keeps track of how much height is still “unresolved” for those row-spanning cells.

When we finally reach the last row of the span, whatever height is left is applied to that row.
That guarantees the total height across all spanned rows matches the cell’s height exactly.

In short: We set once, subtract as we go, and apply the remainder at the end.
This avoids needing to precalculate or retroactively resize earlier rows.

FYI if I didn’t explain it in enough detail, I'm happy to write a more detailed explanation of each step and also point out where it’s important for TableCellVerticalAlignment to work correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. Acking this, appreciated the explanation!

Copy link
Contributor

Choose a reason for hiding this comment

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

the clamping non-negative is not addressed, should use math.max(0.0, ) instead

also would be good to put the explanation as code comment since this is non-trivial

}()),
assert(() {
if (children.isNotEmpty) {
final int cellCount = children.first.children.length;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since TableCell.non is used in place of hidden cells, shouldn't this assertion logic still work? A TableCell for every cell?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the old assert would still work with TableCell.none. I changed it intentionally to scan every row and report the exact row index and expected column count when there’s a mismatch.
I think for small tables the old message is fine, but for large tables (e.g. 20×40) this makes debugging much faster, cuz you can immediately see which row is wrong and what was expected, instead of just getting a generic error.

So I’d prefer to keep the new logic for better diagnostics, but I’m fine reverting if you feel the simpler assert is more appropriate here.

expect(find.text('Bottom Middle'), findsOneWidget);
});

testWidgets('Table with all cells using TableCell.none in spanning area', (
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if the user puts another widget in a hidden cell instead of TableCell.none?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, nothing will happen and it will just use it as a placeholder same as TableCell.none. I did consider adding an assert, but decided to leave it optional and allow the use of TableCell.none for a couple of reasons:

  1. Some users might prefer to use SizedBox.shrink() or another widget, so this gives them the flexibility to choose.
  2. Imagine a scenario where cells can be dynamically expanded or collapsed. In such cases, developers can simply update the colSpan or rowSpan value without having to switch between TableCell.none and an actual widget.

Anyway if you’d prefer to enforce the use of TableCell.none, I can add an assert, I'm fine with that too :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Connecting the other thread - we checked against TableView, and overlap like this is allowed. It is in the realm of possibility folks may want to be able to do this for their UI.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am slightly confused about the reasoning for allowing use of other widget in placed for the hidden cell? If I understand correctly

              TableRow(
                children: <Widget>[
                  TableCell(colSpan: 2, child: Text('Spanning Cell')),
                  Text('hidden'), // This is ignored?
                ],
              ),

This feels like code smell that they should use TableCell.none to be more descriptive.

@Piinks
Copy link
Contributor

Piinks commented Nov 17, 2025

Also, FYI it looks like there are some failing tests here.

@hm21
Copy link
Contributor Author

hm21 commented Nov 28, 2025

Hey @Piinks, so sorry for my late response, I was on vacation in Iceland, and afterwards got a bit caught up with some personal work projects🙈

Couple tests I want to make sure we've covered..
From your doc (thanks again for providing!):
"If a developer forgets to include the required TableCell.none entries, assertions during layout report the conflict and specify which row and column overlap occurred."
Is that covered here? I might have missed it.

Currently it detects two different behaviors:

  1. If the user forgets to add a TableCell.none or any other widget in a row, it will assert here, indicating exactly which row has the incorrect number of children.
  2. If the number of children is correct but the colSpan or rowSpan exceeds the available columns or rows, it will assert here which throws an error with the exact row and column number where the problem occurred.

Also, what happens if the configuration of spans causes overlapping merged cells? How is that handled?

At the moment, there's no assert that throws an error in that case, so if it happens, the merged cells will overlap the other cells in reverse order (the last widget ends up on top of the others). I guess it would make sense to implement an assert for that too, right? Or how do you handle that case in TableView?


Also, FYI it looks like there are some failing tests here.

Oh, I thought it was cuz of the infrastructure issue with the Linux tests that were failing around the time I created that PR. In another PR, I had the same problem and was told here that it was related to that. But my bad for not double-checking also here more carefully.

Some tests might fail again now, since I’m pretty sure I didn’t implement the golden tests correctly but I’ll fix them ASAP ;)

EDIT:
The Flutter Dashboard bot here mentions reviewing the golden tests. I opened the link and tried it (logged in with the same email as on GitHub), but got this error message: "Unexpected error triaging: 401 (Are you logged in with the right account?)"

So I’m guessing that part is only available to Flutter team members, or in this case, to “you,” right? Or is there something else I need to do first?

@flutter-dashboard
Copy link

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #177102 at sha 0dfad75

@flutter-dashboard flutter-dashboard bot added the will affect goldens Changes to golden files label Nov 28, 2025
baselines[y * columns + x] = childBaseline;
haveBaseline = true;
} else {
if (!isHiddenCell) {
Copy link
Contributor

@Piinks Piinks Dec 9, 2025

Choose a reason for hiding this comment

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

Should this assert !isHiddenCell instead? Looking at the continue based on isHiddenCell on l#1619, we should not end up here if isHiddenCell is true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I removed it directly in 0a3684d since, as you mentioned, we should never reach this code path.
I can still add an assert if you prefer, but the golden tests will fail in the case this path is reached anyway. I’m also fairly sure the widget tests would fail in that case as well. But if you prefer, I can also add an assert here.

Copy link
Contributor

Choose a reason for hiding this comment

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

we should never reach this code path.

How do we end up reaching it in widget and golden file tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is I think the last outstanding question for me. I am about to go OOO for the holidays, but I'll be lurking around Github tinkering with personal projects. I'd love to help get this in without having to wait for the new year. :)
That being said, we do need a second review. I have sent off or a second reviewer, with the hopes that code review is the perfect pastime for the quiet holiday period. 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How do we end up reaching it in widget and golden file tests?

Oops, my bad I just tested it and you're right. The tests won't fail cuz it seems the logic still keeps the "hidden" cell with a zero size, so it's more of a performance improvement and not something that would cause the logic to crash.

The point is, before, there was an if (!isHiddenCell) directly inside the switch for just some cases. But with the current logic, none of the cases should run if isHiddenCell is missing. So if we add an assert here, it needs to go before the switch, and it would look like this:

final bool isHiddenCell = currentRowHiddenCells.contains(x);
if (isHiddenCell) {
  continue;
}
assert(!isHiddenCell, '...');

I mean, we can do that, but it feels wrong because we'd be directly asserting the if right after it runs. Alternatively, we could add a doc comment like "Important for performance improvement", but on the other hand, I feel like if someone sees the code skipping "hidden" cells, it should already be clear that it's for performance reasons. But I’m open to it, I can add the assert, the doc comment, or both, whatever you prefer :)


This is I think the last outstanding question for me. I am about to go OOO...

Sounds good to me, and thanks a lot for pushing this forward 🙌
Really appreciate you lining up a second review as well.

Enjoy the holidays and OOO time! I’ll keep an eye on GitHub too, so if anything comes up during review I’m happy to jump in and help😊

case TableCellVerticalAlignment.middle:
case TableCellVerticalAlignment.bottom:
case TableCellVerticalAlignment.intrinsicHeight:
if (!isHiddenCell) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here as well?

@Piinks
Copy link
Contributor

Piinks commented Dec 9, 2025

Thanks for the updates! I hope you had a nice time in Iceland. Let me check how TableView handles overlapping merged cells. I do not recall, and there are probably some fundamental differences here since it lays out lazily.

@Piinks
Copy link
Contributor

Piinks commented Dec 9, 2025

Thanks for the updates! I hope you had a nice time in Iceland. Let me check how TableView handles overlapping merged cells. I do not recall, and there are probably some fundamental differences here since it lays out lazily.

Looks like the TableView will not complain about overlapping merged cells. So maybe that's alright here too. :)

@Piinks
Copy link
Contributor

Piinks commented Dec 9, 2025

The Flutter Dashboard bot #177102 (comment) mentions reviewing the golden tests. I opened the link and tried it (logged in with the same email as on GitHub), but got this error message: "Unexpected error triaging: 401 (Are you logged in with the right account?)"

Yes, we have an allow list to manage write access to images. I can approve them, looking now.

@Piinks
Copy link
Contributor

Piinks commented Dec 9, 2025

Delightful. 🙂
image

@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #177102 at sha ec0c44f

@hm21
Copy link
Contributor Author

hm21 commented Dec 10, 2025

Hey @Piinks, thanks again for your review :)

Looks like the TableView will not complain about overlapping merged cells. So maybe that's alright here too. :)

Okay, that's good to know but if you still prefer me to add it here, just let me know :)

@chunhtai chunhtai self-requested a review December 18, 2025 00:07
@victorsanni victorsanni self-requested a review December 19, 2025 02:56
for (final RenderBox? child in _children) {
if (child != null && child.hasSize) {
final cellParentData = child.parentData! as TableCellParentData;
if (cellParentData.rowSpan != 0 && cellParentData.colSpan != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

convert this to be a private getter in TableCellParentData

final int rowSpan = parentData.rowSpan;

// Only process if there are actual spans to avoid unnecessary work
if (colSpan <= 1 && rowSpan <= 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

same this can use a private getter for readability

late double _tableWidth;

// Cached layout data used during painting to avoid recomputation.
List<Set<int>> _cachedSpannedColumnsPerRow = const <Set<int>>[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
List<Set<int>> _cachedSpannedColumnsPerRow = const <Set<int>>[];
List<Set<int>> _cachedSpannedColumnsForRows = const <Set<int>>[];


// Cached layout data used during painting to avoid recomputation.
List<Set<int>> _cachedSpannedColumnsPerRow = const <Set<int>>[];
List<Set<int>> _cachedSpannedRowsPerColumn = const <Set<int>>[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
List<Set<int>> _cachedSpannedRowsPerColumn = const <Set<int>>[];
List<Set<int>> _cachedSpannedRowsForColumns = const <Set<int>>[];

expect(find.text('Bottom Middle'), findsOneWidget);
});

testWidgets('Table with all cells using TableCell.none in spanning area', (
Copy link
Contributor

Choose a reason for hiding this comment

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

I am slightly confused about the reasoning for allowing use of other widget in placed for the hidden cell? If I understand correctly

              TableRow(
                children: <Widget>[
                  TableCell(colSpan: 2, child: Text('Spanning Cell')),
                  Text('hidden'), // This is ignored?
                ],
              ),

This feels like code smell that they should use TableCell.none to be more descriptive.

case TableCellVerticalAlignment.middle:
case TableCellVerticalAlignment.bottom:
case TableCellVerticalAlignment.intrinsicHeight:
final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x]));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also consider colspan? for example the for colspan == 2 the width will be the sum of widths[x] + widths[x + 1]

Comment on lines 1494 to 1498
pendingRowSpanHeights[futureY] -= rowHeight;
// Ensure it doesn't go negative
if (pendingRowSpanHeights[futureY] < 0) {
pendingRowSpanHeights[futureY] = 0.0;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

the clamping non-negative is not addressed, should use math.max(0.0, ) instead

also would be good to put the explanation as code comment since this is non-trivial

final Float64List rowHeights = Float64List(rows);
final Float64List beforeBaselineDistances = Float64List(rows);
// Use flat arrays instead of nested lists for better cache locality.
final Float64List spanWidths = Float64List(rows * columns); // [row][col] = row * columns + col
Copy link
Contributor

Choose a reason for hiding this comment

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

I can understand the code from comment in line 1532, but I am not sure what the code comment in this line mean.

final Float64List spanWidths = Float64List(rows * columns); // [row][col] = row * columns + col
final Float64List baselines = Float64List(rows * columns); // [row][col] = row * columns + col

final bool isTextDirectionRtl = textDirection == TextDirection.rtl;
Copy link
Contributor

Choose a reason for hiding this comment

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

use line 1539 to set this value

final bool isTextDirectionRtl;

switch (...) {
   case ...
     isTextDirectionRtl = true;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table > TableRow > TableCell colspan

3 participants