import "package:flutter/rendering.dart"; import "package:flutter/widgets.dart"; import "package:flutter_test/flutter_test.dart"; import "package:garage_ui/garage_ui.dart"; import "support/test_scheme.dart"; // A centred label sat in an Expanded, so it centred in the space LEFT OVER // after a leading icon rather than across the button - one icon threw the text // off centre by half its own width plus the gap. void main() { Widget host(Widget child) => Directionality( textDirection: TextDirection.ltr, child: GarageTheme( data: ThemeData(colorScheme: testScheme, density: const Density()), child: Center(child: SizedBox(width: 300, child: child)), ), ); Future offset(WidgetTester t, Widget b) async { await t.pumpWidget(host(b)); return t.getRect(find.text("Label")).center.dx - t.getRect(find.byType(Button)).center.dx; } Button btn({Widget? leading, Widget? trailing, AlignmentGeometry? alignment}) => Button( style: const ButtonStyle.secondary(), alignment: alignment, leading: leading, trailing: trailing, onPressed: () {}, child: const Text("Label"), ); const icon = Icon(LucideIcons.mail); _fieldCentring(); _labelNeverWraps(); testWidgets("centred label is centred with no icons", (t) async { expect(await offset(t, btn(alignment: Alignment.center)), 0.0); }); testWidgets("centred label is centred with a leading icon", (t) async { expect(await offset(t, btn(alignment: Alignment.center, leading: icon)), 0.0); }); testWidgets("centred label is centred with a trailing icon", (t) async { expect(await offset(t, btn(alignment: Alignment.center, trailing: icon)), 0.0); }); testWidgets("centred label is centred with both", (t) async { expect( await offset(t, btn(alignment: Alignment.center, leading: icon, trailing: icon)), 0.0); }); testWidgets("no alignment means no balancing - the button still shrink-wraps", (t) async { // without an alignment there is no centre to miss, and padding the empty // side out would only make the button wider. // unconstrained, so the button reports its own intrinsic width Widget loose(Widget c) => Directionality( textDirection: TextDirection.ltr, child: GarageTheme( data: ThemeData(colorScheme: testScheme, density: const Density()), child: Center(child: c), ), ); await t.pumpWidget(loose(btn(leading: icon))); final unaligned = t.getSize(find.byType(Button)).width; await t.pumpWidget(loose(btn(alignment: Alignment.center, leading: icon))); final aligned = t.getSize(find.byType(Button)).width; // the balanced one is wider by exactly the mirrored icon plus its gap expect(aligned, greaterThan(unaligned)); expect(aligned - unaligned, const Density().iconSize + const Density().controlGap); }); testWidgets("an icon button with a leading icon isnt balanced out", (t) async { // there is no label in here to sit off centre - both slots are glyphs - so // the mirror spacer is 15px of nothing. the snap targets button in the // blender header measured 52 wide for 38 of content because of it. Widget loose(Widget c) => Directionality( textDirection: TextDirection.ltr, child: GarageTheme( data: ThemeData(colorScheme: testScheme, density: const Density()), child: Center(child: c), ), ); await t.pumpWidget(loose(IconButton.outline( density: ControlDensity.compact, leading: Icon(LucideIcons.ruler_dimension_line).iconSmall, icon: Icon(LucideIcons.chevron_down).iconSmall, onPressed: () {}, ))); const d = Density(); final pad = ControlDensity.compact .resolveIcon(ThemeData(colorScheme: testScheme, density: d)); expect( t.getSize(find.byType(IconButton)).width, pad.horizontal + d.iconSize * 2 + d.controlGap, ); }); } // The same defect in TextField: centred text sits in an Expanded, so a leading // feature pushed it off the field's centre by half that feature plus the gap. void _fieldCentring() { Widget host(Widget child) => Directionality( textDirection: TextDirection.ltr, child: GarageTheme( data: ThemeData(colorScheme: testScheme, density: const Density()), child: Center(child: SizedBox(width: 300, child: child)), ), ); testWidgets("centred field text is centred with a leading feature", (t) async { await t.pumpWidget(host(const TextField( initialValue: "abc", readOnly: true, enabled: false, textAlign: TextAlign.center, features: [InputFeature.leading(Icon(LucideIcons.mail))], ))); final field = t.getRect(find.byType(TextField)); final text = t.getRect(find.text("abc")); expect((text.center.dx - field.center.dx).abs(), lessThan(0.5)); }); testWidgets("balancing uses a spacer, not a second icon", (t) async { // a copy of the feature would sit in the tree twice, which is how this // broke a test that looked for exactly one icon inside a field. await t.pumpWidget(host(const TextField( initialValue: "abc", readOnly: true, enabled: false, textAlign: TextAlign.center, features: [InputFeature.leading(Icon(LucideIcons.mail))], ))); expect( find.descendant( of: find.byType(TextField), matching: find.byIcon(LucideIcons.mail)), findsOneWidget, ); }); } // A leading icon takes width off the label's allowance; Expanded forces the // label to absorb it, and Text used to resolve that by wrapping - "Use" on one // line and the rest on a second the button's height hid. It reads as a // truncation but never was one. void _labelNeverWraps() { Widget host(Widget child, double cap) => Directionality( textDirection: TextDirection.ltr, child: GarageTheme( data: ThemeData(colorScheme: testScheme, density: const Density()), child: Center(child: SizedBox(width: cap, child: Wrap(children: [child]))), ), ); for (final cap in [400.0, 130.0, 90.0]) { testWidgets("label stays on one line at width $cap", (t) async { await t.pumpWidget(host( Button( style: const ButtonStyle.primary(), leading: const Icon(LucideIcons.mail), onPressed: () {}, child: const Text("Use passkey"), ), cap, )); final rp = t.renderObject(find.text("Use passkey")) as RenderParagraph; expect(rp.size.height, const Density().lineBox, reason: "a wrapped label is twice this tall"); }); } }