Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions lib/src/components/tab_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,30 @@ class TabBar extends StatelessComponent {
),
);

return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: tabComponents,
return Stack(
children: [
// A full-width base rule painted behind the tab segments, in the
// border's color like the segments themselves. It fills the cells
// fractional pane widths leave uncovered (the segments size with
// maxWidth.toInt(), truncating half cells), and its ends reach one
// cell outside the bar to merge the underline into a surrounding
// box border (┝/┥). Ends with no border to join paint nothing.
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Divider(
style: DividerStyle.bold,
indent: -1,
endIndent: -1,
color: ServerpodTheme.of(context).subtleDivider,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: tabComponents,
),
],
);
}
}
Expand Down Expand Up @@ -136,9 +157,10 @@ class _Tab extends StatelessComponent {
),
Text(
''.padLeft(underlineWidth, '━'),
// The selection highlight keeps its accent color; every other
// underline segment matches the surrounding box border.
style: TextStyle(
color: selected ? theme.activationKey : null,
fontWeight: selected ? FontWeight.normal : FontWeight.dim,
color: selected ? theme.activationKey : theme.subtleDivider,
),
),
],
Expand Down Expand Up @@ -228,7 +250,12 @@ class _TabSpacing extends StatelessComponent {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(''.padLeft(width)),
Text(underline, style: const TextStyle(fontWeight: FontWeight.dim)),
// The underline shares the surrounding box border's color and
// weight so bar and border read as one frame.
Text(
underline,
style: TextStyle(color: ServerpodTheme.of(context).subtleDivider),
),
],
);
}
Expand Down
113 changes: 113 additions & 0 deletions test/src/components/tab_bar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,119 @@ void main() {
});
});

test(
'Given a tab bar laid out at a fractional width '
'when rendered inside a bordered box '
'then the underline has no gap before the border junction',
() async {
tester = await NoctermTester.create(size: const Size(40, 6));
await tester.pumpComponent(
Container(
decoration: BoxDecoration(
border: BoxBorder.all(style: BoxBorderStyle.rounded),
),
child: Row(
children: [
// Forces the bar onto fractional cells: its segments size
// with maxWidth.toInt(), which would leave the final half
// cell unpainted without the base rule behind them.
const SizedBox(width: 1.5),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
labels: const ['Server logs'],
selectedTab: 0,
onTabChanged: (_) {},
),
Expanded(child: const SizedBox.shrink()),
],
),
),
],
),
),
);

final ts = tester.terminalState;
// The cell beside the border keeps the underline; a truncation gap
// here would break the line just before its junction.
expect(ts.getTextAt(38, 2, length: 1), '━');
expect(ts.getTextAt(39, 2, length: 1), '┥');
},
);

test(
'Given a tab bar inside a bordered box '
'when rendered then the underline merges into both border sides',
() async {
tester = await NoctermTester.create(size: const Size(40, 6));
await tester.pumpComponent(
Container(
decoration: BoxDecoration(
border: BoxBorder.all(style: BoxBorderStyle.rounded),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
labels: const ['Server logs'],
selectedTab: 0,
onTabChanged: (_) {},
),
Expanded(child: const SizedBox.shrink()),
],
),
),
);

// The tab bar sits inside the border, so its label is on row 1 and
// its underline on row 2. The underline's ends reach the border
// cells and form heavy tees instead of leaving gaps.
final ts = tester.terminalState;
expect(ts.getTextAt(0, 2, length: 1), '┝');
expect(ts.getTextAt(39, 2, length: 1), '┥');
// The whole underline - junctions and segments alike - carries the
// surrounding box border's color, so bar and border read as one
// frame.
final borderColor = ServerpodThemeData.dark.subtleDivider;
expect(ts.getCellAt(0, 2)?.style.color, borderColor);
expect(ts.getCellAt(20, 2)?.style.color, borderColor);
},
);

test(
'Given a tab bar with no surrounding border '
'when rendered '
'then the underline ends paint no stray junction arms',
() async {
tester = await NoctermTester.create(size: const Size(40, 4));
await tester.pumpComponent(
Container(
padding: EdgeInsets.all(1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
labels: const ['Server logs'],
selectedTab: 0,
onTabChanged: (_) {},
),
Expanded(child: const SizedBox.shrink()),
],
),
),
);

// The base rule reaches one cell outside the tab bar on each side,
// but with nothing there to join it leaves those cells untouched.
final ts = tester.terminalState;
expect(ts.getTextAt(0, 2, length: 1), ' ');
expect(ts.getTextAt(39, 2, length: 1), ' ');
},
);

test(
'Given fewer states than labels '
'when rendered '
Expand Down
Loading