Skip to content

Commit 725d085

Browse files
juanruitinaCopilot
andcommitted
fix(manage): wrap package filter when it overflows beside search
When the locale produces long labels (e.g. Tamil), the package-type dropdown + system-apps switch row no longer clips past the right edge. The filter group is measured at its natural width; when it fits beside the search field it stays inline (existing behaviour), and when it doesn't the full filter group wraps to a second row while the sort control remains right-aligned. The previous Wrap-based approach let the package-type dropdown and the system-apps toggle split across lines independently. The new custom render object (_SearchFilterSortLayout / _RenderSearchFilterSort) keeps the two controls together as a single unit that only breaks as a group. UDENG-9624 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 088994e commit 725d085

1 file changed

Lines changed: 183 additions & 12 deletions

File tree

packages/app_center/lib/manage/manage_page.dart

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:math' as math;
23

34
import 'package:app_center/constants.dart';
45
import 'package:app_center/error/error.dart';
@@ -532,11 +533,12 @@ class _FilterRow extends ConsumerWidget {
532533

533534
final filterGroup = Row(
534535
mainAxisSize: MainAxisSize.min,
536+
crossAxisAlignment: CrossAxisAlignment.center,
535537
children: [
536538
packageTypeFilter,
537539
if (packageType == PackageTypeFilter.snap) ...[
538-
SizedBox(width: kSpacing),
539-
Flexible(child: showSystemApps),
540+
const SizedBox(width: kSpacing),
541+
showSystemApps,
540542
],
541543
],
542544
);
@@ -572,16 +574,16 @@ class _FilterRow extends ConsumerWidget {
572574
maxLines: 1,
573575
)..layout();
574576
final searchWidth = hintPainter.width + _searchFieldDecorationOverhead;
575-
final effectiveSearchWidth =
576-
searchWidth > _searchFieldDefaultWidth ? searchWidth : _searchFieldDefaultWidth;
577-
578-
return Row(
579-
crossAxisAlignment: CrossAxisAlignment.start,
580-
children: [
581-
SizedBox(width: effectiveSearchWidth, child: searchField),
582-
const SizedBox(width: kSpacing),
583-
Expanded(child: filterAndSort),
584-
],
577+
final effectiveSearchWidth = searchWidth > _searchFieldDefaultWidth
578+
? searchWidth
579+
: _searchFieldDefaultWidth;
580+
581+
return _SearchFilterSortLayout(
582+
searchWidth: effectiveSearchWidth,
583+
spacing: kSpacing,
584+
search: searchField,
585+
filter: filterGroup,
586+
sort: sortBy,
585587
);
586588
}
587589
}
@@ -674,6 +676,175 @@ class _RenderFilterSort extends RenderBox
674676
defaultHitTestChildren(result, position: position);
675677
}
676678

679+
/// Lays out a search field, a filter group, and a sort control.
680+
///
681+
/// When the filter group fits beside the search field it is placed there, with
682+
/// the sort control right-aligned (wrapping to a second line if needed).
683+
/// When the filter group is too wide to sit beside the search field the search
684+
/// field occupies the first row and the filter + sort occupy the second row
685+
/// (sort right-aligned, wrapping to a third line if needed).
686+
class _SearchFilterSortLayout extends MultiChildRenderObjectWidget {
687+
_SearchFilterSortLayout({
688+
required Widget search,
689+
required Widget filter,
690+
required Widget sort,
691+
required this.searchWidth,
692+
this.spacing = 0,
693+
}) : super(children: [search, filter, sort]);
694+
695+
final double searchWidth;
696+
final double spacing;
697+
698+
@override
699+
RenderObject createRenderObject(BuildContext context) =>
700+
_RenderSearchFilterSort(searchWidth: searchWidth, spacing: spacing);
701+
702+
@override
703+
void updateRenderObject(
704+
BuildContext context,
705+
_RenderSearchFilterSort renderObject,
706+
) {
707+
renderObject
708+
..searchWidth = searchWidth
709+
..spacing = spacing;
710+
}
711+
}
712+
713+
class _RenderSearchFilterSort extends RenderBox
714+
with
715+
ContainerRenderObjectMixin<RenderBox, _FilterSortParentData>,
716+
RenderBoxContainerDefaultsMixin<RenderBox, _FilterSortParentData> {
717+
_RenderSearchFilterSort({
718+
required double searchWidth,
719+
required double spacing,
720+
}) : _searchWidth = searchWidth,
721+
_spacing = spacing;
722+
723+
double _searchWidth;
724+
set searchWidth(double v) {
725+
if (_searchWidth == v) return;
726+
_searchWidth = v;
727+
markNeedsLayout();
728+
}
729+
730+
double _spacing;
731+
set spacing(double v) {
732+
if (_spacing == v) return;
733+
_spacing = v;
734+
markNeedsLayout();
735+
}
736+
737+
@override
738+
void setupParentData(RenderBox child) {
739+
if (child.parentData is! _FilterSortParentData) {
740+
child.parentData = _FilterSortParentData();
741+
}
742+
}
743+
744+
@override
745+
void performLayout() {
746+
final search = firstChild!;
747+
final filter = childAfter(search)!;
748+
final sort = lastChild!;
749+
750+
final searchParent = search.parentData! as _FilterSortParentData;
751+
final filterParent = filter.parentData! as _FilterSortParentData;
752+
final sortParent = sort.parentData! as _FilterSortParentData;
753+
754+
final maxW = constraints.maxWidth;
755+
756+
// Lay out sort and search to know their sizes.
757+
sort.layout(BoxConstraints(maxWidth: maxW), parentUsesSize: true);
758+
final sortSize = sort.size;
759+
760+
search.layout(
761+
BoxConstraints.tightFor(width: _searchWidth),
762+
parentUsesSize: true,
763+
);
764+
final searchSize = search.size;
765+
766+
// How much horizontal space is available to the right of the search field.
767+
final spaceForFilter = maxW - _searchWidth - _spacing;
768+
769+
// Measure the filter group's natural (unconstrained) width.
770+
final filterNatural = filter.getMaxIntrinsicWidth(double.infinity);
771+
772+
if (filterNatural <= spaceForFilter) {
773+
// ── Inline layout ──
774+
// [search] [filter ·····] [sort right-aligned]
775+
// Sort wraps to a second line if filter + sort don't both fit.
776+
filter.layout(
777+
BoxConstraints(maxWidth: spaceForFilter),
778+
parentUsesSize: true,
779+
);
780+
final filterSize = filter.size;
781+
782+
searchParent.offset = Offset.zero;
783+
filterParent.offset = Offset(_searchWidth + _spacing, 0);
784+
785+
if (filterSize.width + _spacing + sortSize.width <= spaceForFilter) {
786+
sortParent.offset = Offset(maxW - sortSize.width, 0);
787+
final rowH = [searchSize.height, filterSize.height, sortSize.height]
788+
.reduce(math.max);
789+
size = constraints.constrain(Size(maxW, rowH));
790+
} else {
791+
sortParent.offset = Offset(
792+
maxW - sortSize.width,
793+
filterSize.height + _spacing,
794+
);
795+
final row1H = math.max(searchSize.height, filterSize.height);
796+
size = constraints.constrain(
797+
Size(maxW, row1H + _spacing + sortSize.height),
798+
);
799+
}
800+
} else {
801+
// ── Stacked layout ──
802+
// [search]
803+
// [filter ·····] [sort right-aligned]
804+
// Sort wraps to a third line if filter + sort don't both fit.
805+
filter.layout(BoxConstraints(maxWidth: maxW), parentUsesSize: true);
806+
final filterSize = filter.size;
807+
808+
searchParent.offset = Offset.zero;
809+
filterParent.offset = Offset(0, searchSize.height + _spacing);
810+
811+
if (filterSize.width + _spacing + sortSize.width <= maxW) {
812+
sortParent.offset = Offset(
813+
maxW - sortSize.width,
814+
searchSize.height + _spacing,
815+
);
816+
final row2H = math.max(filterSize.height, sortSize.height);
817+
size = constraints.constrain(
818+
Size(maxW, searchSize.height + _spacing + row2H),
819+
);
820+
} else {
821+
sortParent.offset = Offset(
822+
maxW - sortSize.width,
823+
searchSize.height + _spacing + filterSize.height + _spacing,
824+
);
825+
size = constraints.constrain(
826+
Size(
827+
maxW,
828+
searchSize.height +
829+
_spacing +
830+
filterSize.height +
831+
_spacing +
832+
sortSize.height,
833+
),
834+
);
835+
}
836+
}
837+
}
838+
839+
@override
840+
void paint(PaintingContext context, Offset offset) =>
841+
defaultPaint(context, offset);
842+
843+
@override
844+
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
845+
defaultHitTestChildren(result, position: position);
846+
}
847+
677848
class _DebouncedSearchField extends ConsumerStatefulWidget {
678849
const _DebouncedSearchField({required this.hintText});
679850

0 commit comments

Comments
 (0)