sourcemain/tab_container

Provide a border to the active tab container

Opened this issue · 3 comments

Currently there is no way to provide a custom border to the selected tab and body combined. It would be great if there was a way to provide simple border to the active tab container that isn't heavy on the engine.

kish10 commented

Hi @soumya-ventura,

I created a temporary work around for this issue on my project, by creating a custom widget for the tabs & children.

Here is an example rough code for reference, hope it saves someone else time:
(The example is only for two tabs, but should be able to extend to multiple tabs)

Example code
import 'package:tab_container/tab_container.dart';

enum _TabElementType {
  firstTab,
  firstChild,
  secondTab,
  secondChild
}

class _TabElement extends StatelessWidget {

  final Widget child;
  final _TabElementType tabElementType;
  final bool onFirstTab;

  const _TabElement({
    super.key,
    required this.child,
    required this.tabElementType,
    required this.onFirstTab
  });

  (Border?, BorderRadius) getBorderInfo(context) {
    const radiusDegree = 20.0;

    // default border for tab child
    Border? border = Border.all(
        width: 8.0,
        color: Color(0xffa275e3)
    );

    // default borderRadius for tabs
    BorderRadius borderRadius = const BorderRadius.only(
      topLeft: Radius.circular(radiusDegree),
      topRight: Radius.circular(radiusDegree),
    );

    switch(tabElementType) {
      case(_TabElementType.firstTab):
        if (!onFirstTab) {
          border = null; 
        }

        break;

      case(_TabElementType.secondTab):
        if (onFirstTab) {
          border = null;
        }

        break;

      case(_TabElementType.firstChild):
        borderRadius = const BorderRadius.only(
          bottomLeft: Radius.circular(radiusDegree),
          bottomRight: Radius.circular(radiusDegree),
          topRight: Radius.circular(radiusDegree),
        );

        break;

      case(_TabElementType.secondChild):
        borderRadius = const BorderRadius.only(
          bottomLeft: Radius.circular(radiusDegree),
          bottomRight: Radius.circular(radiusDegree),
          topLeft: Radius.circular(radiusDegree),
        );

        break;
    }

    return (border, borderRadius);
  }

  @override
  Widget build(BuildContext context) {
    var (border, borderRadius) = getBorderInfo(context);
    return Container(
      decoration: BoxDecoration(
          border: border,
          borderRadius: borderRadius
      ),
      child: SizedBox.expand(
          child: child
      ),
    );
  }


class _MainContainer extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MainContainerState();
  }
}

class _MainContainerState extends State<_MainContainer> {
  late final TabContainerController _tabController;
  bool onFirstTab = true;
  int currentTabIndex = 0;

  @override
  void initState() {
    _tabController = TabContainerController(length: 2);
    _tabController.addListener(() {
      setState(() {
        onFirstTab = _tabController.index == 0;
        currentTabIndex = _tabController.index;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    _tabController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TabContainer(
        controller: _tabController,
        radius: 20,
        tabs: [
          _TabElement(
              tabElementType: _TabElementType.firstTab,
              onFirstTab: onFirstTab,
              child: Text('Select Item')
          ),
          _TabElement(
              tabElementType: _TabElementType.secondTab,
              onFirstTab: onFirstTab,
              child: Text('History')
          ),
        ],
        isStringTabs: false,
        children: [
          _TabElement(
              tabElementType: _TabElementType.firstChild,
              onFirstTab: onFirstTab,
              child: Text('Tab 1 - $onFirstTab - $currentTabIndex')
          ),
          _TabElement(
              tabElementType: _TabElementType.secondChild,
              onFirstTab: onFirstTab,
              child: Text('Tab 2 - $onFirstTab - $currentTabIndex')
          ),
        ]
    );
  }

}
Liaxum commented

Hi,

I have the same problem in my project. I'm going to look at adding the functionality without going through a custom widget and for all tabs not just 2. If you want to follow the progress of the fix I've created a fork of the project here

import 'dart:math';
import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

/// Specifies which side the tabs will be on.
enum BorderSide { right, bottom, left }

/// Specifies which side the tabs will be on.
enum TabEdge { top }

class TabContainerController extends ValueNotifier {
TabContainerController(super.value);
}

extension on double {
bool isBetween(double num1, double num2) {
return num1 <= this && this <= num2;
}
}

class _TabMetrics {
_TabMetrics({
required this.count,
required this.range,
required this.minLength,
required this.maxLength,
});

final int count;
final double range;
final double minLength;
final double maxLength;

double get length => (range / count).clamp(minLength, maxLength);

double get totalLength => count * length;
}

class _TabViewport {
_TabViewport({
required this.parentSize,
required this.tabEdge,
required this.tabExtent,
required this.tabsStart,
required this.tabsEnd,
});

final Size parentSize;
final TabEdge tabEdge;
final double tabExtent;
final double tabsStart;
final double tabsEnd;

double get side => (tabEdge == TabEdge.top) ? parentSize.width : parentSize.height;

double get start => side * tabsStart;

double get end => side * tabsEnd;

double get range => end - start;

Size get size => (tabEdge == TabEdge.top) ? Size(range, tabExtent) : Size(tabExtent, range);

bool contains(double x, double y, double totalLength) {
final double minEnd = min(end, start + totalLength);
switch (tabEdge) {
case TabEdge.top:
if (y <= tabExtent && x.isBetween(start, minEnd)) {
return true;
}
break;
}
return false;
}
}

/// Displays [children] in accordance with the tab selection.
///
/// Handles styling and animation and exposes control over tab selection through [TabController].
class TabContainer extends StatefulWidget {
const TabContainer({
required this.topBorderColor,
required this.topBorderWidth,
required this.rightBorderColor,
required this.rightBorderWidth,
required this.bottomBorderColor,
required this.bottomBorderWidth,
required this.leftBorderColor,
required this.leftBorderWidth,
required this.tabs,
super.key,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.easeInOut,
this.controller,
this.children,
this.child,
this.childPadding = EdgeInsets.zero,
this.borderRadius = const BorderRadius.all(Radius.circular(12.0)),
this.tabBorderRadius = const BorderRadius.all(Radius.circular(12.0)),
this.tabExtent = 50.0,
this.tabEdge = TabEdge.top,
this.tabsStart = 0.0,
this.tabsEnd = 1.0,
this.tabMinLength = 0.0,
this.tabMaxLength = double.infinity,
this.color,
this.transitionBuilder,
this.semanticsConfiguration,
this.overrideTextProperties = false,
this.selectedTextStyle,
this.unselectedTextStyle,
this.textDirection,
this.enabled = true,
this.enableFeedback = true,
this.childDuration,
this.childCurve,
});

final TabController? controller;

final List? children;

final Widget? child;

final List tabs;

final BorderRadius borderRadius;

final BorderRadius tabBorderRadius;

final EdgeInsets childPadding;

final double tabExtent;

final TabEdge tabEdge;

final double tabsStart;

final double tabsEnd;

final double tabMinLength;

final double tabMaxLength;

final Color? color;

final Duration duration;

final Curve curve;

final Duration? childDuration;

final Curve? childCurve;

final Widget Function(Widget, Animation)? transitionBuilder;

final SemanticsConfiguration? semanticsConfiguration;

final bool overrideTextProperties;

final TextStyle? selectedTextStyle;

final TextStyle? unselectedTextStyle;

final TextDirection? textDirection;

final bool enabled;

final bool enableFeedback;

final Color topBorderColor;

final double topBorderWidth;

final Color rightBorderColor;

final double rightBorderWidth;

final Color bottomBorderColor;

final double bottomBorderWidth;

final Color leftBorderColor;

final double leftBorderWidth;

@OverRide
State createState() => _TabContainerState();
}

class _TabContainerState extends State with SingleTickerProviderStateMixin {
late TabController _controller;

TabController? _defaultController;

late ScrollController _scrollController;

late Widget _child;

List _tabs = [];

late TextStyle _selectedTextStyle;

late TextStyle _unselectedTextStyle;

late TextDirection _textDirection;

double _progress = 0;

Color? _color;

@OverRide
void initState() {
super.initState();
if (widget.controller == null) {
_defaultController = TabController(
vsync: this,
animationDuration: widget.duration,
length: widget.tabs.length,
);
_controller = _defaultController!;
} else {
_controller = widget.controller!;
}

_controller.addListener(_tabListener);
_controller.animation!.addListener(_animationListener);

_progress = _controller.animation!.value;

_scrollController = ScrollController();

_buildChild();

}

@OverRide
void didChangeDependencies() {
_selectedTextStyle =
widget.selectedTextStyle ?? Theme.of(context).textTheme.bodyMedium ?? const TextStyle();
_unselectedTextStyle = widget.unselectedTextStyle ??
Theme.of(context).textTheme.bodyMedium ??
const TextStyle();
_textDirection = widget.textDirection ?? Directionality.of(context);
super.didChangeDependencies();
_remountController();
_buildChild();
_buildTabs();
}

@OverRide
void didUpdateWidget(covariant TabContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_remountController();
}
_buildChild();
_buildTabs();
}

@OverRide
void dispose() {
_scrollController.dispose();

_controller.animation?.removeListener(_animationListener);
_controller.removeListener(_tabListener);
_defaultController?.dispose();

super.dispose();

}

double _animationFraction(double current, int previous, int next) {
if (next - previous == 0) {
return 1;
}
return (current - previous) / (next - previous);
}

void _animationListener() {
_progress = _controller.animation!.value;
_updateTabs(_controller.previousIndex, _controller.index);
}

void _tabListener() {
_buildChild();
}

void _remountController() {
if (widget.controller != null) {
if (widget.controller == _controller) {
return;
}
} else if (_defaultController != null && _defaultController == _controller) {
return;
}

_controller.animation?.removeListener(_animationListener);
_controller.removeListener(_tabListener);
_defaultController?.dispose();
_defaultController = null;

if (widget.controller != null) {
  _controller = widget.controller!;
} else {
  _defaultController = TabController(
    vsync: this,
    animationDuration: widget.duration,
    length: widget.tabs.length,
  );
  _controller = _defaultController!;
}

_controller.addListener(_tabListener);
_controller.animation!.addListener(_animationListener);

_progress = _controller.animation!.value;

}

TextStyle _calculateTextStyle(int index) {
final TextStyleTween styleTween = TextStyleTween(
begin: _unselectedTextStyle,
end: _selectedTextStyle,
);

final double animationFraction = _animationFraction(
  _progress,
  _controller.previousIndex,
  _controller.index,
);

if (index == _controller.index) {
  return styleTween
      .lerp(animationFraction)
      .copyWith(fontSize: _unselectedTextStyle.fontSize);
} else if (index == _controller.previousIndex) {
  return styleTween
      .lerp(1 - animationFraction)
      .copyWith(fontSize: _unselectedTextStyle.fontSize);
} else {
  return _unselectedTextStyle;
}

}

double _calculateTextScale(int index) {
final double animationFraction = _animationFraction(
_progress,
_controller.previousIndex,
_controller.index,
);
final double textRatio = _selectedTextStyle.fontSize! / _unselectedTextStyle.fontSize!;

if (index == _controller.index) {
  return lerpDouble(1, textRatio, animationFraction)!;
} else if (index == _controller.previousIndex) {
  return lerpDouble(textRatio, 1, animationFraction)!;
} else {
  return 1.0;
}

}

Widget _getTab(int index) {
final Widget tab = widget.tabs[index];

if (widget.overrideTextProperties) {
  return tab;
}

return Transform(
  alignment: Alignment.center,
  transform: Matrix4.identity()..scale(_calculateTextScale(index)),
  child: Container(
    child: DefaultTextStyle.merge(
      child: tab,
      textAlign: TextAlign.center,
      overflow: TextOverflow.fade,
      style: _calculateTextStyle(index),
    ),
  ),
);

}

void _updateTabs(int previous, int next) {
setState(() {
_tabs[previous] = _getTab(previous);
_tabs[next] = _getTab(next);
});
}

void _buildTabs() {
List tabs = [];

for (int index = 0; index < widget.tabs.length; index++) {
  tabs.add(_getTab(index));
}

setState(() {
  _tabs = tabs;
});

}

void _buildChild() {
Widget child = widget.child ??
Padding(
padding: widget.childPadding,
child: AnimatedSwitcher(
duration: widget.childDuration ?? widget.duration,
switchInCurve: widget.childCurve ?? widget.curve,
transitionBuilder:
widget.transitionBuilder ?? AnimatedSwitcher.defaultTransitionBuilder,
child: IndexedStack(
key: ValueKey(_controller.index),
index: _controller.index,
children: widget.children!,
),
),
);

setState(() {
  _child = child;
});

}

@OverRide
Widget build(BuildContext context) {
return TabFrame(
controller: _controller,
scrollController: _scrollController,
progress: _progress,
curve: widget.curve,
duration: widget.duration,
tabs: _tabs,
borderRadius: widget.borderRadius,
tabBorderRadius: widget.tabBorderRadius,
tabExtent: widget.tabExtent,
tabEdge: widget.tabEdge,
tabAxis: Axis.horizontal,
tabsStart: widget.tabsStart,
tabsEnd: widget.tabsEnd,
tabMinLength: widget.tabMinLength,
tabMaxLength: widget.tabMaxLength,
color: _color ?? widget.color ?? Colors.transparent,
semanticsConfiguration: widget.semanticsConfiguration,
enabled: widget.enabled,
enableFeedback: widget.enableFeedback,
textDirection: _textDirection,
topBorderColor: widget.topBorderColor,
topBorderWidth: widget.topBorderWidth,
rightBorderColor: widget.rightBorderColor,
rightBorderWidth: widget.rightBorderWidth,
bottomBorderColor: widget.bottomBorderColor,
bottomBorderWidth: widget.bottomBorderWidth,
leftBorderColor: widget.leftBorderColor,
leftBorderWidth: widget.leftBorderWidth,
child: _child,
);
}
}

class TabFrame extends MultiChildRenderObjectWidget {
final TabController controller;
final ScrollController scrollController;
final double progress;
final Curve curve;
final Duration duration;
final Widget child;
final List tabs;
final BorderRadius borderRadius;
final BorderRadius tabBorderRadius;
final double tabExtent;
final TabEdge tabEdge;
final Axis tabAxis;
final double tabsStart;
final double tabsEnd;
final double tabMinLength;
final double tabMaxLength;
final Color color;
final SemanticsConfiguration? semanticsConfiguration;
final bool enabled;
final bool enableFeedback;
final TextDirection textDirection;
final Color topBorderColor;
final double topBorderWidth;
final Color rightBorderColor;
final double rightBorderWidth;
final Color bottomBorderColor;
final double bottomBorderWidth;
final Color leftBorderColor;
final double leftBorderWidth;

TabFrame({
required this.controller,
required this.scrollController,
required this.progress,
required this.curve,
required this.duration,
required this.child,
required this.tabs,
required this.borderRadius,
required this.tabBorderRadius,
required this.tabExtent,
required this.tabEdge,
required this.tabAxis,
required this.tabsStart,
required this.tabsEnd,
required this.tabMinLength,
required this.tabMaxLength,
required this.color,
required this.semanticsConfiguration,
required this.enabled,
required this.enableFeedback,
required this.textDirection,
required this.topBorderColor,
required this.topBorderWidth,
required this.rightBorderColor,
required this.rightBorderWidth,
required this.bottomBorderColor,
required this.bottomBorderWidth,
required this.leftBorderColor,
required this.leftBorderWidth,
super.key,
}) : super(children: [child, ...tabs]);

@OverRide
RenderTabFrame createRenderObject(BuildContext context) {
return RenderTabFrame(
context: context,
controller: controller,
scrollController: scrollController,
progress: progress,
curve: curve,
duration: duration,
tabs: tabs,
borderRadius: borderRadius,
tabBorderRadius: tabBorderRadius,
tabExtent: tabExtent,
tabEdge: tabEdge,
tabAxis: tabAxis,
tabsStart: tabsStart,
tabsEnd: tabsEnd,
tabMinLength: tabMinLength,
tabMaxLength: tabMaxLength,
color: color,
semanticsConfiguration: semanticsConfiguration,
enabled: enabled,
enableFeedback: enableFeedback,
textDirection: textDirection,
topBorderColor: topBorderColor,
topBorderWidth: topBorderWidth,
rightBorderColor: rightBorderColor,
rightBorderWidth: rightBorderWidth,
bottomBorderColor: bottomBorderColor,
bottomBorderWidth: bottomBorderWidth,
leftBorderColor: leftBorderColor,
leftBorderWidth: leftBorderWidth,
);
}

@OverRide
void updateRenderObject(BuildContext context, RenderTabFrame renderObject) {
renderObject
..context = context
..controller = controller
..scrollController = scrollController
..progress = progress
..curve = curve
..duration = duration
..tabs = tabs
..borderRadius = borderRadius
..tabBorderRadius = tabBorderRadius
..tabExtent = tabExtent
..tabEdge = tabEdge
..tabAxis = tabAxis
..tabsStart = tabsStart
..tabsEnd = tabsEnd
..tabMinLength = tabMinLength
..tabMaxLength = tabMaxLength
..color = color
..semanticsConfiguration = semanticsConfiguration
..enabled = enabled
..enableFeedback = enableFeedback
..textDirection = textDirection
..topBorderColor = topBorderColor
..topBorderWidth = topBorderWidth
..rightBorderColor = rightBorderColor
..rightBorderWidth = rightBorderWidth
..bottomBorderColor = bottomBorderColor
..bottomBorderWidth = bottomBorderWidth
..leftBorderColor = leftBorderColor
..leftBorderWidth = leftBorderWidth;
}
}

class TabFrameParentData extends ContainerBoxParentData {}

class RenderTabFrame extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, TabFrameParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, TabFrameParentData> {
RenderTabFrame({
required BuildContext context,
required TabController controller,
required ScrollController scrollController,
required double progress,
required Curve curve,
required Duration duration,
required List tabs,
required BorderRadius borderRadius,
required BorderRadius tabBorderRadius,
required double tabExtent,
required TabEdge tabEdge,
required Axis tabAxis,
required double tabsStart,
required double tabsEnd,
required double tabMinLength,
required double tabMaxLength,
required Color color,
required SemanticsConfiguration? semanticsConfiguration,
required bool enabled,
required bool enableFeedback,
required TextDirection textDirection,
required Color topBorderColor,
required double topBorderWidth,
required Color rightBorderColor,
required double rightBorderWidth,
required Color bottomBorderColor,
required double bottomBorderWidth,
required Color leftBorderColor,
required double leftBorderWidth,
}) : _context = context,
_controller = controller,
_scrollController = scrollController,
_progress = progress,
_curve = curve,
_duration = duration,
_tabs = tabs,
_borderRadius = borderRadius,
_tabBorderRadius = tabBorderRadius,
_tabExtent = tabExtent,
_tabEdge = tabEdge,
_tabAxis = tabAxis,
_tabsStart = tabsStart,
_tabsEnd = tabsEnd,
_tabMinLength = tabMinLength,
_tabMaxLength = tabMaxLength,
_color = color,
_semanticsConfiguration = semanticsConfiguration,
_enabled = enabled,
_enableFeedback = enableFeedback,
_textDirection = textDirection,
_topBorderColor = topBorderColor,
_topBorderWidth = topBorderWidth,
_rightBorderColor = rightBorderColor,
_rightBorderWidth = rightBorderWidth,
_bottomBorderColor = bottomBorderColor,
_bottomBorderWidth = bottomBorderWidth,
_leftBorderColor = leftBorderColor,
_leftBorderWidth = leftBorderWidth,
super();

BuildContext get context => _context;
BuildContext _context;
set context(BuildContext value) {
if (value == _context) return;
_context = value;
markNeedsLayout();
}

TabController get controller => _controller;
TabController _controller;
set controller(TabController value) {
if (value == _controller) return;
_controller = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}

ScrollController get scrollController => _scrollController;
ScrollController _scrollController;
set scrollController(ScrollController value) {
if (value == _scrollController) return;
_scrollController = value;
markNeedsLayout();
}

double get scrollOffset => _scrollOffset;
double _scrollOffset = 0;
set scrollOffset(double value) {
if (value == _scrollOffset || !_hasTabOverflow) return;
_scrollOffset = value.clamp(0, _tabOverflow);
markNeedsLayout();
}

double get progress => _progress;
double _progress;
set progress(double value) {
if (value == _progress) return;
assert(value >= 0 && value <= _tabs.length);

_progress = value;

_implicitScroll();

if (_progress == _progress.round()) {
  markNeedsSemanticsUpdate();
}

markNeedsLayout();

}

Curve get curve => _curve;
Curve _curve;
set curve(Curve value) {
if (value == _curve) return;
_curve = value;
}

Duration get duration => _duration;
Duration _duration;
set duration(Duration value) {
if (value == _duration) return;
_duration = value;
}

List get tabs => _tabs;
List _tabs;
set tabs(List value) {
if (value == _tabs) return;
assert(value.isNotEmpty);
_tabs = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}

BorderRadius get borderRadius => _borderRadius;
BorderRadius _borderRadius;
set borderRadius(BorderRadius value) {
if (value == _borderRadius) return;
_borderRadius = value;
markNeedsPaint();
}

BorderRadius get tabBorderRadius => _tabBorderRadius;
BorderRadius _tabBorderRadius;
set tabBorderRadius(BorderRadius value) {
if (value == _tabBorderRadius) return;
_tabBorderRadius = value;
markNeedsPaint();
}

double get tabExtent => _tabExtent;
double _tabExtent;
set tabExtent(double value) {
if (value == _tabExtent) return;
assert(value >= 0);
_tabExtent = value;
markNeedsLayout();
}

TabEdge get tabEdge => _tabEdge;
TabEdge _tabEdge;
set tabEdge(TabEdge value) {
if (value == _tabEdge) return;
_tabEdge = value;
markNeedsLayout();
}

Axis get tabAxis => _tabAxis;
Axis _tabAxis;
set tabAxis(Axis value) {
if (value == _tabAxis) return;
_tabAxis = value;
markNeedsLayout();
}

double get tabsStart => _tabsStart;
double _tabsStart;
set tabsStart(double value) {
if (value == _tabsStart) return;
_tabsStart = value;
markNeedsLayout();
}

double get tabsEnd => _tabsEnd;
double _tabsEnd;
set tabsEnd(double value) {
if (value == _tabsEnd) return;
_tabsEnd = value;
markNeedsLayout();
}

double get tabMinLength => _tabMinLength;
double _tabMinLength;
set tabMinLength(double value) {
if (value == _tabMinLength) return;
_tabMinLength = value;
markNeedsLayout();
}

double get tabMaxLength => _tabMaxLength;
double _tabMaxLength;
set tabMaxLength(double value) {
if (value == _tabMaxLength) return;
_tabMaxLength = value;
markNeedsLayout();
}

Color get color => _color;
Color _color;
set color(Color value) {
if (value == _color) return;
_color = value;
markNeedsPaint();
}

SemanticsConfiguration? get semanticsConfiguration => _semanticsConfiguration;
SemanticsConfiguration? _semanticsConfiguration;
set semanticsConfiguration(SemanticsConfiguration? value) {
if (value == _semanticsConfiguration) return;
_semanticsConfiguration = value;
markNeedsSemanticsUpdate();
}

bool get enabled => _enabled;
bool _enabled;
set enabled(bool value) {
if (value == _enabled) return;
_enabled = value;
_tapGestureRecognizer.onTapDown = _enabled ? _onTapDown : null;
_dragGestureRecognizer?.onUpdate = _enabled ? _onDragUpdate : null;
markNeedsSemanticsUpdate();
}

bool get enableFeedback => _enableFeedback;
bool _enableFeedback;
set enableFeedback(bool value) {
if (value == _enableFeedback) return;
_enableFeedback = value;
}

TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (value == _textDirection) return;
_textDirection = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}

double get topBorderWidth => _topBorderWidth;
double _topBorderWidth;
set topBorderWidth(double value) {
if (value == _topBorderWidth) return;
_topBorderWidth = value;
markNeedsLayout();
}

Color get topBorderColor => _topBorderColor;
Color _topBorderColor;
set topBorderColor(Color value) {
if (value == _topBorderColor) return;
_topBorderColor = value;
markNeedsPaint();
}

Color get rightBorderColor => _rightBorderColor;
Color _rightBorderColor;
set rightBorderColor(Color value) {
if (value == _rightBorderColor) return;
_rightBorderColor = value;

markNeedsPaint();

}

double get rightBorderWidth => _rightBorderWidth;
double _rightBorderWidth;
set rightBorderWidth(double value) {
if (value == _rightBorderWidth) return;
_rightBorderWidth = value;
markNeedsPaint();
}

Color get bottomBorderColor => _bottomBorderColor;
Color _bottomBorderColor;
set bottomBorderColor(Color value) {
if (value == _bottomBorderColor) return;
_bottomBorderColor = value;
markNeedsPaint();
}

double get bottomBorderWidth => _bottomBorderWidth;
double _bottomBorderWidth;
set bottomBorderWidth(double value) {
if (value == _bottomBorderWidth) return;
_bottomBorderWidth = value;
markNeedsPaint();
}

Color get leftBorderColor => _leftBorderColor;
Color _leftBorderColor;
set leftBorderColor(Color value) {
if (value == _leftBorderColor) return;
_leftBorderColor = value;
markNeedsPaint();
}

double get leftBorderWidth => _leftBorderWidth;
double _leftBorderWidth;
set leftBorderWidth(double value) {
if (value == _leftBorderWidth) return;
_leftBorderWidth = value;
markNeedsPaint();
}

@OverRide
void setupParentData(covariant RenderObject child) {
if (child.parentData is! TabFrameParentData) {
child.parentData = TabFrameParentData();
}
}

@OverRide
void attach(covariant PipelineOwner owner) {
super.attach(owner);

_tapGestureRecognizer = TapGestureRecognizer(debugOwner: this)
  ..onTapDown = enabled ? _onTapDown : null;

_dragGestureRecognizer = HorizontalDragGestureRecognizer(debugOwner: this)
  ..onUpdate = enabled ? _onDragUpdate : null;

}

@OverRide
void detach() {
super.detach();

_tapGestureRecognizer.dispose();
_dragGestureRecognizer?.dispose();

}

@OverRide
void dispose() {
_clipPathLayer.layer = null;
_tapGestureRecognizer.dispose();
_dragGestureRecognizer?.dispose();
super.dispose();
}

@OverRide
bool hitTestSelf(Offset position) {
return size.contains(position);
}

@OverRide
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
bool isHit = false;

for (var child = firstChild; child != null; child = childAfter(child)) {
  final TabFrameParentData childParentData = child.parentData as TabFrameParentData;
  isHit = result.addWithPaintOffset(
    offset: childParentData.offset,
    position: position,
    hitTest: (BoxHitTestResult result, Offset? transformed) {
      assert(transformed == position - childParentData.offset);
      return child!.hitTest(result, position: transformed!);
    },
  );
}

return isHit;

}

late TapGestureRecognizer _tapGestureRecognizer;
DragGestureRecognizer? _dragGestureRecognizer;

@OverRide
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
assert(debugHandleEvent(event, entry));

if (event is PointerScrollEvent) {
  if (_hasTabOverflow) {
    _onPointerScroll(event);
  }
} else if (event is PointerPanZoomStartEvent) {
  if (_hasTabOverflow) {
    _dragGestureRecognizer?.addPointerPanZoom(event);
  }
} else if (event is PointerDownEvent) {
  _tapGestureRecognizer.addPointer(event);
  if (_hasTabOverflow) {
    _dragGestureRecognizer?.addPointer(event);
  }
}

}

double _alignScrollDelta(PointerScrollEvent event) {
final Set pressed = HardwareKeyboard.instance.logicalKeysPressed;
final bool flipAxes = pressed.any(
ScrollConfiguration.of(context).pointerAxisModifiers.contains,
) &&
event.kind == PointerDeviceKind.mouse;

return flipAxes ? event.scrollDelta.dx : event.scrollDelta.dy;

}

void _handlePointerScroll(PointerSignalEvent event) {
assert(event is PointerScrollEvent);
final double delta = _alignScrollDelta(event as PointerScrollEvent);
scrollOffset += delta;
}

void _onPointerScroll(PointerScrollEvent event) {
final double dx = event.localPosition.dx;
final double dy = event.localPosition.dy;

if (_tabViewport.contains(dx, dy, _tabMetrics.totalLength)) {
  final double delta = _alignScrollDelta(event);
  if (delta != 0.0) {
    GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
  }
}

}

void _onTapDown(TapDownDetails details) {
final double dx = details.localPosition.dx;
final double dy = details.localPosition.dy;

if (_tabViewport.contains(dx, dy, _tabMetrics.totalLength)) {
  double pos = dx;

  controller.animateTo(
    (pos - _tabViewport.start + scrollOffset) ~/ _tabMetrics.length,
    curve: curve,
  );
  if (enableFeedback) {
    Feedback.forTap(context);
  }
}

return;

}

void _onDragUpdate(DragUpdateDetails details) {
final double dx = details.localPosition.dx;
final double dy = details.localPosition.dy;

if (_tabViewport.contains(dx, dy, _tabMetrics.totalLength)) {
  scrollOffset -= details.primaryDelta!;
}

}

void _implicitScroll() {
final (destinationStart, destinationEnd) = _getIndicatorBounds(controller.index.toDouble());
if (destinationStart >= _tabViewport.start && destinationEnd <= _tabViewport.end) {
return;
}

final (indicatorStart, indicatorEnd) = _getIndicatorBounds(progress);

if (indicatorEnd > _tabViewport.end && indicatorStart >= _tabViewport.start) {
  scrollOffset += indicatorEnd - _tabViewport.end;
} else if (indicatorStart < _tabViewport.start && indicatorEnd <= _tabViewport.end) {
  scrollOffset += indicatorStart - _tabViewport.start;
}

}

@OverRide
bool get alwaysNeedsCompositing => _hasTabOverflow;

bool _hasTabOverflow = false;
double _tabOverflow = 0;

late _TabViewport _tabViewport;
late _TabMetrics _tabMetrics;
_TabViewport? _prevTabViewport;

@OverRide
void performLayout() {
//Layout the main child
RenderBox? child = firstChild;

if (child == null) {
  return;
}

late final EdgeInsets edges;

edges = EdgeInsets.only(top: tabExtent);

child.layout(constraints.deflate(edges), parentUsesSize: true);

size = constraints.constrain(edges.inflateSize(child.size));

final TabFrameParentData childParentData = child.parentData as TabFrameParentData;

if (tabEdge == TabEdge.top) {
  childParentData.offset = Offset(0, tabExtent);
}

//Layout the tabs
child = childAfter(child);

_tabViewport = _TabViewport(
  parentSize: size,
  tabEdge: tabEdge,
  tabExtent: tabExtent,
  tabsStart: tabsStart,
  tabsEnd: tabsEnd,
);

_tabMetrics = _TabMetrics(
  count: tabs.length,
  range: _tabViewport.range,
  minLength: tabMinLength,
  maxLength: tabMaxLength,
);

bool tabViewportChanged = _prevTabViewport?.size != _tabViewport.size ||
    _prevTabViewport?.start != _tabViewport.start;

_prevTabViewport = _tabViewport;

_tabOverflow = _tabMetrics.totalLength - _tabViewport.range;

if (_hasTabOverflow != _tabOverflow > 0) {
  markNeedsCompositingBitsUpdate();
}
_hasTabOverflow = _tabOverflow > 0;

if (_hasTabOverflow && (_clipPath == null || tabViewportChanged)) {
  final double viewportWidth = _tabViewport.size.width;
  final double brx = tabBorderRadius.bottomRight.x;
  final double cutoff = max(0, _tabViewport.start - brx);

  _clipPath = Path.combine(
    PathOperation.xor,
    Path()
      ..addRect(
        Rect.fromPoints(
          Offset(cutoff, size.height - tabExtent),
          Offset(
            min(size.width, cutoff + viewportWidth + brx),
            size.height,
          ),
        ),
      ),
    Path()
      ..addRect(
        Rect.fromPoints(
          Offset.zero,
          Offset(size.width, size.height - tabExtent),
        ),
      ),
  );
  if (tabEdge == TabEdge.top) {
    _clipPath = _clipPath!.transform(
      (Matrix4.identity()
            ..scale(1.0, -1.0)
            ..translate(0.0, -size.height))
          .storage,
    );
  }
}

BoxConstraints tabConstraints = BoxConstraints(
  maxWidth: _tabMetrics.length,
  maxHeight: tabExtent,
);

for (var index = 0; child != null; index++, child = childAfter(child)) {
  child.layout(tabConstraints, parentUsesSize: true);

  final TabFrameParentData tabParentData = child.parentData as TabFrameParentData;

  final double displacement = _tabMetrics.length * index - scrollOffset;

  final EdgeInsets tabInsets = EdgeInsets.only(
    top: (tabConstraints.maxHeight - child.size.height) / 2,
    left: (tabConstraints.maxWidth - child.size.width) / 2,
  );

  switch (tabEdge) {
    case TabEdge.top:
      tabParentData.offset = Offset(
        tabInsets.left + displacement + _tabViewport.start,
        tabInsets.top,
      );
      break;
  }
}

}

@OverRide
bool get isRepaintBoundary => true;

@OverRide
bool get sizedByParent => false;

Path? _clipPath;

final LayerHandle _clipPathLayer = LayerHandle();

(double, double) _getIndicatorBounds(double factor) {
final double start = factor * _tabMetrics.length + _tabViewport.start - scrollOffset;
final double end = start + _tabMetrics.length;

return (start, end);

}

void _paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final Paint paint = Paint()..color = color;

canvas.drawPath(_getPath(size), paint);

// Draw the main path
Path path = _getPath(size);
canvas.drawPath(path, paint);

// Draw the border stroke
if (topBorderWidth > 0) {
  final Paint borderPaint = Paint()
    ..color = topBorderColor
    ..style = PaintingStyle.stroke
    ..strokeWidth = topBorderWidth;

  canvas.drawPath(path, borderPaint);

  // Draw each border separately
  _drawBorder(canvas, size, rightBorderColor, rightBorderWidth, BorderSide.right);
  _drawBorder(canvas, size, bottomBorderColor, bottomBorderWidth, BorderSide.bottom);
  _drawBorder(canvas, size, leftBorderColor, leftBorderWidth, BorderSide.left);
}

for (var child = firstChild; child != null; child = childAfter(child)) {
  context.paintChild(
    child,
    (child.parentData as TabFrameParentData).offset,
  );
}

}

@OverRide
void paint(
PaintingContext context,
Offset offset,
) {
if (_hasTabOverflow && _clipPath != null) {
_clipPathLayer.layer = context.pushClipPath(
needsCompositing,
offset,
Offset.zero & size,
_clipPath!,
_paint,
clipBehavior: Clip.hardEdge,
oldLayer: _clipPathLayer.layer,
);
} else {
_clipPathLayer.layer = null;
_paint(context, offset);
}
}

void _drawBorder(Canvas canvas, Size size, Color color, double width, BorderSide side) {
if (width > 0) {
final Paint borderPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = width;

  Path borderPath;
  switch (side) {
    case BorderSide.right:
      borderPath = _getRightBorderPath(size);
      break;
    case BorderSide.bottom:
      borderPath = _getBottomBorderPath(size);
      break;
    case BorderSide.left:
      borderPath = _getLeftBorderPath(size);
      break;
  }

  canvas.drawPath(borderPath, borderPaint);
}

}

Path _getPath(Size size) {
final double width = size.width;
final double height = size.height;

final (indicatorStart, indicatorEnd) = _getIndicatorBounds(progress);

double? critical1;
double? critical2;
double? critical3;
double? critical4;

double brx = borderRadius.bottomRight.x;
double tblx = tabBorderRadius.bottomLeft.x;
double tbrx = tabBorderRadius.topLeft.y;
double blx = borderRadius.bottomLeft.x;

final double sum1 = brx + tblx;
if (sum1 > 0 && width - indicatorEnd < sum1) {
  critical1 = brx / sum1 * (width - indicatorEnd);
  critical2 = tblx / sum1 * (width - indicatorEnd);
}

final double sum2 = tbrx + blx;
if (sum2 > 0 && indicatorStart < sum2) {
  critical3 = tbrx / sum2 * (indicatorStart);
  critical4 = blx / sum2 * (indicatorStart);
}

Path path = Path()
  ..moveTo(0, borderRadius.topLeft.y)
  ..quadraticBezierTo(0, 0, borderRadius.topLeft.x, 0)
  ..lineTo(width - borderRadius.topRight.x, 0)
  ..quadraticBezierTo(width, 0, width, borderRadius.topRight.y)
  ..lineTo(width, height - tabExtent - borderRadius.bottomRight.y)
  ..quadraticBezierTo(
    width,
    height - tabExtent,
    max(width - (critical1 ?? brx), indicatorEnd),
    height - tabExtent,
  )
  ..lineTo(
    min(width, indicatorEnd + (critical2 ?? tblx)),
    height - tabExtent,
  )
  ..quadraticBezierTo(
    indicatorEnd,
    height - tabExtent,
    indicatorEnd,
    height - tabExtent + tabBorderRadius.bottomLeft.y,
  )
  ..lineTo(indicatorEnd, height - tabBorderRadius.topLeft.y)
  ..quadraticBezierTo(
    indicatorEnd,
    height,
    indicatorEnd - tabBorderRadius.topLeft.x,
    height,
  )
  ..lineTo(indicatorStart + tabBorderRadius.topRight.x, height)
  ..quadraticBezierTo(
    indicatorStart,
    height,
    indicatorStart,
    height - tabBorderRadius.topRight.y,
  )
  ..lineTo(
    indicatorStart,
    height - tabExtent + tabBorderRadius.bottomRight.y,
  )
  ..quadraticBezierTo(
    indicatorStart,
    height - tabExtent,
    max(0, indicatorStart - (critical3 ?? tbrx)),
    height - tabExtent,
  )
  ..lineTo(min(critical4 ?? blx, indicatorStart), height - tabExtent)
  ..quadraticBezierTo(
    0,
    height - tabExtent,
    0,
    height - tabExtent - borderRadius.bottomLeft.y,
  )
  ..close();
if (tabEdge == TabEdge.top) {
  return path.transform(
    (Matrix4.identity()
          ..scale(1.0, -1.0)
          ..translate(0.0, -height))
        .storage,
  );
}
return path;

}

Path _getRightBorderPath(Size size) {
final double height = size.height;
Path path = Path()
..moveTo(size.width, borderRadius.topRight.y)
..lineTo(size.width, height - borderRadius.bottomRight.y)
..quadraticBezierTo(size.width, height, size.width - borderRadius.bottomRight.x, height);
return path;
}

Path _getBottomBorderPath(Size size) {
final double width = size.width;
final double height = size.height;
Path path = Path()
..moveTo(width - borderRadius.bottomRight.x, height)
..quadraticBezierTo(width, height, width, height - borderRadius.bottomRight.y)
..lineTo(borderRadius.bottomLeft.x, height)
..quadraticBezierTo(0, height, 0, height - borderRadius.bottomLeft.y);
return path;
}

Path _getLeftBorderPath(Size size) {
final double height = size.height;
Path path = Path()
..moveTo(0, height - borderRadius.bottomLeft.y)
..lineTo(0, borderRadius.topLeft.y)
..quadraticBezierTo(0, 0, borderRadius.topLeft.x, 0);
return path;
}
}

Try using this code to add border for all sides or a single side.