|
1 | 1 | import 'dart:async'; |
| 2 | +import 'dart:math' as math; |
2 | 3 |
|
3 | 4 | import 'package:app_center/constants.dart'; |
4 | 5 | import 'package:app_center/error/error.dart'; |
@@ -532,11 +533,12 @@ class _FilterRow extends ConsumerWidget { |
532 | 533 |
|
533 | 534 | final filterGroup = Row( |
534 | 535 | mainAxisSize: MainAxisSize.min, |
| 536 | + crossAxisAlignment: CrossAxisAlignment.center, |
535 | 537 | children: [ |
536 | 538 | packageTypeFilter, |
537 | 539 | if (packageType == PackageTypeFilter.snap) ...[ |
538 | | - SizedBox(width: kSpacing), |
539 | | - Flexible(child: showSystemApps), |
| 540 | + const SizedBox(width: kSpacing), |
| 541 | + showSystemApps, |
540 | 542 | ], |
541 | 543 | ], |
542 | 544 | ); |
@@ -572,16 +574,16 @@ class _FilterRow extends ConsumerWidget { |
572 | 574 | maxLines: 1, |
573 | 575 | )..layout(); |
574 | 576 | 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, |
585 | 587 | ); |
586 | 588 | } |
587 | 589 | } |
@@ -674,6 +676,175 @@ class _RenderFilterSort extends RenderBox |
674 | 676 | defaultHitTestChildren(result, position: position); |
675 | 677 | } |
676 | 678 |
|
| 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 | + |
677 | 848 | class _DebouncedSearchField extends ConsumerStatefulWidget { |
678 | 849 | const _DebouncedSearchField({required this.hintText}); |
679 | 850 |
|
|
0 commit comments