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
3 changes: 3 additions & 0 deletions docs_snippets/lib/usages/foundation/collapsible.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// ignore_for_file: avoid_redundant_argument_values

import 'package:flutter/widgets.dart';

import 'package:forui/forui.dart';

const collapsible = FCollapsible(
// {@category "Core"}
value: 0.5,
axis: .vertical,
child: Placeholder(),
// {@endcategory}
);
4 changes: 4 additions & 0 deletions forui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ and an API similar to other widgets.
* Add support for styling the label in the `focused` state.


### `FCollapsible`
* Add `FCollapsible.axis` to collapse the child horizontally.


### `FContextMenu` (New)
* Add `FContextMenu`.

Expand Down
58 changes: 45 additions & 13 deletions forui/lib/src/foundation/collapsible.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,74 @@ import 'package:flutter/widgets.dart';
/// See:
/// * https://forui.dev/docs/widgets/foundation/collapsible for working examples.
class FCollapsible extends StatelessWidget {
/// The axis along which the child collapses. Defaults to [Axis.vertical].
final Axis axis;

/// The value of the collapsible.
final double value;

/// The child of the collapsible.
final Widget child;

/// Creates a [FCollapsible].
const FCollapsible({required this.value, required this.child, super.key});
const FCollapsible({required this.value, required this.child, this.axis = .vertical, super.key});

// We use a combination of a custom render box & clip rect to avoid visual oddities. This is caused by
// RenderPaddings (created by Paddings in the child) shrinking the constraints by the given padding, causing the
// child to layout at a smaller size while the amount of padding remains the same.
@override
Widget build(BuildContext context) => _Expandable(
axis: axis,
value: value,
child: ClipRect(clipper: _Clipper(value), child: child),
child: ClipRect(clipper: _Clipper(value, axis), child: child),
);

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('value', value));
properties
..add(EnumProperty('axis', axis))
..add(DoubleProperty('value', value));
}
}

class _Expandable extends SingleChildRenderObjectWidget {
final Axis axis;
final double value;

const _Expandable({required this.value, required super.child});
const _Expandable({required this.axis, required this.value, required super.child});

@override
RenderObject createRenderObject(BuildContext _) => _RenderExpandable(value);
RenderObject createRenderObject(BuildContext _) => _RenderExpandable(value, axis);

@override
void updateRenderObject(BuildContext context, _RenderExpandable renderObject) => renderObject..value = value;
void updateRenderObject(BuildContext context, _RenderExpandable renderObject) => renderObject
..axis = axis
..value = value;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(PercentProperty('value', value));
properties
..add(EnumProperty('axis', axis))
..add(PercentProperty('value', value));
}
}

class _RenderExpandable extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
double _value;
Axis _axis;

_RenderExpandable(this._value);
_RenderExpandable(this._value, this._axis);

@override
void performLayout() {
if (child case final child?) {
child.layout(constraints.normalize(), parentUsesSize: true);
size = Size(child.size.width, child.size.height * _value);
size = switch (_axis) {
.vertical => Size(child.size.width, child.size.height * _value),
.horizontal => Size(child.size.width * _value, child.size.height),
};
} else {
size = constraints.smallest;
}
Expand All @@ -85,6 +100,17 @@ class _RenderExpandable extends RenderBox with RenderObjectWithChildMixin<Render
return false;
}

Axis get axis => _axis;

set axis(Axis axis) {
if (_axis == axis) {
return;
}

_axis = axis;
markNeedsLayout();
}

double get value => _value;

set value(double value) {
Expand All @@ -99,18 +125,24 @@ class _RenderExpandable extends RenderBox with RenderObjectWithChildMixin<Render
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(PercentProperty('value', value));
properties
..add(EnumProperty('axis', axis))
..add(PercentProperty('value', value));
}
}

class _Clipper extends CustomClipper<Rect> {
final double percentage;
final Axis axis;

_Clipper(this.percentage);
_Clipper(this.percentage, this.axis);

@override
Rect getClip(Size size) => Offset.zero & Size(size.width, size.height * percentage);
Rect getClip(Size size) => switch (axis) {
.vertical => Offset.zero & Size(size.width, size.height * percentage),
.horizontal => Offset.zero & Size(size.width * percentage, size.height),
};

@override
bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage;
bool shouldReclip(covariant _Clipper oldClipper) => oldClipper.percentage != percentage || oldClipper.axis != axis;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:forui/forui.dart';
import '../test_scaffold.dart';
import '../../test_scaffold.dart';

void main() {
group('FCollapsible', () {
Expand All @@ -25,7 +25,7 @@ void main() {
await expectLater(find.byType(TestScaffold), matchesGoldenFile('collapsible/${theme.name}/fully-expanded.png'));
});

testWidgets('half expanded', (tester) async {
testWidgets('half expanded vertical', (tester) async {
await tester.pumpWidget(
TestScaffold(
theme: theme.data,
Expand All @@ -36,7 +36,28 @@ void main() {
),
);

await expectLater(find.byType(TestScaffold), matchesGoldenFile('collapsible/${theme.name}/half-expanded.png'));
await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('collapsible/${theme.name}/half-expanded-vertical.png'),
);
});

testWidgets('half expanded horizontal', (tester) async {
await tester.pumpWidget(
TestScaffold(
theme: theme.data,
child: const FCollapsible(
value: 0.5,
axis: Axis.horizontal,
child: ColoredBox(color: Colors.yellow, child: SizedBox.square(dimension: 50)),
),
),
);

await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('collapsible/${theme.name}/half-expanded-horizontal.png'),
);
});

testWidgets('fully collapsed', (tester) async {
Expand Down
26 changes: 26 additions & 0 deletions forui/test/src/foundation/collapsible/collapsible_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';

import 'package:flutter_test/flutter_test.dart';

import 'package:forui/forui.dart';
import '../../test_scaffold.dart';

void main() {
group('FCollapsible', () {
testWidgets('relayouts when axis changes', (tester) async {
await tester.pumpWidget(
TestScaffold(child: const FCollapsible(value: 0.5, child: SizedBox.square(dimension: 50))),
);

expect(tester.getSize(find.byType(FCollapsible)), const Size(50, 25));

await tester.pumpWidget(
TestScaffold(
child: const FCollapsible(value: 0.5, axis: Axis.horizontal, child: SizedBox.square(dimension: 50)),
),
);

expect(tester.getSize(find.byType(FCollapsible)), const Size(25, 50));
});
});
}
Loading