From 9c3bfef0622681f805b11fbfe6614dd69458b6f4 Mon Sep 17 00:00:00 2001 From: suragch Date: Tue, 4 Jun 2024 13:16:49 +0800 Subject: [PATCH 1/7] implement minimal Rectangle functionality in Flutter --- .../project.pbxproj | 16 +++++++ .../swift_ui_gallery/ContentView.swift | 4 ++ .../swift_ui_gallery/Localizable.xcstrings | 15 +++++++ .../shapes/RectangleExamples.swift | 43 ++++++++++++++++++ .../swift_ui_gallery/shapes/ShapesPage.swift | 21 +++++++++ example/lib/home_screen.dart | 44 +++++++++++++++---- example/lib/shapes/rectangle.dart | 38 ++++++++++++++++ example/lib/shapes/shapes_examples.dart | 25 +++++++++++ .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/src/layout_adjustments/frame.dart | 14 ++++++ lib/src/shapes/rectangle.dart | 22 ++++++++++ lib/src/shapes/shape_style.dart | 15 +++++++ lib/swift_ui.dart | 2 + 14 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift create mode 100644 doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/ShapesPage.swift create mode 100644 example/lib/shapes/rectangle.dart create mode 100644 example/lib/shapes/shapes_examples.dart create mode 100644 lib/src/shapes/rectangle.dart create mode 100644 lib/src/shapes/shape_style.dart diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj index 1be5564..8c2de4d 100644 --- a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 714951452C0EB31C00818B06 /* ShapesPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714951442C0EB31C00818B06 /* ShapesPage.swift */; }; + 714951472C0EB52400818B06 /* RectangleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714951462C0EB52400818B06 /* RectangleExamples.swift */; }; 7179CF832B26DEB900C5927B /* TextTypographyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7179CF822B26DEB900C5927B /* TextTypographyPage.swift */; }; 7179CF852B26DEDD00C5927B /* TextAccessibilityPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7179CF842B26DEDD00C5927B /* TextAccessibilityPage.swift */; }; 7179CF872B26DF0E00C5927B /* TextLocalizationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7179CF862B26DF0E00C5927B /* TextLocalizationPage.swift */; }; @@ -51,6 +53,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 714951442C0EB31C00818B06 /* ShapesPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesPage.swift; sourceTree = ""; }; + 714951462C0EB52400818B06 /* RectangleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleExamples.swift; sourceTree = ""; }; 7179CF822B26DEB900C5927B /* TextTypographyPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextTypographyPage.swift; sourceTree = ""; }; 7179CF842B26DEDD00C5927B /* TextAccessibilityPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAccessibilityPage.swift; sourceTree = ""; }; 7179CF862B26DF0E00C5927B /* TextLocalizationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLocalizationPage.swift; sourceTree = ""; }; @@ -105,6 +109,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 714951432C0EB2C200818B06 /* shapes */ = { + isa = PBXGroup; + children = ( + 714951442C0EB31C00818B06 /* ShapesPage.swift */, + 714951462C0EB52400818B06 /* RectangleExamples.swift */, + ); + path = shapes; + sourceTree = ""; + }; 7179CF882B26E2E900C5927B /* typography */ = { isa = PBXGroup; children = ( @@ -179,6 +192,7 @@ C9CAE27F2AC23AB40042DBC7 /* swift_ui_gallery */ = { isa = PBXGroup; children = ( + 714951432C0EB2C200818B06 /* shapes */, B3CBD1542B2E18320095DE1F /* Localizable.xcstrings */, B3CBD1562B2E1BE20095DE1F /* LocalizableAlternative.xcstrings */, C9CAE2B92AE108790042DBC7 /* layouts */, @@ -411,12 +425,14 @@ C9CAE2AD2AE106420042DBC7 /* CollectionsPage.swift in Sources */, 719116892B216D500007C4DE /* TextPage.swift in Sources */, 7179CF852B26DEDD00C5927B /* TextAccessibilityPage.swift in Sources */, + 714951472C0EB52400818B06 /* RectangleExamples.swift in Sources */, 7179CF832B26DEB900C5927B /* TextTypographyPage.swift in Sources */, C9CAE2832AC23AB40042DBC7 /* ContentView.swift in Sources */, C9CAE2BB2AE1089A0042DBC7 /* LayoutsPage.swift in Sources */, C9CAE2B12AE1065F0042DBC7 /* ControlsPage.swift in Sources */, C9CAE2AB2ADFB1480042DBC7 /* PrimitivesPage.swift in Sources */, C9FB17582C0C4D25004479AA /* ZStackExamples.swift in Sources */, + 714951452C0EB31C00818B06 /* ShapesPage.swift in Sources */, C902DDE72AE3904400242DBA /* TodoPage.swift in Sources */, C9CAE2B32AE1066B0042DBC7 /* MotionPage.swift in Sources */, B3DD96702B66513800F66E9F /* ImageLocalizationPage.swift in Sources */, diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/ContentView.swift b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/ContentView.swift index 74498a4..3c9cf27 100644 --- a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/ContentView.swift +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/ContentView.swift @@ -26,6 +26,10 @@ struct ContentView: View { MotionPage().tabItem{ Label("Motion", systemImage: "move.3d") } + + ShapesPage().tabItem{ + Label("Shapes", systemImage: "square.on.circle") + } } } } diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/Localizable.xcstrings b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/Localizable.xcstrings index 71e829c..cafeb1b 100644 --- a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/Localizable.xcstrings +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/Localizable.xcstrings @@ -196,12 +196,18 @@ }, "First" : { + }, + "First\nSecond" : { + }, "flight" : { }, "footnote" : { + }, + "Fourth\nFifth\nSixth" : { + }, "Frame" : { @@ -367,6 +373,9 @@ }, "Other initializers" : { + }, + "Overlay" : { + }, "Party LET" : { @@ -412,6 +421,9 @@ }, "Raised pitch" : { + }, + "Rectangle" : { + }, "red" : { @@ -455,6 +467,9 @@ }, "serif" : { + }, + "Shapes" : { + }, "size 6" : { diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift new file mode 100644 index 0000000..213f38f --- /dev/null +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift @@ -0,0 +1,43 @@ +import SwiftUI + +struct RectanglePage: View { + var body: some View { + ScrollView { + VStack(spacing: 20) { + // Basic rectangle + Rectangle() + .fill(Color.blue) + .frame(width: 200, height: 100) + + // Rectangle with stroke + Rectangle() + .stroke(Color.red, lineWidth: 4) + .frame(width: 200, height: 100) + + // Rectangle with rounded corners + Rectangle() + .fill(Color.green) + .cornerRadius(20) + .frame(width: 200, height: 100) + + // Rectangle with gradient fill + Rectangle() + .fill(LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .leading, endPoint: .trailing)) + .frame(width: 200, height: 100) + + // Rectangle with shadow + Rectangle() + .fill(Color.purple) + .shadow(color: .gray, radius: 10, x: 0, y: 10) + .frame(width: 200, height: 100) + } + } + } +} + +struct RectanglePage_Previews: PreviewProvider { + static var previews: some View { + RectanglePage() + } +} + diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/ShapesPage.swift b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/ShapesPage.swift new file mode 100644 index 0000000..4fbe5ca --- /dev/null +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/ShapesPage.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct ShapesPage: View { + var body: some View { + NavigationStack { + List() { + NavigationLink { + RectanglePage() + } label: { + Text("Rectangle") + } + }.navigationTitle("Shapes") + } + } +} + +struct ShapesPage_Previews: PreviewProvider { + static var previews: some View { + ShapesPage() + } +} diff --git a/example/lib/home_screen.dart b/example/lib/home_screen.dart index d281944..4877ede 100644 --- a/example/lib/home_screen.dart +++ b/example/lib/home_screen.dart @@ -1,9 +1,11 @@ import 'package:example/collections/collection_examples.dart'; import 'package:example/controls/controls_examples.dart'; +import 'package:example/infrastructure/inventory_page.dart'; import 'package:example/layouts/layout_examples.dart'; import 'package:example/motion/motion_examples.dart'; import 'package:example/primitives/primitive_examples.dart'; import 'package:example/scaffolds/scaffold_examples.dart'; +import 'package:example/shapes/shapes_examples.dart'; import 'package:flutter/cupertino.dart'; class HomeScreen extends StatefulWidget { @@ -29,9 +31,7 @@ class _HomeScreenState extends State { case 3: return const ScaffoldsPage(); case 4: - return const ControlsPage(); - case 5: - return const MotionPage(); + return const OverflowPage(); default: throw Exception("Unknown tab index: $index"); } @@ -55,15 +55,41 @@ class _HomeScreenState extends State { label: "Scaffolds", ), BottomNavigationBarItem( - icon: Icon(CupertinoIcons.slider_horizontal_3), - label: "Controls", - ), - BottomNavigationBarItem( - icon: Icon(CupertinoIcons.move), - label: "Motion", + icon: Icon(CupertinoIcons.ellipsis), + label: "More", ), ], ), ); } } + +class OverflowPage extends StatelessWidget { + const OverflowPage({super.key}); + + @override + Widget build(BuildContext context) { + return InventoryPage( + title: "More", + groups: [ + InventoryGroup( + title: 'ADDITIONAL CATEGORIES', + items: [ + InventoryItem( + label: "Controls", + pageBuilder: (context) => const ControlsPage(), + ), + InventoryItem( + label: "Motion", + pageBuilder: (context) => const MotionPage(), + ), + InventoryItem( + label: "Shapes", + pageBuilder: (context) => const ShapesPage(), + ), + ], + ), + ], + ); + } +} diff --git a/example/lib/shapes/rectangle.dart b/example/lib/shapes/rectangle.dart new file mode 100644 index 0000000..c037941 --- /dev/null +++ b/example/lib/shapes/rectangle.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:swift_ui/swift_ui.dart'; + +class RectangleDemo extends StatelessWidget { + const RectangleDemo({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const VStack( + [ + Frame( + width: 200, + height: 100, + child: Rectangle( + fill: ShapeStyle( + color: Colors.blue, + ), + ), + ), + Frame( + width: 200, + height: 100, + child: Rectangle( + fill: ShapeStyle( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], + begin: AlignmentDirectional.centerStart, + end: AlignmentDirectional.centerEnd, + ), + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/shapes/shapes_examples.dart b/example/lib/shapes/shapes_examples.dart new file mode 100644 index 0000000..658ab10 --- /dev/null +++ b/example/lib/shapes/shapes_examples.dart @@ -0,0 +1,25 @@ +import 'package:example/demo_screen.dart'; +import 'package:example/infrastructure/inventory_page.dart'; +import 'package:example/shapes/rectangle.dart'; +import 'package:flutter/cupertino.dart'; + +class ShapesPage extends StatelessWidget { + const ShapesPage({super.key}); + + @override + Widget build(BuildContext context) { + return InventoryPage( + title: "Shapes", + groups: [ + InventoryGroup( + items: [ + InventoryItem( + label: "Rectangle", + pageBuilder: createDemo(const RectangleDemo()), + ), + ], + ), + ], + ); + } +} diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 27e0f50..7d9ca67 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 397f3d3..15368ec 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Sat, 8 Jun 2024 10:51:40 +0800 Subject: [PATCH 2/7] completed basic Rectangle functionality --- .gitignore | 3 + .vscode/launch.json | 13 ++ CHANGELOG.md | 6 + .../project.pbxproj | 2 +- .../shapes/RectangleExamples.swift | 21 +-- example/lib/shapes/rectangle.dart | 31 ++++- lib/src/shapes/rectangle.dart | 127 +++++++++++++++++- lib/src/shapes/shape_style.dart | 13 ++ test/shapes/goldens/rectangle_smoke-test.png | Bin 0 -> 12943 bytes test/shapes/rectangle_golden_test.dart | 62 +++++++++ test/shapes/rectangle_test.dart | 3 + 11 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 test/shapes/goldens/rectangle_smoke-test.png create mode 100644 test/shapes/rectangle_golden_test.dart create mode 100644 test/shapes/rectangle_test.dart diff --git a/.gitignore b/.gitignore index ac5aa98..21c41dc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ + +# Don't check in golden failure output. +**/failures/*.png \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6788b48 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "configurations": [ + { + "name": "Golden", + "request": "launch", + "type": "dart", + "codeLens": { + "for": ["run-test", "run-test-file"] + }, + "args": ["--update-goldens"] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2e25f..b3143b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,8 @@ +## 0.0.2 + +- Added `HStack` widget. +- Added `VStack` widget. +- Added `Rectangle` widget. + ## 0.0.1 - Sept, 2023 Setting up the project structure. This release is not usable. diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj index 8c2de4d..4651f65 100644 --- a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery.xcodeproj/project.pbxproj @@ -192,7 +192,6 @@ C9CAE27F2AC23AB40042DBC7 /* swift_ui_gallery */ = { isa = PBXGroup; children = ( - 714951432C0EB2C200818B06 /* shapes */, B3CBD1542B2E18320095DE1F /* Localizable.xcstrings */, B3CBD1562B2E1BE20095DE1F /* LocalizableAlternative.xcstrings */, C9CAE2B92AE108790042DBC7 /* layouts */, @@ -201,6 +200,7 @@ C9CAE2B62AE107D30042DBC7 /* scaffolds */, C9CAE2B52AE107C90042DBC7 /* collections */, C9CAE2B42AE107B60042DBC7 /* primitives */, + 714951432C0EB2C200818B06 /* shapes */, C9CAE2802AC23AB40042DBC7 /* swift_ui_galleryApp.swift */, C9CAE2822AC23AB40042DBC7 /* ContentView.swift */, C9CAE2842AC23AB50042DBC7 /* Assets.xcassets */, diff --git a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift index 213f38f..baf49cb 100644 --- a/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift +++ b/doc_swift_ui_gallery/swift_ui_gallery/swift_ui_gallery/shapes/RectangleExamples.swift @@ -4,31 +4,24 @@ struct RectanglePage: View { var body: some View { ScrollView { VStack(spacing: 20) { - // Basic rectangle + // Rectangle with solid fill Rectangle() .fill(Color.blue) .frame(width: 200, height: 100) - // Rectangle with stroke - Rectangle() - .stroke(Color.red, lineWidth: 4) - .frame(width: 200, height: 100) - - // Rectangle with rounded corners + // Rectangle with gradient fill Rectangle() - .fill(Color.green) - .cornerRadius(20) + .fill(LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .leading, endPoint: .trailing)) .frame(width: 200, height: 100) - // Rectangle with gradient fill + // Rectangle with solid stroke Rectangle() - .fill(LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .leading, endPoint: .trailing)) + .stroke(Color.red, lineWidth: 4) .frame(width: 200, height: 100) - // Rectangle with shadow + // Rectangle with gradient stroke Rectangle() - .fill(Color.purple) - .shadow(color: .gray, radius: 10, x: 0, y: 10) + .stroke(LinearGradient(gradient: Gradient(colors: [.yellow, .blue]), startPoint: .leading, endPoint: .trailing), lineWidth: 14) .frame(width: 200, height: 100) } } diff --git a/example/lib/shapes/rectangle.dart b/example/lib/shapes/rectangle.dart index c037941..0f335f0 100644 --- a/example/lib/shapes/rectangle.dart +++ b/example/lib/shapes/rectangle.dart @@ -10,6 +10,7 @@ class RectangleDemo extends StatelessWidget { Widget build(BuildContext context) { return const VStack( [ + // Rectangle with solid fill Frame( width: 200, height: 100, @@ -19,6 +20,8 @@ class RectangleDemo extends StatelessWidget { ), ), ), + + // Rectangle with gradient fill Frame( width: 200, height: 100, @@ -26,12 +29,36 @@ class RectangleDemo extends StatelessWidget { fill: ShapeStyle( gradient: LinearGradient( colors: [Colors.yellow, Colors.orange], - begin: AlignmentDirectional.centerStart, - end: AlignmentDirectional.centerEnd, ), ), ), ), + + // Rectangle with solid stroke + Frame( + width: 200, + height: 100, + child: Rectangle( + stroke: ShapeStyle( + color: Colors.red, + ), + strokeLineWidth: 4.0, + ), + ), + + // Rectangle with gradient stroke + Frame( + width: 200, + height: 100, + child: Rectangle( + stroke: ShapeStyle( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.blue], + ), + ), + strokeLineWidth: 14.0, + ), + ), ], ); } diff --git a/lib/src/shapes/rectangle.dart b/lib/src/shapes/rectangle.dart index 546be7f..5e600bf 100644 --- a/lib/src/shapes/rectangle.dart +++ b/lib/src/shapes/rectangle.dart @@ -1,22 +1,139 @@ import 'package:flutter/widgets.dart'; import 'package:swift_ui/src/shapes/shape_style.dart'; -/// A rectangular shape that takes the size of +/// A rectangular shape that takes the size of the parent class Rectangle extends StatelessWidget { const Rectangle({ super.key, this.fill, + this.stroke, + this.strokeLineWidth = 1.0, }); + /// A color or gradient used for painting the interior of the rectangle. final ShapeStyle? fill; + /// A color or gradient used for painting the outline of the rectangle. + /// + /// The stroke line is drawn inside the frame of the rectangle, and its + /// width is determined by [strokeLineWidth]. + final ShapeStyle? stroke; + + /// The width of the stroke used to paint the outline of the rectangle. + /// + /// The default is 1.0. + final double strokeLineWidth; + @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: fill?.color, - gradient: fill?.gradient, + return SizedBox.expand( + child: CustomPaint( + painter: _RectanglePainter( + fill: fill, + stroke: stroke, + strokeLineWidth: strokeLineWidth, + ), ), ); } } + +class _RectanglePainter extends CustomPainter { + _RectanglePainter({ + required this.fill, + required this.stroke, + required this.strokeLineWidth, + }); + + final ShapeStyle? fill; + final ShapeStyle? stroke; + final double strokeLineWidth; + + @override + void paint(Canvas canvas, Size size) { + _paintFill(canvas, size, fill); + _paintStroke(canvas, size, stroke, strokeLineWidth); + } + + void _paintFill(Canvas canvas, Size size, ShapeStyle? fillStyle) { + if (fillStyle == null) { + return; + } + if (fillStyle.color != null) { + _paintSolidFill(canvas, size, fillStyle); + } else { + _paintGradientFill(canvas, size, fillStyle); + } + } + + void _paintSolidFill(Canvas canvas, Size size, ShapeStyle fillStyle) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final fillPaint = Paint() + ..style = PaintingStyle.fill + ..color = fillStyle.color ?? const Color(0x00000000); + canvas.drawRect(rect, fillPaint); + } + + void _paintGradientFill(Canvas canvas, Size size, ShapeStyle fillStyle) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final fillPaint = Paint() + ..style = PaintingStyle.fill + ..shader = fillStyle.gradient?.createShader(rect); + canvas.drawRect(rect, fillPaint); + } + + void _paintStroke( + Canvas canvas, + Size size, + ShapeStyle? strokeStyle, + double lineWidth, + ) { + if (strokeStyle == null) { + return; + } + final rect = Rect.fromLTWH( + lineWidth / 2, + lineWidth / 2, + size.width - lineWidth, + size.height - lineWidth, + ); + if (strokeStyle.color != null) { + _paintSolidStroke(canvas, rect, strokeStyle.color!, lineWidth); + } else if (strokeStyle.gradient != null) { + _paintGradientStroke(canvas, rect, strokeStyle.gradient!, lineWidth); + } + } + + void _paintSolidStroke( + Canvas canvas, + Rect rect, + Color color, + double lineWidth, + ) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..color = color + ..strokeWidth = lineWidth; + canvas.drawRect(rect, paint); + } + + void _paintGradientStroke( + Canvas canvas, + Rect rect, + Gradient gradient, + double lineWidth, + ) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..shader = gradient.createShader(rect) + ..strokeWidth = lineWidth; + canvas.drawRect(rect, paint); + } + + @override + bool shouldRepaint(covariant _RectanglePainter oldDelegate) { + return oldDelegate.fill != fill || + oldDelegate.stroke != stroke || + oldDelegate.strokeLineWidth != strokeLineWidth; + } +} diff --git a/lib/src/shapes/shape_style.dart b/lib/src/shapes/shape_style.dart index 03fb129..e1d584d 100644 --- a/lib/src/shapes/shape_style.dart +++ b/lib/src/shapes/shape_style.dart @@ -2,6 +2,8 @@ import 'package:flutter/painting.dart'; /// A color or pattern used for painting a shape. class ShapeStyle { + // Can't make color an optional positional parameter + // https://github.com/dart-lang/language/issues/1076 const ShapeStyle({ this.color, this.gradient, @@ -12,4 +14,15 @@ class ShapeStyle { /// A gradient used for painting a shape. final Gradient? gradient; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ShapeStyle && + other.color == color && + other.gradient == gradient; + } + + @override + int get hashCode => Object.hash(color, gradient); } diff --git a/test/shapes/goldens/rectangle_smoke-test.png b/test/shapes/goldens/rectangle_smoke-test.png new file mode 100644 index 0000000000000000000000000000000000000000..8ece611500f110c565bf2331af9bce0c9eb9cf75 GIT binary patch literal 12943 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 rz!xQk;n9QzPP<<--&+%%|+f( literal 0 HcmV?d00001 diff --git a/test/shapes/rectangle_golden_test.dart b/test/shapes/rectangle_golden_test.dart new file mode 100644 index 0000000..8a24aac --- /dev/null +++ b/test/shapes/rectangle_golden_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:swift_ui/swift_ui.dart'; + +void main() { + group("Shapes > Rectangle >", () { + testGoldens("smoke test", (widgetTester) async { + final builder = GoldenBuilder.grid(columns: 2, widthToHeightRatio: 1) + ..addScenario( + 'Solid fill', + const Frame( + width: 200, + height: 100, + child: Rectangle( + fill: ShapeStyle(color: Colors.blue), + ), + ), + ) + ..addScenario( + 'Gradient fill', + const Frame( + width: 200, + height: 100, + child: Rectangle( + fill: ShapeStyle( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], + ), + ), + ), + ), + ) + ..addScenario( + 'Solid stroke', + const Frame( + width: 200, + height: 100, + child: Rectangle( + stroke: ShapeStyle(color: Colors.blue), + ), + ), + ) + ..addScenario( + 'Gradient stroke', + const Frame( + width: 200, + height: 100, + child: Rectangle( + stroke: ShapeStyle( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], + ), + ), + ), + ), + ); + await widgetTester.pumpWidgetBuilder(builder.build()); + await screenMatchesGolden(widgetTester, 'rectangle_smoke-test'); + }); + }); +} diff --git a/test/shapes/rectangle_test.dart b/test/shapes/rectangle_test.dart new file mode 100644 index 0000000..fedc295 --- /dev/null +++ b/test/shapes/rectangle_test.dart @@ -0,0 +1,3 @@ +void main() { + // TODO: +} From 3bb3b91b1c2ef99eeabf3ea49302b5abb88d50e2 Mon Sep 17 00:00:00 2001 From: suragch Date: Sat, 8 Jun 2024 10:56:29 +0800 Subject: [PATCH 3/7] cleanup --- lib/src/shapes/shape_style.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/shapes/shape_style.dart b/lib/src/shapes/shape_style.dart index e1d584d..66d04d4 100644 --- a/lib/src/shapes/shape_style.dart +++ b/lib/src/shapes/shape_style.dart @@ -2,8 +2,6 @@ import 'package:flutter/painting.dart'; /// A color or pattern used for painting a shape. class ShapeStyle { - // Can't make color an optional positional parameter - // https://github.com/dart-lang/language/issues/1076 const ShapeStyle({ this.color, this.gradient, From 66535809f41ab076d27d4bfa795db12ade262b29 Mon Sep 17 00:00:00 2001 From: suragch Date: Tue, 11 Jun 2024 11:17:19 +0800 Subject: [PATCH 4/7] removed shape style --- example/lib/shapes/rectangle.dart | 20 ++--- lib/src/shapes/rectangle.dart | 103 +++++++++++++++---------- lib/src/shapes/shape_style.dart | 26 ------- lib/swift_ui.dart | 1 - test/shapes/rectangle_golden_test.dart | 16 ++-- 5 files changed, 74 insertions(+), 92 deletions(-) delete mode 100644 lib/src/shapes/shape_style.dart diff --git a/example/lib/shapes/rectangle.dart b/example/lib/shapes/rectangle.dart index 0f335f0..fb3ab8c 100644 --- a/example/lib/shapes/rectangle.dart +++ b/example/lib/shapes/rectangle.dart @@ -15,9 +15,7 @@ class RectangleDemo extends StatelessWidget { width: 200, height: 100, child: Rectangle( - fill: ShapeStyle( - color: Colors.blue, - ), + fillColor: Colors.blue, ), ), @@ -26,10 +24,8 @@ class RectangleDemo extends StatelessWidget { width: 200, height: 100, child: Rectangle( - fill: ShapeStyle( - gradient: LinearGradient( - colors: [Colors.yellow, Colors.orange], - ), + fillGradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], ), ), ), @@ -39,9 +35,7 @@ class RectangleDemo extends StatelessWidget { width: 200, height: 100, child: Rectangle( - stroke: ShapeStyle( - color: Colors.red, - ), + strokeColor: Colors.red, strokeLineWidth: 4.0, ), ), @@ -51,10 +45,8 @@ class RectangleDemo extends StatelessWidget { width: 200, height: 100, child: Rectangle( - stroke: ShapeStyle( - gradient: LinearGradient( - colors: [Colors.yellow, Colors.blue], - ), + strokeGradient: LinearGradient( + colors: [Colors.yellow, Colors.blue], ), strokeLineWidth: 14.0, ), diff --git a/lib/src/shapes/rectangle.dart b/lib/src/shapes/rectangle.dart index 5e600bf..14d9e00 100644 --- a/lib/src/shapes/rectangle.dart +++ b/lib/src/shapes/rectangle.dart @@ -1,23 +1,41 @@ import 'package:flutter/widgets.dart'; -import 'package:swift_ui/src/shapes/shape_style.dart'; /// A rectangular shape that takes the size of the parent class Rectangle extends StatelessWidget { const Rectangle({ super.key, - this.fill, - this.stroke, + this.fillColor, + this.fillGradient, + this.strokeColor, + this.strokeGradient, this.strokeLineWidth = 1.0, }); - /// A color or gradient used for painting the interior of the rectangle. - final ShapeStyle? fill; + /// A color used for painting the interior of the rectangle. + /// + /// This is an alternative to [fillGradient]. + final Color? fillColor; + + /// A gradient used for painting the interior of the rectangle. + /// + /// This is an alternative to [fillColor]. + final Gradient? fillGradient; - /// A color or gradient used for painting the outline of the rectangle. + /// A color used for painting the outline of the rectangle. /// /// The stroke line is drawn inside the frame of the rectangle, and its /// width is determined by [strokeLineWidth]. - final ShapeStyle? stroke; + /// + /// This is an alternative to [strokeGradient]. + final Color? strokeColor; + + /// A gradient used for painting the outline of the rectangle. + /// + /// The stroke line is drawn inside the frame of the rectangle, and its + /// width is determined by [strokeLineWidth]. + /// + /// This is an alternative to [strokeColor]. + final Gradient? strokeGradient; /// The width of the stroke used to paint the outline of the rectangle. /// @@ -29,8 +47,10 @@ class Rectangle extends StatelessWidget { return SizedBox.expand( child: CustomPaint( painter: _RectanglePainter( - fill: fill, - stroke: stroke, + fillColor: fillColor, + fillGradient: fillGradient, + strokeColor: strokeColor, + strokeGradient: strokeGradient, strokeLineWidth: strokeLineWidth, ), ), @@ -40,67 +60,66 @@ class Rectangle extends StatelessWidget { class _RectanglePainter extends CustomPainter { _RectanglePainter({ - required this.fill, - required this.stroke, + required this.fillColor, + required this.fillGradient, + required this.strokeColor, + required this.strokeGradient, required this.strokeLineWidth, }); - final ShapeStyle? fill; - final ShapeStyle? stroke; + final Color? fillColor; + final Gradient? fillGradient; + final Color? strokeColor; + final Gradient? strokeGradient; final double strokeLineWidth; @override void paint(Canvas canvas, Size size) { - _paintFill(canvas, size, fill); - _paintStroke(canvas, size, stroke, strokeLineWidth); + _paintFill(canvas, size, fillColor, fillGradient); + _paintStroke(canvas, size, strokeColor, strokeGradient, strokeLineWidth); } - void _paintFill(Canvas canvas, Size size, ShapeStyle? fillStyle) { - if (fillStyle == null) { - return; - } - if (fillStyle.color != null) { - _paintSolidFill(canvas, size, fillStyle); - } else { - _paintGradientFill(canvas, size, fillStyle); + void _paintFill(Canvas canvas, Size size, Color? color, Gradient? gradient) { + if (color != null) { + _paintSolidFill(canvas, size, color); + } else if (gradient != null) { + _paintGradientFill(canvas, size, gradient); } } - void _paintSolidFill(Canvas canvas, Size size, ShapeStyle fillStyle) { + void _paintSolidFill(Canvas canvas, Size size, Color color) { final rect = Rect.fromLTWH(0, 0, size.width, size.height); - final fillPaint = Paint() + final paint = Paint() ..style = PaintingStyle.fill - ..color = fillStyle.color ?? const Color(0x00000000); - canvas.drawRect(rect, fillPaint); + ..color = color; + canvas.drawRect(rect, paint); } - void _paintGradientFill(Canvas canvas, Size size, ShapeStyle fillStyle) { + void _paintGradientFill(Canvas canvas, Size size, Gradient gradient) { final rect = Rect.fromLTWH(0, 0, size.width, size.height); - final fillPaint = Paint() + final paint = Paint() ..style = PaintingStyle.fill - ..shader = fillStyle.gradient?.createShader(rect); - canvas.drawRect(rect, fillPaint); + ..shader = gradient.createShader(rect); + canvas.drawRect(rect, paint); } void _paintStroke( Canvas canvas, Size size, - ShapeStyle? strokeStyle, + Color? color, + Gradient? gradient, double lineWidth, ) { - if (strokeStyle == null) { - return; - } final rect = Rect.fromLTWH( lineWidth / 2, lineWidth / 2, size.width - lineWidth, size.height - lineWidth, ); - if (strokeStyle.color != null) { - _paintSolidStroke(canvas, rect, strokeStyle.color!, lineWidth); - } else if (strokeStyle.gradient != null) { - _paintGradientStroke(canvas, rect, strokeStyle.gradient!, lineWidth); + if (color != null) { + _paintSolidStroke(canvas, rect, color, lineWidth); + } else if (gradient != null) { + _paintGradientStroke(canvas, rect, gradient, lineWidth); } } @@ -132,8 +151,10 @@ class _RectanglePainter extends CustomPainter { @override bool shouldRepaint(covariant _RectanglePainter oldDelegate) { - return oldDelegate.fill != fill || - oldDelegate.stroke != stroke || + return oldDelegate.fillColor != fillColor || + oldDelegate.fillGradient != fillGradient || + oldDelegate.strokeColor != strokeColor || + oldDelegate.strokeGradient != strokeGradient || oldDelegate.strokeLineWidth != strokeLineWidth; } } diff --git a/lib/src/shapes/shape_style.dart b/lib/src/shapes/shape_style.dart deleted file mode 100644 index 66d04d4..0000000 --- a/lib/src/shapes/shape_style.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/painting.dart'; - -/// A color or pattern used for painting a shape. -class ShapeStyle { - const ShapeStyle({ - this.color, - this.gradient, - }); - - /// A solid color used for painting a shape. - final Color? color; - - /// A gradient used for painting a shape. - final Gradient? gradient; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is ShapeStyle && - other.color == color && - other.gradient == gradient; - } - - @override - int get hashCode => Object.hash(color, gradient); -} diff --git a/lib/swift_ui.dart b/lib/swift_ui.dart index 4a68bdb..b8d6f26 100644 --- a/lib/swift_ui.dart +++ b/lib/swift_ui.dart @@ -20,7 +20,6 @@ export 'src/layout_adjustments/frame.dart'; // Shapes export 'src/shapes/ellipse.dart'; export 'src/shapes/rectangle.dart'; -export 'src/shapes/shape_style.dart'; // Infrastructure export 'src/infrastructure/late_bound_build.dart'; diff --git a/test/shapes/rectangle_golden_test.dart b/test/shapes/rectangle_golden_test.dart index 8a24aac..4fd1ae9 100644 --- a/test/shapes/rectangle_golden_test.dart +++ b/test/shapes/rectangle_golden_test.dart @@ -13,7 +13,7 @@ void main() { width: 200, height: 100, child: Rectangle( - fill: ShapeStyle(color: Colors.blue), + fillColor: Colors.blue, ), ), ) @@ -23,10 +23,8 @@ void main() { width: 200, height: 100, child: Rectangle( - fill: ShapeStyle( - gradient: LinearGradient( - colors: [Colors.yellow, Colors.orange], - ), + fillGradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], ), ), ), @@ -37,7 +35,7 @@ void main() { width: 200, height: 100, child: Rectangle( - stroke: ShapeStyle(color: Colors.blue), + strokeColor: Colors.blue, ), ), ) @@ -47,10 +45,8 @@ void main() { width: 200, height: 100, child: Rectangle( - stroke: ShapeStyle( - gradient: LinearGradient( - colors: [Colors.yellow, Colors.orange], - ), + strokeGradient: LinearGradient( + colors: [Colors.yellow, Colors.orange], ), ), ), From 5844095432952d43830dd38f144139bc634d8654 Mon Sep 17 00:00:00 2001 From: suragch Date: Tue, 11 Jun 2024 13:42:41 +0800 Subject: [PATCH 5/7] resolve all PR review issues besides Alignment --- .gitignore | 3 +- lib/src/layout/hstack.dart | 3 +- lib/src/shapes/rectangle.dart | 77 ++++++++++++++------------------- test/shapes/rectangle_test.dart | 3 -- 4 files changed, 37 insertions(+), 49 deletions(-) delete mode 100644 test/shapes/rectangle_test.dart diff --git a/.gitignore b/.gitignore index 21c41dc..c18acde 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ migrate_working_dir/ build/ # Don't check in golden failure output. -**/failures/*.png \ No newline at end of file +test/**/failures/** +test_goldens/**/failures/** \ No newline at end of file diff --git a/lib/src/layout/hstack.dart b/lib/src/layout/hstack.dart index 6e26b08..60836d3 100644 --- a/lib/src/layout/hstack.dart +++ b/lib/src/layout/hstack.dart @@ -60,7 +60,8 @@ class HStack extends StatelessWidget { return CrossAxisAlignment.end; case VerticalAlignment.firstTextBaseline: case VerticalAlignment.lastTextBaseline: - throw Exception("HStack does not yet support this alignment: $alignment"); + throw Exception( + "HStack does not yet support this alignment: $alignment"); } } } diff --git a/lib/src/shapes/rectangle.dart b/lib/src/shapes/rectangle.dart index 14d9e00..c653ed7 100644 --- a/lib/src/shapes/rectangle.dart +++ b/lib/src/shapes/rectangle.dart @@ -75,77 +75,66 @@ class _RectanglePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - _paintFill(canvas, size, fillColor, fillGradient); - _paintStroke(canvas, size, strokeColor, strokeGradient, strokeLineWidth); + _paintFill(canvas, size); + _paintStroke(canvas, size); } - void _paintFill(Canvas canvas, Size size, Color? color, Gradient? gradient) { - if (color != null) { - _paintSolidFill(canvas, size, color); - } else if (gradient != null) { - _paintGradientFill(canvas, size, gradient); + void _paintFill(Canvas canvas, Size size) { + if (fillColor != null) { + _paintSolidFill(canvas, size); + } else if (fillGradient != null) { + _paintGradientFill(canvas, size); } } - void _paintSolidFill(Canvas canvas, Size size, Color color) { + void _paintSolidFill(Canvas canvas, Size size) { final rect = Rect.fromLTWH(0, 0, size.width, size.height); final paint = Paint() ..style = PaintingStyle.fill - ..color = color; + ..color = fillColor!; canvas.drawRect(rect, paint); } - void _paintGradientFill(Canvas canvas, Size size, Gradient gradient) { + void _paintGradientFill(Canvas canvas, Size size) { final rect = Rect.fromLTWH(0, 0, size.width, size.height); final paint = Paint() ..style = PaintingStyle.fill - ..shader = gradient.createShader(rect); + ..shader = fillGradient!.createShader(rect); canvas.drawRect(rect, paint); } - void _paintStroke( - Canvas canvas, - Size size, - Color? color, - Gradient? gradient, - double lineWidth, - ) { - final rect = Rect.fromLTWH( - lineWidth / 2, - lineWidth / 2, - size.width - lineWidth, - size.height - lineWidth, - ); - if (color != null) { - _paintSolidStroke(canvas, rect, color, lineWidth); - } else if (gradient != null) { - _paintGradientStroke(canvas, rect, gradient, lineWidth); + void _paintStroke(Canvas canvas, Size size) { + if (strokeColor != null) { + _paintSolidStroke(canvas, size); + } else if (strokeGradient != null) { + _paintGradientStroke(canvas, size); } } - void _paintSolidStroke( - Canvas canvas, - Rect rect, - Color color, - double lineWidth, - ) { + void _paintSolidStroke(Canvas canvas, Size size) { + final rect = _adjustRectForLineWidth(size); final paint = Paint() ..style = PaintingStyle.stroke - ..color = color - ..strokeWidth = lineWidth; + ..color = strokeColor! + ..strokeWidth = strokeLineWidth; canvas.drawRect(rect, paint); } - void _paintGradientStroke( - Canvas canvas, - Rect rect, - Gradient gradient, - double lineWidth, - ) { + Rect _adjustRectForLineWidth(Size size) { + return Rect.fromLTWH( + strokeLineWidth / 2, + strokeLineWidth / 2, + size.width - strokeLineWidth, + size.height - strokeLineWidth, + ); + } + + void _paintGradientStroke(Canvas canvas, Size size) { + final rect = _adjustRectForLineWidth(size); final paint = Paint() ..style = PaintingStyle.stroke - ..shader = gradient.createShader(rect) - ..strokeWidth = lineWidth; + ..shader = strokeGradient!.createShader(rect) + ..strokeWidth = strokeLineWidth; canvas.drawRect(rect, paint); } diff --git a/test/shapes/rectangle_test.dart b/test/shapes/rectangle_test.dart deleted file mode 100644 index fedc295..0000000 --- a/test/shapes/rectangle_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -void main() { - // TODO: -} From e337cf7f4776129937ea620e28a2b24f00542bea Mon Sep 17 00:00:00 2001 From: suragch Date: Wed, 12 Jun 2024 13:50:10 +0800 Subject: [PATCH 6/7] add Alignment enum --- lib/src/layout/hstack.dart | 19 +-- lib/src/layout/vstack.dart | 25 +-- lib/src/layout_adjustments/alignment.dart | 117 +++++++++++++ lib/src/layout_adjustments/frame.dart | 47 +++++- lib/swift_ui.dart | 1 + .../layout_adjustments/frame_golden_test.dart | 159 ++++++++++++++++++ .../goldens/frame_smoke-test.png | Bin 0 -> 4844 bytes 7 files changed, 322 insertions(+), 46 deletions(-) create mode 100644 lib/src/layout_adjustments/alignment.dart create mode 100644 test/layout_adjustments/frame_golden_test.dart create mode 100644 test/layout_adjustments/goldens/frame_smoke-test.png diff --git a/lib/src/layout/hstack.dart b/lib/src/layout/hstack.dart index 60836d3..f9dac28 100644 --- a/lib/src/layout/hstack.dart +++ b/lib/src/layout/hstack.dart @@ -1,5 +1,7 @@ import 'package:flutter/widgets.dart'; +import '../layout_adjustments/alignment.dart' hide Alignment; + /// A layout widget that displays its children horizontally, like a row. /// /// An `HStack` tightly wraps its [children]. Each child must have an intrinsic width @@ -65,20 +67,3 @@ class HStack extends StatelessWidget { } } } - -enum VerticalAlignment { - /// Alignment with the top of a bounding space. - top, - - /// Alignment that's equidistant from the top and bottom of a bounding space. - center, - - /// Alignment with the bottom of a bounding space. - bottom, - - /// Alignment with the baseline of the first line of text within another widget. - firstTextBaseline, - - /// Alignment with the baseline of the last line of text within another widget. - lastTextBaseline, -} diff --git a/lib/src/layout/vstack.dart b/lib/src/layout/vstack.dart index 1b3b5e3..6b191ea 100644 --- a/lib/src/layout/vstack.dart +++ b/lib/src/layout/vstack.dart @@ -1,5 +1,7 @@ import 'package:flutter/widgets.dart'; +import '../layout_adjustments/alignment.dart' hide Alignment; + /// A layout widget that displays its children vertically, like a column. /// /// A `VStack` tightly wraps its [children]. Each child must have an intrinsic height @@ -64,26 +66,3 @@ class VStack extends StatelessWidget { } } } - -enum HorizontalAlignment { - /// Alignment with the "leading" or "starting" edge of a bounding space. - /// - /// In a left-to-right layout context, the leading edge is the left edge. - leading, - - /// Alignment that's equidistant from the left and right edges of a bounding space. - center, - - /// Alignment with the "trailing" or "ending" edge of a bounding space. - /// - /// In a left-to-right layout context, the trailing edge is the right edge. - trailing, - - /// Alignment with the leading edge of a list separator - this alignment is only - /// relevant when a widget is displayed within a list that has dividers. - listRowSeparatorLeading, - - /// Alignment with the trailing edge of a list separator - this alignment is only - /// relevant when a widget is displayed within a list that has dividers. - listRowSeparatorTrailing, -} diff --git a/lib/src/layout_adjustments/alignment.dart b/lib/src/layout_adjustments/alignment.dart new file mode 100644 index 0000000..e69e5b1 --- /dev/null +++ b/lib/src/layout_adjustments/alignment.dart @@ -0,0 +1,117 @@ +enum HorizontalAlignment { + /// Alignment with the "leading" or "starting" edge of a bounding space. + /// + /// In a left-to-right layout context, the leading edge is the left edge. + leading, + + /// Alignment that's equidistant from the left and right edges of a bounding space. + center, + + /// Alignment with the "trailing" or "ending" edge of a bounding space. + /// + /// In a left-to-right layout context, the trailing edge is the right edge. + trailing, + + /// Alignment with the leading edge of a list separator - this alignment is only + /// relevant when a widget is displayed within a list that has dividers. + listRowSeparatorLeading, + + /// Alignment with the trailing edge of a list separator - this alignment is only + /// relevant when a widget is displayed within a list that has dividers. + listRowSeparatorTrailing, +} + +enum VerticalAlignment { + /// Alignment with the top of a bounding space. + top, + + /// Alignment that's equidistant from the top and bottom of a bounding space. + center, + + /// Alignment with the bottom of a bounding space. + bottom, + + /// Alignment with the baseline of the first line of text within another widget. + firstTextBaseline, + + /// Alignment with the baseline of the last line of text within another widget. + lastTextBaseline, +} + +enum Alignment { + /// Alignment that's equidistant from all edges of the bounding space. + center(horizontal: HorizontalAlignment.center, vertical: VerticalAlignment.center), + + /// Alignment with the "leading" (or "starting") edge of the bounding space. + /// + /// In a left-to-right layout context, this is the left-most edge. + leading(horizontal: HorizontalAlignment.leading, vertical: VerticalAlignment.center), + + /// Alignment with the "trailing" (or "ending") edge of the bounding space. + /// + /// In a left-to-right layout context, this is the right-most edge. + trailing(horizontal: HorizontalAlignment.trailing, vertical: VerticalAlignment.center), + + /// Alignment with the top edge of the bounding space. + top(horizontal: HorizontalAlignment.center, vertical: VerticalAlignment.top), + + /// Alignment with the bottom edge of the bounding space. + bottom(horizontal: HorizontalAlignment.center, vertical: VerticalAlignment.bottom), + + /// Alignment with the top and "leading" (or "starting") edges of the bounding space. + /// + /// In a left-to-right layout context, this is the top-left corner. + topLeading(horizontal: HorizontalAlignment.leading, vertical: VerticalAlignment.top), + + /// Alignment with the top and "trailing" (or "ending") edges of the bounding space. + /// + /// In a left-to-right layout context, this is the top-right corner. + topTrailing(horizontal: HorizontalAlignment.trailing, vertical: VerticalAlignment.top), + + /// Alignment with the bottom and "leading" (or "starting") edges of the bounding space. + /// + /// In a left-to-right layout context, this is the bottom-left corner. + bottomLeading(horizontal: HorizontalAlignment.leading, vertical: VerticalAlignment.bottom), + + /// Alignment with the bottom and "trailing" (or "ending") edges of the bounding space. + /// + /// In a left-to-right layout context, this is the bottom-right corner. + bottomTrailing(horizontal: HorizontalAlignment.trailing, vertical: VerticalAlignment.bottom), + + /// Alignment with the top-most baseline. + centerFirstTextBaseline(horizontal: HorizontalAlignment.center, vertical: VerticalAlignment.firstTextBaseline), + + /// Alignment with the bottom-most baseline. + centerLastTextBaseline(horizontal: HorizontalAlignment.center, vertical: VerticalAlignment.lastTextBaseline), + + /// Alignment with the "leading" (or "starting") edge of the top-most baseline. + /// + /// In a left-to-right layout context, this is the left edge of the first line of text. + leadingFirstTextBaseline(horizontal: HorizontalAlignment.leading, vertical: VerticalAlignment.firstTextBaseline), + + /// Alignment with the "trailing" (or "ending") edge of the top-most baseline. + /// + /// In a left-to-right layout context, this is the right edge of the first line of text. + trailingFirstTextBaseline(horizontal: HorizontalAlignment.trailing, vertical: VerticalAlignment.firstTextBaseline), + + /// Alignment with the "leading" (or "starting") edge of the bottom-most baseline. + /// + /// In a left-to-right layout context, this is the left edge of the last line of text. + leadingLastTextBaseline(horizontal: HorizontalAlignment.leading, vertical: VerticalAlignment.lastTextBaseline), + + /// Alignment with the "trailing" (or "ending") edge of the bottom-most baseline. + /// + /// In a left-to-right layout context, this is the right edge of the last line of text. + trailingLastTextBaseline(horizontal: HorizontalAlignment.trailing, vertical: VerticalAlignment.lastTextBaseline); + + const Alignment({ + required this.horizontal, + required this.vertical, + }); + + /// The horizontal alignment of the widget within its bounding space. + final HorizontalAlignment horizontal; + + /// The vertical alignment of the widget within its bounding space. + final VerticalAlignment vertical; +} diff --git a/lib/src/layout_adjustments/frame.dart b/lib/src/layout_adjustments/frame.dart index f523311..610fc33 100644 --- a/lib/src/layout_adjustments/frame.dart +++ b/lib/src/layout_adjustments/frame.dart @@ -1,12 +1,14 @@ import 'package:flutter/widgets.dart'; +import 'alignment.dart' as swift_ui; + /// A layout widget that aligns its child within a fixed-size rectangle. class Frame extends StatelessWidget { const Frame({ super.key, this.width, this.height, - this.alignment = Alignment.center, + this.alignment = swift_ui.Alignment.center, required this.child, }); @@ -22,8 +24,8 @@ class Frame extends StatelessWidget { /// The alignment of the child within the frame. /// - /// See [Alignment] for more info about the available alignments. - final Alignment alignment; + /// See [swift_ui.Alignment] for more info about the available alignments. + final swift_ui.Alignment alignment; /// The child widget to be displayed within the frame. final Widget child; @@ -34,12 +36,14 @@ class Frame extends StatelessWidget { return child; } + final textDirection = Directionality.of(context); + if (width == null) { // Use the intrinsic width of the child. return SizedBox( height: height, child: Align( - alignment: alignment, + alignment: _alignment(textDirection), child: IntrinsicWidth( child: child, ), @@ -52,7 +56,7 @@ class Frame extends StatelessWidget { return SizedBox( width: width, child: Align( - alignment: alignment, + alignment: _alignment(textDirection), child: IntrinsicHeight( child: child, ), @@ -64,9 +68,40 @@ class Frame extends StatelessWidget { width: width, height: height, child: Align( - alignment: alignment, + alignment: _alignment(textDirection), child: child, ), ); } + + AlignmentGeometry _alignment(TextDirection direction) { + final isLtr = direction == TextDirection.ltr; + switch (alignment) { + case swift_ui.Alignment.center: + return Alignment.center; + case swift_ui.Alignment.leading: + return (isLtr) ? Alignment.centerLeft : Alignment.centerRight; + case swift_ui.Alignment.trailing: + return (isLtr) ? Alignment.centerRight : Alignment.centerLeft; + case swift_ui.Alignment.top: + return Alignment.topCenter; + case swift_ui.Alignment.bottom: + return Alignment.bottomCenter; + case swift_ui.Alignment.topLeading: + return (isLtr) ? Alignment.topLeft : Alignment.topRight; + case swift_ui.Alignment.topTrailing: + return (isLtr) ? Alignment.topRight : Alignment.topLeft; + case swift_ui.Alignment.bottomLeading: + return (isLtr) ? Alignment.bottomLeft : Alignment.bottomRight; + case swift_ui.Alignment.bottomTrailing: + return (isLtr) ? Alignment.bottomRight : Alignment.bottomLeft; + case swift_ui.Alignment.centerFirstTextBaseline: + case swift_ui.Alignment.centerLastTextBaseline: + case swift_ui.Alignment.leadingFirstTextBaseline: + case swift_ui.Alignment.trailingFirstTextBaseline: + case swift_ui.Alignment.leadingLastTextBaseline: + case swift_ui.Alignment.trailingLastTextBaseline: + throw Exception("Frame does not yet support this alignment: $alignment"); + } + } } diff --git a/lib/swift_ui.dart b/lib/swift_ui.dart index b8d6f26..d1954d6 100644 --- a/lib/swift_ui.dart +++ b/lib/swift_ui.dart @@ -16,6 +16,7 @@ export 'src/layout/zstack.dart'; // Layout Adjustments export 'src/layout_adjustments/frame.dart'; +export 'src/layout_adjustments/alignment.dart'; // Shapes export 'src/shapes/ellipse.dart'; diff --git a/test/layout_adjustments/frame_golden_test.dart b/test/layout_adjustments/frame_golden_test.dart new file mode 100644 index 0000000..c0ba54b --- /dev/null +++ b/test/layout_adjustments/frame_golden_test.dart @@ -0,0 +1,159 @@ +import 'package:flutter/widgets.dart' hide Alignment; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:swift_ui/swift_ui.dart' hide Border; + +void main() { + group("Layout adjustments > Frame >", () { + testGoldens("smoke test", (widgetTester) async { + final builder = GoldenBuilder.grid(columns: 4, widthToHeightRatio: 1) + ..addScenario( + 'Center', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.center, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'Top', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.top, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'Bottom', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.bottom, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'Leading', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.leading, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'Trailing', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.trailing, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'topLeading', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.topLeading, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'topTrailing', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.topTrailing, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'bottomLeading', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.bottomLeading, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ) + ..addScenario( + 'bottomTrailing', + Container( + decoration: BoxDecoration(border: Border.all()), + child: Frame( + width: 100, + height: 100, + alignment: Alignment.bottomTrailing, + child: Container( + width: 10, + height: 10, + color: Colors.blue, + ), + ), + ), + ); + + await widgetTester.pumpWidgetBuilder(builder.build()); + await screenMatchesGolden(widgetTester, 'frame_smoke-test'); + }); + }); +} diff --git a/test/layout_adjustments/goldens/frame_smoke-test.png b/test/layout_adjustments/goldens/frame_smoke-test.png new file mode 100644 index 0000000000000000000000000000000000000000..6e76099b6b2acbd55c331bb38a58cc7a5edd71cf GIT binary patch literal 4844 zcmds5do-JA8h=BIUMZc?R;JO*nx$4dF59MuM2%9tplC}=*%FFzsSqlaprdCxomOXP z1tE-9({-DX6@)a{b~1|~BchQCrKIW-mysZ%`cO#VntzI@;NKJRn+ zU2?_M*>UZv&8q+aYn=|-y8)nX2!NWWmIibs3!fYbeZX*Tj&`851u+DDf#d9)JhY(K zMXi9(0MNbVWWV1djxyT4Kf=3X6X!`f4GP}Y_vI`4-Y$t?O!3hvCDOb?x!@- zdnrDGI<+Jdc+rkf^vC$5zls=N<(N*v8XCS#8L*Fv{@|YT`m6)YV%M#)#RLh4(=YMz zW#Iyi%0{V!4U}M!EEooW)=WpSHWUbDiy9n&&(<3PVEYAtemD!)0zhZGEdYrdVQK(8 z(9!|G&><1J_ajwr^?JtaovSdwl3whWYM%0CyW;UBbhi_?GMefO z7U-Ab!!yiSUZu6TYB_@0!9a=$A#Nh#jFQJ~y@61CRA1aE^QWOCt!z*9!qWmV&bhn= z2*pfyd~7TM(-voKCOTbNlxeZ~TBBHeMvA(VZXO@(A-gZdgwiAoCbNs6k#;W(Q!D4+ zW+=xM1=-ZCD4Bn<*UeFmD^4H~I42}ltyulmiFiLS0r>6hpQw6VBlnS_T5aURlao!m zv?I5HM5?agZpSU?i=JXaEL~2isHm8%WcO_DwFdw*@V@D*$wjp@M%#mY#lZn)yh?Bk-)r?DgO#Uk$BQ0WKN_KgKJjrM7?JlcmqxQiHcwAw==zmdqVC-VRC z@ArUcjf2MGFS=&(k7uM+Ar1DlI!AYRb0^YW%&is27B5XT6Zz0%4~b7z&S&Pc?6#!Y zC|&jw1R!gpVawc%|C<(neO3DIVf7wz=?<}swA$Wya$73VBd6k#VX z$p^OF`I3g<0ll{$!`EY6p5b2Fs;lW}0Qdp@gTgDNH8rT*NhdIv=At6Hkgwcd$qLR2 zrm8!eDq!qSa^(fp`-b%*f8b6L>*$M+HdL$)wF_lkcscz?$vgOPoI72k=T2bDc&;z5k2qF#i3ti`*ctiX3Dj(h)-v90 z1#gp~=yYu3*jTt!{TipgoE1}(g(~3DMn*@cbKMUa7i2hKrHTdm;H=f$$drr3+!orX zqoYG?amaXpgCv>0nCGxHGO-vV@^N9PC7-=r!7E|N#l%H89T!Umo2No8hlna;*h__d zjT12Q<5ZZldd#;-5To-7Bk;~y)&=L8$<*`yt9{89VIp*bU`5a4IR;`2dQ541loPnaXj%GI{hntw9OBc! z3)h&!)Mm;BqLk&(ZPcCGvdH!uKR^A=uQPpf1Edq`E;5Ou4#FN%8==J)9mlR+n3HPm zUC;NYDLsFtb;;y43|cD7g=t;=rfx39BwZ#e%xw-sDOZHDG^s+s-J6?szfW4r3%lbcGL8*!)d(d^_>^ z4p$|oIGKfH1$Idi^_qwP94WQ1?P46Is5|daz(Az+a+1^_-%b6^nr z+#MMklM(YK;=CD^;gH3#+HbviCkO5Vqz-fYXG?3`X|pN z52p&}S(1M^QxN`&g#ZBI60~UW);xSuRDt=!$-|xD=}M(03wbULj$@1(b) zUwA+}*TGfByCvCDa!Tpy|KPg171vGp=n*ZjrLV6#D*DIUi{YYT#~vHvpisp*D$B^B z`E3n_!Mc)Oau*|KdOZNWkM^nG-ar}=IBZY@k#5z$!BPGqq5a Date: Wed, 12 Jun 2024 14:53:57 +0800 Subject: [PATCH 7/7] alignment and rectangle code review adjustments --- .../alignment.dart | 33 ++++++ lib/src/layout/frame.dart | 76 +++++++++++++ lib/src/layout/hstack.dart | 5 +- lib/src/layout/vstack.dart | 2 +- lib/src/layout_adjustments/frame.dart | 107 ------------------ lib/src/shapes/rectangle.dart | 17 ++- lib/swift_ui.dart | 4 +- .../frame_golden_test.dart | 0 .../goldens/frame_smoke-test.png | Bin 9 files changed, 122 insertions(+), 122 deletions(-) rename lib/src/{layout_adjustments => layout}/alignment.dart (77%) create mode 100644 lib/src/layout/frame.dart delete mode 100644 lib/src/layout_adjustments/frame.dart rename test/{layout_adjustments => layout}/frame_golden_test.dart (100%) rename test/{layout_adjustments => layout}/goldens/frame_smoke-test.png (100%) diff --git a/lib/src/layout_adjustments/alignment.dart b/lib/src/layout/alignment.dart similarity index 77% rename from lib/src/layout_adjustments/alignment.dart rename to lib/src/layout/alignment.dart index e69e5b1..2a68a1c 100644 --- a/lib/src/layout_adjustments/alignment.dart +++ b/lib/src/layout/alignment.dart @@ -1,3 +1,5 @@ +import 'package:flutter/painting.dart' as painting; + enum HorizontalAlignment { /// Alignment with the "leading" or "starting" edge of a bounding space. /// @@ -114,4 +116,35 @@ enum Alignment { /// The vertical alignment of the widget within its bounding space. final VerticalAlignment vertical; + + painting.AlignmentGeometry toFlutter(painting.TextDirection direction) { + final isLtr = direction == painting.TextDirection.ltr; + switch (this) { + case Alignment.center: + return painting.Alignment.center; + case Alignment.leading: + return isLtr ? painting.Alignment.centerLeft : painting.Alignment.centerRight; + case Alignment.trailing: + return isLtr ? painting.Alignment.centerRight : painting.Alignment.centerLeft; + case Alignment.top: + return painting.Alignment.topCenter; + case Alignment.bottom: + return painting.Alignment.bottomCenter; + case Alignment.topLeading: + return isLtr ? painting.Alignment.topLeft : painting.Alignment.topRight; + case Alignment.topTrailing: + return isLtr ? painting.Alignment.topRight : painting.Alignment.topLeft; + case Alignment.bottomLeading: + return isLtr ? painting.Alignment.bottomLeft : painting.Alignment.bottomRight; + case Alignment.bottomTrailing: + return isLtr ? painting.Alignment.bottomRight : painting.Alignment.bottomLeft; + case Alignment.centerFirstTextBaseline: + case Alignment.centerLastTextBaseline: + case Alignment.leadingFirstTextBaseline: + case Alignment.trailingFirstTextBaseline: + case Alignment.leadingLastTextBaseline: + case Alignment.trailingLastTextBaseline: + throw Exception("swift_ui does not yet support this alignment: $this"); + } + } } diff --git a/lib/src/layout/frame.dart b/lib/src/layout/frame.dart new file mode 100644 index 0000000..ec360cb --- /dev/null +++ b/lib/src/layout/frame.dart @@ -0,0 +1,76 @@ +import 'package:flutter/widgets.dart' hide Alignment; + +import 'alignment.dart'; + +/// A layout widget that aligns its child within a fixed-size rectangle. +class Frame extends StatelessWidget { + const Frame({ + super.key, + this.width, + this.height, + this.alignment = Alignment.center, + required this.child, + }); + + /// The width of the frame. + /// + /// If `null`, the intrinsic width of the child is used. + final double? width; + + /// The height of the frame. + /// + /// If `null`, the intrinsic height of the child is used. + final double? height; + + /// The alignment of the child within the frame. + /// + /// See [Alignment] for more info about the available alignments. + final Alignment alignment; + + /// The child widget to be displayed within the frame. + final Widget child; + + @override + Widget build(BuildContext context) { + if (width == null && height == null) { + return child; + } + + final textDirection = Directionality.of(context); + + if (width == null) { + // Use the intrinsic width of the child. + return SizedBox( + height: height, + child: Align( + alignment: alignment.toFlutter(textDirection), + child: IntrinsicWidth( + child: child, + ), + ), + ); + } + + if (height == null) { + // Use the intrinsic height of the child. + return SizedBox( + width: width, + child: Align( + alignment: alignment.toFlutter(textDirection), + child: IntrinsicHeight( + child: child, + ), + ), + ); + } + + return SizedBox( + width: width, + height: height, + child: Align( + alignment: alignment.toFlutter(textDirection), + child: child, + ), + ); + } +} diff --git a/lib/src/layout/hstack.dart b/lib/src/layout/hstack.dart index f9dac28..0703996 100644 --- a/lib/src/layout/hstack.dart +++ b/lib/src/layout/hstack.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import '../layout_adjustments/alignment.dart' hide Alignment; +import 'alignment.dart' hide Alignment; /// A layout widget that displays its children horizontally, like a row. /// @@ -62,8 +62,7 @@ class HStack extends StatelessWidget { return CrossAxisAlignment.end; case VerticalAlignment.firstTextBaseline: case VerticalAlignment.lastTextBaseline: - throw Exception( - "HStack does not yet support this alignment: $alignment"); + throw Exception("HStack does not yet support this alignment: $alignment"); } } } diff --git a/lib/src/layout/vstack.dart b/lib/src/layout/vstack.dart index 6b191ea..7dde20c 100644 --- a/lib/src/layout/vstack.dart +++ b/lib/src/layout/vstack.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import '../layout_adjustments/alignment.dart' hide Alignment; +import 'alignment.dart' hide Alignment; /// A layout widget that displays its children vertically, like a column. /// diff --git a/lib/src/layout_adjustments/frame.dart b/lib/src/layout_adjustments/frame.dart deleted file mode 100644 index 610fc33..0000000 --- a/lib/src/layout_adjustments/frame.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'alignment.dart' as swift_ui; - -/// A layout widget that aligns its child within a fixed-size rectangle. -class Frame extends StatelessWidget { - const Frame({ - super.key, - this.width, - this.height, - this.alignment = swift_ui.Alignment.center, - required this.child, - }); - - /// The width of the frame. - /// - /// If `null`, the intrinsic width of the child is used. - final double? width; - - /// The height of the frame. - /// - /// If `null`, the intrinsic height of the child is used. - final double? height; - - /// The alignment of the child within the frame. - /// - /// See [swift_ui.Alignment] for more info about the available alignments. - final swift_ui.Alignment alignment; - - /// The child widget to be displayed within the frame. - final Widget child; - - @override - Widget build(BuildContext context) { - if (width == null && height == null) { - return child; - } - - final textDirection = Directionality.of(context); - - if (width == null) { - // Use the intrinsic width of the child. - return SizedBox( - height: height, - child: Align( - alignment: _alignment(textDirection), - child: IntrinsicWidth( - child: child, - ), - ), - ); - } - - if (height == null) { - // Use the intrinsic height of the child. - return SizedBox( - width: width, - child: Align( - alignment: _alignment(textDirection), - child: IntrinsicHeight( - child: child, - ), - ), - ); - } - - return SizedBox( - width: width, - height: height, - child: Align( - alignment: _alignment(textDirection), - child: child, - ), - ); - } - - AlignmentGeometry _alignment(TextDirection direction) { - final isLtr = direction == TextDirection.ltr; - switch (alignment) { - case swift_ui.Alignment.center: - return Alignment.center; - case swift_ui.Alignment.leading: - return (isLtr) ? Alignment.centerLeft : Alignment.centerRight; - case swift_ui.Alignment.trailing: - return (isLtr) ? Alignment.centerRight : Alignment.centerLeft; - case swift_ui.Alignment.top: - return Alignment.topCenter; - case swift_ui.Alignment.bottom: - return Alignment.bottomCenter; - case swift_ui.Alignment.topLeading: - return (isLtr) ? Alignment.topLeft : Alignment.topRight; - case swift_ui.Alignment.topTrailing: - return (isLtr) ? Alignment.topRight : Alignment.topLeft; - case swift_ui.Alignment.bottomLeading: - return (isLtr) ? Alignment.bottomLeft : Alignment.bottomRight; - case swift_ui.Alignment.bottomTrailing: - return (isLtr) ? Alignment.bottomRight : Alignment.bottomLeft; - case swift_ui.Alignment.centerFirstTextBaseline: - case swift_ui.Alignment.centerLastTextBaseline: - case swift_ui.Alignment.leadingFirstTextBaseline: - case swift_ui.Alignment.trailingFirstTextBaseline: - case swift_ui.Alignment.leadingLastTextBaseline: - case swift_ui.Alignment.trailingLastTextBaseline: - throw Exception("Frame does not yet support this alignment: $alignment"); - } - } -} diff --git a/lib/src/shapes/rectangle.dart b/lib/src/shapes/rectangle.dart index c653ed7..f66a6c6 100644 --- a/lib/src/shapes/rectangle.dart +++ b/lib/src/shapes/rectangle.dart @@ -44,15 +44,14 @@ class Rectangle extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox.expand( - child: CustomPaint( - painter: _RectanglePainter( - fillColor: fillColor, - fillGradient: fillGradient, - strokeColor: strokeColor, - strokeGradient: strokeGradient, - strokeLineWidth: strokeLineWidth, - ), + return CustomPaint( + size: Size.infinite, + painter: _RectanglePainter( + fillColor: fillColor, + fillGradient: fillGradient, + strokeColor: strokeColor, + strokeGradient: strokeGradient, + strokeLineWidth: strokeLineWidth, ), ); } diff --git a/lib/swift_ui.dart b/lib/swift_ui.dart index d1954d6..3ef7c55 100644 --- a/lib/swift_ui.dart +++ b/lib/swift_ui.dart @@ -15,8 +15,8 @@ export 'src/layout/vstack.dart'; export 'src/layout/zstack.dart'; // Layout Adjustments -export 'src/layout_adjustments/frame.dart'; -export 'src/layout_adjustments/alignment.dart'; +export 'src/layout/frame.dart'; +export 'src/layout/alignment.dart'; // Shapes export 'src/shapes/ellipse.dart'; diff --git a/test/layout_adjustments/frame_golden_test.dart b/test/layout/frame_golden_test.dart similarity index 100% rename from test/layout_adjustments/frame_golden_test.dart rename to test/layout/frame_golden_test.dart diff --git a/test/layout_adjustments/goldens/frame_smoke-test.png b/test/layout/goldens/frame_smoke-test.png similarity index 100% rename from test/layout_adjustments/goldens/frame_smoke-test.png rename to test/layout/goldens/frame_smoke-test.png