diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a15e3..55dceb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.24.0 +- Add state pattern: State Manipulator. + ## 0.23.14 - Replace web renderer html to canvakit (deploy_flutter_demo.dart). diff --git a/README.md b/README.md index 5c6a719..bca5237 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ It contains **Dart** examples for all classic **GoF** design patterns. - [ ] **Mediator** - [x] **Memento** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/memento/conceptual)] [[Memento Editor](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/memento/memento_editor)] - [x] **Observer** - [[Open-Close Editor Events](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/open_close_editor_events)] [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/app_observer)] [[Subscriber Flutter Widget](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/subscriber_flutter_widget)] - - [ ] **State** + - [x] **State** - [[State Manipulator](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/state/manipulator_state)] - [ ] **Template Method** - [X] **Visitor** [[Shape XML Exporter](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/visitor/shapes_exporter)] - [X] **Strategy** [[Reservation Cargo Spaces](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/strategy/reservation_cargo_spaces)] @@ -32,8 +32,8 @@ It contains **Dart** examples for all classic **GoF** design patterns. ## Requirements -The examples were written in **Dart 2.15**. -Some complex examples require **Flutter 2.12**. +The examples were written in **Dart 2.17**. +Some complex examples require **Flutter 3.0.0**. ## Contributor's Guide diff --git a/analysis_options.yaml b/analysis_options.yaml index dee8927..39e5146 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -15,10 +15,9 @@ include: package:lints/recommended.yaml # Uncomment the following section to specify additional rules. -# linter: -# rules: -# - camel_case_types - +linter: + rules: + library_private_types_in_public_api: false # analyzer: # exclude: # - path/to/excluded/files/** diff --git a/bin/deploy_flutter_demos.dart b/bin/deploy_flutter_demos.dart index 8721544..18b34db 100644 --- a/bin/deploy_flutter_demos.dart +++ b/bin/deploy_flutter_demos.dart @@ -20,9 +20,9 @@ void main() async { clear(); } -late final tmpDir = Directory.systemTemp.createTempSync(); -late final projectDir = thisPath(r'..\'); -late final webBuildDir = Directory(projectDir.uri.toFilePath() + r'build\web'); + final tmpDir = Directory.systemTemp.createTempSync(); + final projectDir = thisPath(r'..\'); + final webBuildDir = Directory(projectDir.uri.toFilePath() + r'build\web'); late final String originUrl; Future init() async { diff --git a/bin/main.dart b/bin/main.dart index 1180c2d..5200232 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -3,6 +3,7 @@ import '../patterns/abstract_factory/tool_panel_factory/main.dart'; import '../patterns/observer/subscriber_flutter_widget/main.dart'; import '../patterns/adapter/flutter_adapter/main.dart'; import '../patterns/memento/memento_editor/main.dart'; +import '../patterns/state/manipulator_state/main.dart'; void main() { runApp(MyApp()); @@ -13,13 +14,20 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Refactoring Guru: Flutter launcher', - theme: ThemeData(primarySwatch: Colors.pink), - initialRoute: '/abstract_factory/tool_panel_factory', + theme: ThemeData( + primarySwatch: Colors.pink, + iconTheme: IconThemeData( + size: 32, + color: Colors.white, + ), + ), + initialRoute: '/state/manipulator_state', routes: { '/observer/subscriber_flutter_widget': (_) => SubscriberFlutterApp(), '/adapter/flutter_adapter': (_) => FlutterAdapterApp(), '/memento/flutter_memento_editor': (_) => FlutterMementoEditorApp(), '/abstract_factory/tool_panel_factory': (_) => ToolPanelFactoryApp(), + '/state/manipulator_state': (_) => ManipulatorStateApp(), }, ); } diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart index 06343f5..c123fa8 100644 --- a/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; class Panel extends StatelessWidget { static const thicknessWidth = 64.0; - static const thicknessHeight = 48.0; + final double thicknessHeight; final Axis direction; final Widget child; @@ -11,6 +11,7 @@ class Panel extends StatelessWidget { Key? key, required this.direction, required this.child, + this.thicknessHeight = 48.0, }) : super(key: key); @override diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart index c0a633b..441154a 100644 --- a/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart @@ -10,7 +10,7 @@ class ToolButton extends StatelessWidget { const ToolButton({ Key? key, required this.onTap, - required this.active, + this.active = false, required this.icon, }) : super(key: key); diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart index 3371e09..7bc950d 100644 --- a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart @@ -15,7 +15,7 @@ class FieldLabel extends StatelessWidget { return Row( children: [ SizedBox(width: 10), - Text(text + ':'), + Text('$text:'), SizedBox(width: 10), child, SizedBox(width: 20), diff --git a/patterns/builder/cars/main.dart b/patterns/builder/cars/main.dart index a7a3bc3..e1c55c5 100644 --- a/patterns/builder/cars/main.dart +++ b/patterns/builder/cars/main.dart @@ -34,5 +34,5 @@ void main() { // RU: Директор может знать больше одного рецепта строительства director.constructSportsCar(manualBuilder); final carManual = manualBuilder.getResult(); - print("Car manual built:\n" + carManual.print()); + print("Car manual built:\n${carManual.print()}"); } diff --git a/patterns/command/text_editor/main.dart b/patterns/command/text_editor/main.dart index 5862594..2d1b217 100644 --- a/patterns/command/text_editor/main.dart +++ b/patterns/command/text_editor/main.dart @@ -32,5 +32,5 @@ void log({ }) { final addOrUndo = isUndo ? 'Undo_' : '[➕] '; final description = '$addOrUndo$command'; - print(description.padRight(72, '_') + '"$editorText"'); + print('${description.padRight(72, '_')}"$editorText"'); } diff --git a/patterns/composite/products_and_boxes/products/box.dart b/patterns/composite/products_and_boxes/products/box.dart index db2c88a..35e091b 100644 --- a/patterns/composite/products_and_boxes/products/box.dart +++ b/patterns/composite/products_and_boxes/products/box.dart @@ -9,8 +9,8 @@ class Box implements Product { @override String get content { final places = size > 1 ? "places: $size, " : ""; - final _price = size > 1 ? "price: $price\$" : "$price\$"; - return 'Box($places$_price)'; + final localPrice = size > 1 ? "price: $price\$" : "$price\$"; + return 'Box($places$localPrice)'; } @override diff --git a/patterns/state/manipulator_state/README.md b/patterns/state/manipulator_state/README.md new file mode 100644 index 0000000..93d1e38 --- /dev/null +++ b/patterns/state/manipulator_state/README.md @@ -0,0 +1,76 @@ +# State Pattern +State is a behavioral design pattern that lets an object alter its behavior when its internal state +changes. It appears as if the object changed its class. + +Tutorial: [here](https://refactoring.guru/design-patterns/state). + +### Online demo: +Click on the picture to see the [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/state/manipulator_state). + +[![image](https://user-images.githubusercontent.com/8049534/171070341-1decb58f-033b-4eb5-89d4-355aafa6b680.png)](https://refactoringguru.github.io/design-patterns-dart/#/state/manipulator_state) + +### Video +https://user-images.githubusercontent.com/8049534/171499203-1400c3ae-d5cd-4e48-a0b6-0252f4345d19.mp4 + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/171740942-659d3ec9-8355-4078-a7d6-b4a338b41187.png) + +## Client code: +### Change FreeState to MoveState: +```dart +class FreeState extends ManipulationState { + @override + void mouseDown(double x, double y) { + tryToSelectAndStartMovingShape(x, y); + } + + bool tryToSelectAndStartMovingShape(double x, double y) { + final selectedShape = context.shapes.findShapeByCoordinates(x, y); + + context.changeState( + MoveState( + startX: x, + startY: y, + selectedShape: selectedShape, + ), + ); + + return true; + } +} +``` + +### Change MoveState to ResizableState: +```dart +class MoveState extends SelectionState { + @override + void mouseMove(double x, double y) { + selectedShape.move(x, y); + context.update(); + } + + @override + void mouseUp() { + context.changeState( + selectedShape.createSelectionState(), + ); + } +} +``` + +### Each shape has its own state manipulator: +```dart +class RectangleShape extends BaseShape { + @override + SelectionState createSelectionState() { + return ResizableState(selectedShape: this); + } +} + +class CircleShape extends BaseShape { + @override + SelectionState createSelectionState() { + return InnerRadiusState(selectedShape: this); + } +} +``` diff --git a/patterns/state/manipulator_state/app/app.dart b/patterns/state/manipulator_state/app/app.dart new file mode 100644 index 0000000..2e754d1 --- /dev/null +++ b/patterns/state/manipulator_state/app/app.dart @@ -0,0 +1,15 @@ +import '../pattern/manipulator.dart'; +import 'shapes.dart'; +import 'tool.dart'; + +class App { + final Shapes shapes; + final Manipulator manipulator; + final List tools; + + App({ + required this.shapes, + required this.manipulator, + required this.tools, + }); +} diff --git a/patterns/state/manipulator_state/app/base_manipulation.dart b/patterns/state/manipulator_state/app/base_manipulation.dart new file mode 100644 index 0000000..85db133 --- /dev/null +++ b/patterns/state/manipulator_state/app/base_manipulation.dart @@ -0,0 +1,87 @@ +part of manipulator; + +class BaseManipulator implements Manipulator { + BaseManipulator({ + required this.shapes, + required ManipulationState initState, + required this.paintStyle, + }) : _state = initState { + _state._context = this; + } + + @override + ManipulationState get state => _state; + + @override + final Shapes shapes; + + @override + final onStateChange = Event(); + + @override + final onUpdate = Event(); + + @override + var cursor = MouseCursor.defer; + + @override + final PaintStyle paintStyle; + + @override + void changeState(ManipulationState newState) { + if (_state == newState) { + return; + } + + _state = newState; + _state._context = this; + _state.init(); + onStateChange._emit(); + } + + @override + void update() { + onUpdate._emit(); + } + + @override + void mouseMove(double x, double y) { + _state.mouseMove(x, y); + } + + @override + void mouseDown(double x, double y) { + _state.mouseDown(x, y); + } + + @override + void mouseUp() { + _state.mouseUp(); + } + + @override + void mouseDoubleClick(double x, double y) { + _state.mouseDoubleClick(x, y); + } + + @override + void keyDown(KeyEvent keyEvent) { + _state.keyDown(keyEvent); + } + + @override + void paint(Canvas canvas) { + _state.paint(canvas); + } + + @override + String toString() { + return _state.toString(); + } + + ManipulationState _state; +} + +class Event extends ChangeNotifier { + void _emit() => notifyListeners(); +} diff --git a/patterns/state/manipulator_state/app/shapes.dart b/patterns/state/manipulator_state/app/shapes.dart new file mode 100644 index 0000000..381b96c --- /dev/null +++ b/patterns/state/manipulator_state/app/shapes.dart @@ -0,0 +1,36 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../shapes/shape.dart'; + +class Shapes with IterableMixin { + final List _shapes; + + Shapes(List shapes) : _shapes = shapes; + + void add(Shape shape) { + _shapes.add(shape); + onChange._emit(); + } + + @override + Iterator get iterator => _shapes.iterator; + + final onChange = Event(); + + Shape? findShapeByCoordinates(x, y) { + for (final shape in _shapes.reversed) { + if (shape.rect.contains(Offset(x, y))) { + return shape; + } + } + + return null; + } +} + +class Event extends ChangeNotifier { + void _emit() => notifyListeners(); +} diff --git a/patterns/state/manipulator_state/app/tool.dart b/patterns/state/manipulator_state/app/tool.dart new file mode 100644 index 0000000..a3e7ca6 --- /dev/null +++ b/patterns/state/manipulator_state/app/tool.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +import '../pattern/manipulator.dart'; + +class Tool { + final Icon icon; + final ManipulationState state; + + Tool({ + required this.icon, + required this.state, + }); +} diff --git a/patterns/state/manipulator_state/main.dart b/patterns/state/manipulator_state/main.dart new file mode 100644 index 0000000..f235a2f --- /dev/null +++ b/patterns/state/manipulator_state/main.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import 'app/app.dart'; +import 'app/shapes.dart'; +import 'app/tool.dart'; +import 'pattern/manipulator.dart'; +import 'states/creations/circle_creation_state.dart'; +import 'states/creations/rectangle_creation_state.dart'; +import 'states/creations/text_creation_state.dart'; +import 'states/free_sate.dart'; +import 'states/_/paint_style.dart'; +import 'widgets/current_state.dart'; +import 'widgets/drawing_board.dart'; +import 'widgets/tool_bar.dart'; + +class ManipulatorStateApp extends StatefulWidget { + const ManipulatorStateApp({Key? key}) : super(key: key); + + @override + State createState() => _ManipulatorStateAppState(); +} + +class _ManipulatorStateAppState extends State { + late final App app; + + @override + void initState() { + final shapes = Shapes([]); + app = App( + shapes: shapes, + manipulator: BaseManipulator( + initState: FreeState(), + shapes: shapes, + paintStyle: PaintStyle(Colors.pink), + ), + tools: [ + Tool( + icon: Icon(MdiIcons.cursorDefaultOutline), + state: FreeState(), + ), + Tool( + icon: Icon(MdiIcons.rectangleOutline), + state: RectangleCreationState(), + ), + Tool( + icon: Icon(MdiIcons.circleOutline), + state: CircleCreationState(), + ), + Tool( + icon: Icon(MdiIcons.formatTextVariant), + state: TextCreationState(), + ), + ], + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SizedBox.expand( + child: Stack( + children: [ + DrawingBoard(app: app), + ToolBar(app: app), + CurrentState(manipulator: app.manipulator), + ], + ), + ), + ); + } +} diff --git a/patterns/state/manipulator_state/pattern/manipulation_state.dart b/patterns/state/manipulator_state/pattern/manipulation_state.dart new file mode 100644 index 0000000..49221a2 --- /dev/null +++ b/patterns/state/manipulator_state/pattern/manipulation_state.dart @@ -0,0 +1,21 @@ +part of manipulator; + +class ManipulationState { + Manipulator get context => _context; + + void init() {} + + void mouseMove(double x, double y) {} + + void mouseDown(double x, double y) {} + + void mouseUp() {} + + void mouseDoubleClick(double x, double y) {} + + void keyDown(KeyEvent keyEvent) {} + + void paint(Canvas canvas) {} + + late Manipulator _context; +} diff --git a/patterns/state/manipulator_state/pattern/manipulator.dart b/patterns/state/manipulator_state/pattern/manipulator.dart new file mode 100644 index 0000000..b7df969 --- /dev/null +++ b/patterns/state/manipulator_state/pattern/manipulator.dart @@ -0,0 +1,40 @@ +library manipulator; + +import 'package:flutter/material.dart'; +import '../states/_/paint_style.dart'; +import '../app/shapes.dart'; + +part 'manipulation_state.dart'; +part '../app/base_manipulation.dart'; + +abstract class Manipulator { + ManipulationState get state; + + Shapes get shapes; + + MouseCursor get cursor; + + set cursor(MouseCursor cursor); + + PaintStyle get paintStyle; + + Event get onStateChange; + + Event get onUpdate; + + void changeState(ManipulationState newState); + + void update(); + + void mouseMove(double x, double y); + + void mouseDown(double x, double y); + + void mouseUp(); + + void mouseDoubleClick(double x, double y); + + void keyDown(KeyEvent keyEvent); + + void paint(Canvas canvas); +} diff --git a/patterns/state/manipulator_state/shapes/base_shape.dart b/patterns/state/manipulator_state/shapes/base_shape.dart new file mode 100644 index 0000000..c14eae8 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/base_shape.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'shape.dart'; + +abstract class BaseShape implements Shape { + BaseShape(double x, double y, double width, double height) + : _x = x, + _y = y, + _width = width, + _height = height; + + @override + double get x => _x; + + @override + double get y => _y; + + @override + double get height => _height; + + @override + double get width => _width; + + @override + Rect get rect => Rect.fromLTWH( + _width < 0 ? x + _width : x, + _height < 0 ? y + _height : y, + _width.abs(), + _height.abs(), + ); + + @override + void move(double x, double y) { + _x = x; + _y = y; + } + + @override + void resize(double width, double height) { + _width = width; + _height = height; + } + + double _x; + double _y; + double _width; + double _height; +} diff --git a/patterns/state/manipulator_state/shapes/circle_shape.dart b/patterns/state/manipulator_state/shapes/circle_shape.dart new file mode 100644 index 0000000..6efb1a5 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/circle_shape.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/inner_radius_state.dart'; +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; + +class CircleShape extends BaseShape { + CircleShape( + super.x, + super.y, + super.width, + super.height, + double innerRadius, + ) { + this.innerRadius = innerRadius; + } + + double get innerRadius => + _turnOnInnerRadius && _drawInnerRadius ? _innerRadius : width / 2; + + set innerRadius(double newValue) { + if (newValue > width / 2) { + _turnOnInnerRadius = false; + _innerRadius = newValue; + _buildPath(); + return; + } else if (newValue < 1) { + newValue = 1; + } + + _drawInnerRadius = true; + _turnOnInnerRadius = _drawInnerRadius; + _innerRadius = newValue; + _buildPath(); + } + + @override + void resize(double width, double height) { + super.resize(width, height); + _drawInnerRadius = width > _innerRadius * 2; + _buildPath(); + } + + @override + void move(double x, double y) { + super.move(x, y); + _buildPath(); + } + + @override + SelectionState createSelectionState() { + return InnerRadiusState(selectedShape: this); + } + + @override + void paint(Canvas canvas) { + canvas.drawPath( + _path, + Paint()..color = Colors.white, + ); + } + + void _buildPath() { + _path = Path() + ..fillType = PathFillType.evenOdd + ..addOval(rect); + + if (_drawInnerRadius) { + final fixHeight = height / width; + final doubleRadius = innerRadius * 2; + _path.addOval( + Rect.fromLTWH( + x + innerRadius, + y + innerRadius * fixHeight, + width - doubleRadius, + height - doubleRadius * fixHeight, + ), + ); + } + } + + late double _innerRadius; + late Path _path; + bool _drawInnerRadius = true; + bool _turnOnInnerRadius = true; +} diff --git a/patterns/state/manipulator_state/shapes/marker_shape.dart b/patterns/state/manipulator_state/shapes/marker_shape.dart new file mode 100644 index 0000000..aef7acf --- /dev/null +++ b/patterns/state/manipulator_state/shapes/marker_shape.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; +import 'shape.dart'; + +class MarkerShape extends BaseShape { + MarkerShape(double size) : super(0, 0, size, -1); + + @override + Rect get rect => Rect.fromLTWH(x - width, y - width, width * 2, width * 2); + + @override + void paint(Canvas canvas) => throw UnimplementedError(); + + @override + SelectionState createSelectionState() => throw UnimplementedError(); +} diff --git a/patterns/state/manipulator_state/shapes/rectangle_shape.dart b/patterns/state/manipulator_state/shapes/rectangle_shape.dart new file mode 100644 index 0000000..c6eeedd --- /dev/null +++ b/patterns/state/manipulator_state/shapes/rectangle_shape.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/resizable_state.dart'; +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; + +class RectangleShape extends BaseShape { + RectangleShape(super.x, super.y, super.width, super.height); + + @override + SelectionState createSelectionState() { + return ResizableState(selectedShape: this); + } + + @override + void paint(Canvas canvas) { + canvas.drawRect( + rect, + Paint()..color = Colors.white, + ); + } +} diff --git a/patterns/state/manipulator_state/shapes/shape.dart b/patterns/state/manipulator_state/shapes/shape.dart new file mode 100644 index 0000000..4484439 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/shape.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import '../states/selections/selection_state.dart'; + +abstract class Shape { + double get x; + + double get y; + + double get width; + + double get height; + + Rect get rect; + + void move(double x, double y); + + void resize(double width, double height); + + void paint(Canvas canvas); + + SelectionState createSelectionState(); +} diff --git a/patterns/state/manipulator_state/shapes/text_shape.dart b/patterns/state/manipulator_state/shapes/text_shape.dart new file mode 100644 index 0000000..9c8d51f --- /dev/null +++ b/patterns/state/manipulator_state/shapes/text_shape.dart @@ -0,0 +1,74 @@ +import 'dart:ui'; + +import '../states/selections/selection_state.dart'; +import '../states/selections/text_resize_state.dart'; +import 'base_shape.dart'; + +class TextShape extends BaseShape { + TextShape(double x, double y, double height) : super(x, y, 0, height) { + _buildParagraph(height); + } + + String get text => _text; + + set text(String newText) { + _text = newText; + _buildParagraph(_textHeight); + } + + double get userHeight => _textHeight; + + Paragraph get paragraph => _paragraph; + + @override + Rect get rect { + final fixY = height - _textHeight; + return Rect.fromLTWH(x, y + fixY, width, height - fixY); + } + + @override + void paint(Canvas can) { + can.drawParagraph(_paragraph, Offset(x, y)); + } + + @override + SelectionState createSelectionState() { + return TextResizeState(selectedShape: this); + } + + @override + void resize(double _, double newHeight) { + if (newHeight < 2) { + newHeight = 2; + } + + final oldWidth = width; + _buildParagraph(newHeight); + final centerX = x - (width - oldWidth) / 2; + move(centerX, y); + } + + void _buildParagraph(double newHeight) { + _textHeight = newHeight; + + final style = ParagraphStyle( + textDirection: TextDirection.ltr, + ); + final tStyle = TextStyle( + fontFamily: 'Arial', + color: Color(0xffffffff), + fontSize: newHeight, + ); + _paragraph = (ParagraphBuilder(style) + ..pushStyle(tStyle) + ..addText(_text)) + .build(); + _paragraph.layout(ParagraphConstraints(width: double.infinity)); + + super.resize(_paragraph.maxIntrinsicWidth, _paragraph.height); + } + + late Paragraph _paragraph; + String _text = 'Text'; + late double _textHeight; +} diff --git a/patterns/state/manipulator_state/states/_/creation_state.dart b/patterns/state/manipulator_state/states/_/creation_state.dart new file mode 100644 index 0000000..3485c86 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/creation_state.dart @@ -0,0 +1,75 @@ +import 'dart:ui'; + +import '../../pattern/manipulator.dart'; +import '../../shapes/shape.dart'; + +abstract class CreationState extends ManipulationState { + Shape createShape(double x, double y); + + @override + void mouseDown(double x, double y) { + _startCreatingShape(x, y); + } + + @override + void mouseMove(double x, double y) { + if (_isCreatingNotStart) { + return; + } + + _resizeNewShape(x, y); + } + + @override + void mouseUp() { + if (_isCreatingNotStart) { + return; + } + + _repositionNewShape(); + context.shapes.add(_newShape!); + _finishCreatingShape(); + } + + @override + void paint(Canvas canvas) { + _newShape?.paint(canvas); + } + + bool get _isCreatingNotStart => _newShape == null; + + void _startCreatingShape(double x, double y) { + _startX = x; + _startY = y; + _newShape = createShape(x, y); + } + + void _resizeNewShape(double x, double y) { + _isDragged = true; + _newShape!.resize(x - _startX, y - _startY); + context.update(); + } + + void _repositionNewShape() { + if (!_isDragged) { + _newShape!.resize(100, 100); + final rect = _newShape!.rect; + _newShape!.move( + _startX - rect.width / 2, + _startY - rect.height / 2, + ); + } + } + + void _finishCreatingShape() { + final selectedShapeState = _newShape!.createSelectionState(); + context.changeState(selectedShapeState); + _isDragged = false; + _newShape = null; + } + + var _startX = 0.0; + var _startY = 0.0; + Shape? _newShape; + var _isDragged = false; +} diff --git a/patterns/state/manipulator_state/states/_/marker.dart b/patterns/state/manipulator_state/states/_/marker.dart new file mode 100644 index 0000000..ea9a188 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/marker.dart @@ -0,0 +1,35 @@ +import 'package:flutter/rendering.dart'; + +import '../../shapes/marker_shape.dart'; +import '../../shapes/shape.dart'; +import 'sub_states/child_state.dart'; +import 'sub_states/parent_state.dart'; + +abstract class Marker extends ChildState { + Marker({ + required ParentState parentState, + }) : super( + parentState: parentState, + markerShape: MarkerShape(5), + ); + + @override + MouseCursor get hoverCursor { + final rect = parentState.selectedShape.rect; + final corner = Offset(markerShape.x, markerShape.y); + + if (corner == rect.topLeft) { + return SystemMouseCursors.resizeUpLeft; + } else if (corner == rect.topRight) { + return SystemMouseCursors.resizeUpRight; + } else if (corner == rect.bottomLeft) { + return SystemMouseCursors.resizeDownLeft; + } else if (corner == rect.bottomRight) { + return SystemMouseCursors.resizeDownRight; + } + + return SystemMouseCursors.move; + } + + T get selectedShape => parentState.selectedShape; +} diff --git a/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart b/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart new file mode 100644 index 0000000..6e5679f --- /dev/null +++ b/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import '../../../pattern/manipulator.dart'; +import '../../../shapes/shape.dart'; + +mixin HoverShapeMixin implements ManipulationState { + Shape? findShapeByCoordinates(double x, double y) { + return context.shapes.findShapeByCoordinates(x, y); + } + + Shape? get hoverShape => _hoverShape; + + bool get isHover => _hoverShape != null; + + @override + void mouseMove(double x, double y) { + final newHover = findShapeByCoordinates(x, y); + + if (newHover == _hoverShape) { + return; + } + + _hoverShape = newHover; + + if (newHover == null) { + onMouseLeave(); + } else { + onHover(); + } + + context.update(); + } + + void onHover() {} + + void onMouseLeave() {} + + @override + void paint(Canvas canvas) { + if (_hoverShape == null) { + return; + } + + context.paintStyle.paintHover(canvas, _hoverShape!); + } + + Shape? _hoverShape; +} diff --git a/patterns/state/manipulator_state/states/_/paint_style.dart b/patterns/state/manipulator_state/states/_/paint_style.dart new file mode 100644 index 0000000..a26d70e --- /dev/null +++ b/patterns/state/manipulator_state/states/_/paint_style.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/circle_shape.dart'; +import '../../shapes/shape.dart'; +import '../../shapes/text_shape.dart'; +import '../selections/text/text_cursor.dart'; + +class PaintStyle { + PaintStyle(this.color) + : _selectStroke = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0, + _markerStroke = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + _markerFill = Paint() + ..style = PaintingStyle.fill + ..color = Colors.black; + + final Color color; + + void paintHover(Canvas canvas, Shape shape) { + canvas.drawRect( + shape.rect.deflate(1), + _selectStroke, + ); + } + + void paintSelection(Canvas canvas, Shape shape) { + paintHover(canvas, shape); + } + + void paintMarker(Canvas canvas, Shape markerShape) { + final point = Offset(markerShape.x, markerShape.y); + canvas.drawCircle( + point, + markerShape.width, + _markerFill, + ); + canvas.drawCircle( + point, + markerShape.width, + _markerStroke, + ); + } + + void paintRadiusLine(CircleShape selectedShape, Canvas canvas) { + canvas.save(); + canvas.translate(selectedShape.x, selectedShape.y); + final x = selectedShape.width - selectedShape.innerRadius; + final y = selectedShape.height / 2; + canvas.drawLine( + Offset(x, y), + Offset(selectedShape.width, y), + Paint() + ..color = color + ..strokeWidth = 1.5, + ); + + canvas.restore(); + } + + void paintSelectedText(TextShape selectedShape, Canvas canvas) { + canvas.drawRect( + selectedShape.rect, + Paint()..color = color.withOpacity(0.3), + ); + } + + void paintTextCursor( + TextCursor cursor, + TextShape selectedShape, + Canvas canvas, + ) { + canvas.drawLine( + Offset( + cursor.xCoordinate, + selectedShape.y + (selectedShape.height - selectedShape.userHeight) + 2, + ), + Offset( + cursor.xCoordinate, + selectedShape.y + (selectedShape.height) - 2, + ), + Paint() + ..strokeWidth = 2.2 + ..color = Colors.white, + ); + } + + final Paint _selectStroke; + final Paint _markerStroke; + final Paint _markerFill; +} diff --git a/patterns/state/manipulator_state/states/_/sub_states/child_state.dart b/patterns/state/manipulator_state/states/_/sub_states/child_state.dart new file mode 100644 index 0000000..b1a1747 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/sub_states/child_state.dart @@ -0,0 +1,88 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; + +import '../../../shapes/shape.dart'; +import '../../../pattern/manipulator.dart'; +import '../../_/mixins/hover_shape_mixin.dart'; +import 'parent_state.dart'; + +abstract class ChildState extends ManipulationState + with HoverShapeMixin { + final ParentState parentState; + final Shape markerShape; + + ChildState({ + required this.parentState, + required this.markerShape, + }) { + updatePosition(); + } + + void updatePosition(); + + void mouseDragAction(double x, double y); + + MouseCursor get hoverCursor; + + @override + void onHover() { + context.cursor = hoverCursor; + } + + @override + void onMouseLeave() { + context.cursor = SystemMouseCursors.basic; + } + + @override + void mouseDown(double x, double y) { + if (isHover) { + _isDown = true; + context.changeState(this); + } + } + + @override + void mouseMove(double x, double y) { + super.mouseMove(x, y); + if (_isDown) { + mouseDragAction(x, y); + parentState.updateKids(); + context.cursor = hoverCursor; + context.update(); + } + } + + @override + void mouseUp() { + if (!_isDown) { + return; + } + + context.changeState(parentState); + _isDown = false; + + if (!isHover) { + context.cursor = SystemMouseCursors.basic; + context.update(); + } + } + + @override + void paint(Canvas canvas) { + parentState.paint(canvas); + } + + @override + Manipulator get context => parentState.context; + + @override + Shape? findShapeByCoordinates(double x, double y) { + return markerShape.rect.contains(Offset(x, y)) ? markerShape : null; + } + + bool get isDown => _isDown; + + bool _isDown = false; +} diff --git a/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart b/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart new file mode 100644 index 0000000..ba0638f --- /dev/null +++ b/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; + +import '../../selections/selection_state.dart'; +import 'child_state.dart'; +import '../../../shapes/shape.dart'; + +abstract class ParentState extends SelectionState { + ParentState({ + required super.selectedShape, + }); + + void addChildren(List markers) { + _children.addAll(markers); + } + + void updateKids() { + for (final child in _children) { + child.updatePosition(); + } + } + + @override + void mouseDown(double x, double y) { + for (final child in _children) { + child.mouseDown(x, y); + if (context.state == child) { + return; + } + } + + super.mouseDown(x, y); + } + + @override + void mouseMove(double x, double y) { + super.mouseMove(x, y); + + for (final child in _children) { + child.mouseMove(x, y); + if (child.isHover) { + return; + } + } + } + + @override + void paint(Canvas canvas) { + super.paint(canvas); + + for (final child in _children) { + context.paintStyle.paintMarker(canvas, child.markerShape); + } + } + + final _children = []; +} diff --git a/patterns/state/manipulator_state/states/creations/circle_creation_state.dart b/patterns/state/manipulator_state/states/creations/circle_creation_state.dart new file mode 100644 index 0000000..dabf811 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/circle_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/circle_shape.dart'; +import '../../shapes/shape.dart'; +import '../_/creation_state.dart'; + +class CircleCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return CircleShape(x, y, 100, 100, 25); + } + + @override + String toString() { + return 'Circle Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart b/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart new file mode 100644 index 0000000..4a61891 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/rectangle_shape.dart'; +import '../../shapes/shape.dart'; +import '../_/creation_state.dart'; + +class RectangleCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return RectangleShape(x, y, 0, 0); + } + + @override + String toString() { + return 'Rectangle Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/creations/text_creation_state.dart b/patterns/state/manipulator_state/states/creations/text_creation_state.dart new file mode 100644 index 0000000..4425a84 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/text_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/shape.dart'; +import '../../shapes/text_shape.dart'; +import '../_/creation_state.dart'; + +class TextCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return TextShape(x, y, 2); + } + + @override + String toString() { + return 'Text Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/free_sate.dart b/patterns/state/manipulator_state/states/free_sate.dart new file mode 100644 index 0000000..e001070 --- /dev/null +++ b/patterns/state/manipulator_state/states/free_sate.dart @@ -0,0 +1,33 @@ +import '../pattern/manipulator.dart'; +import '_/mixins/hover_shape_mixin.dart'; +import 'selections/move_state.dart'; + +class FreeState extends ManipulationState with HoverShapeMixin { + @override + void mouseDown(double x, double y) { + tryToSelectAndStartMovingShape(x, y); + } + + bool tryToSelectAndStartMovingShape(double x, double y) { + final selectedShape = context.shapes.findShapeByCoordinates(x, y); + + if (selectedShape == null) { + return false; + } + + context.changeState( + MoveState( + startX: x, + startY: y, + selectedShape: selectedShape, + ), + ); + + return true; + } + + @override + String toString() { + return 'Free State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart b/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart new file mode 100644 index 0000000..ae60e1b --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart @@ -0,0 +1,32 @@ +import 'package:flutter/src/services/mouse_cursor.dart'; + +import '../../../shapes/circle_shape.dart'; +import '../../_/marker.dart'; + +class InnerRadiusMarkerState extends Marker { + InnerRadiusMarkerState({required super.parentState}); + + @override + void mouseDragAction(double x, double y) { + selectedShape.innerRadius = selectedShape.rect.right - x; + } + + @override + void updatePosition() { + final y = selectedShape.y + selectedShape.height / 2; + final x = selectedShape.x + selectedShape.width; + + markerShape.move( + x - selectedShape.innerRadius, + y, + ); + } + + @override + MouseCursor get hoverCursor => SystemMouseCursors.resizeLeftRight; + + @override + String toString() { + return '${parentState.toString()} + Inner Radius Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/inner_radius_state.dart b/patterns/state/manipulator_state/states/selections/inner_radius_state.dart new file mode 100644 index 0000000..898b5b5 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/inner_radius_state.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/circle_shape.dart'; +import 'inner_radius_markers/inner_radius_marker_state.dart'; +import 'resizable_state.dart'; + +class InnerRadiusState extends ResizableState { + InnerRadiusState({required super.selectedShape}) { + addChildren([ + InnerRadiusMarkerState(parentState: this), + ]); + } + + @override + void paint(Canvas canvas) { + context.paintStyle.paintRadiusLine(selectedShape, canvas); + super.paint(canvas); + } + + @override + String toString() { + return '${super.toString()} + Inner Radius State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/move_state.dart b/patterns/state/manipulator_state/states/selections/move_state.dart new file mode 100644 index 0000000..8f0773c --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/move_state.dart @@ -0,0 +1,31 @@ +import 'selection_state.dart'; + +class MoveState extends SelectionState { + final double startX; + final double startY; + + MoveState({ + required double startX, + required double startY, + required super.selectedShape, + }) : startX = startX - selectedShape.x, + startY = startY - selectedShape.y; + + @override + void mouseMove(double x, double y) { + selectedShape.move(x - startX, y - startY); + context.update(); + } + + @override + void mouseUp() { + context.changeState( + selectedShape.createSelectionState(), + ); + } + + @override + String toString() { + return '${super.toString()} + Moving State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart new file mode 100644 index 0000000..9d2a1f8 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class BottomLeftMarkerState extends Marker { + BottomLeftMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final newX = selectedShape.width + selectedShape.x - x; + final newY = y - selectedShape.y; + + selectedShape + ..resize(newX, newY) + ..move(x, selectedShape.y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x, + selectedShape.y + selectedShape.height, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Bottom Left Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart new file mode 100644 index 0000000..5db6e10 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart @@ -0,0 +1,27 @@ +import '../../_/marker.dart'; + +class BottomRightMarkerState extends Marker { + BottomRightMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + selectedShape.resize( + x - selectedShape.x, + y - selectedShape.y, + ); + } + + @override + void updatePosition() { + final width = selectedShape.x + selectedShape.width; + final height = selectedShape.y + selectedShape.height; + markerShape.move(width, height); + } + + @override + String toString() { + return '${parentState.toString()} + Bottom Right Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart new file mode 100644 index 0000000..74bd92d --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class TopLeftMarkerState extends Marker { + TopLeftMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final newWidth = selectedShape.width + selectedShape.x - x; + final newHeight = selectedShape.height + selectedShape.y - y; + + selectedShape + ..resize(newWidth, newHeight) + ..move(x, y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x, + selectedShape.y, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Top Left Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart new file mode 100644 index 0000000..f03f484 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class TopRightMarkerState extends Marker { + TopRightMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final width = x - selectedShape.x; + final height = selectedShape.height + selectedShape.y - y; + + selectedShape + ..resize(width, height) + ..move(selectedShape.x, y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x + selectedShape.width, + selectedShape.y, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Top Right Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_state.dart b/patterns/state/manipulator_state/states/selections/resizable_state.dart new file mode 100644 index 0000000..92236b0 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_state.dart @@ -0,0 +1,22 @@ +import 'resizable_markers/bottom_left_marker_state.dart'; +import 'resizable_markers/bottom_right_marker_state.dart'; +import 'resizable_markers/top_left_marker_state.dart'; +import 'resizable_markers/top_right_marker_state.dart'; +import '../_/sub_states/parent_state.dart'; +import '../../shapes/shape.dart'; + +class ResizableState extends ParentState { + ResizableState({required super.selectedShape}) { + addChildren([ + TopLeftMarkerState(parentState: this), + TopRightMarkerState(parentState: this), + BottomRightMarkerState(parentState: this), + BottomLeftMarkerState(parentState: this), + ]); + } + + @override + String toString() { + return '${super.toString()} + Resizable State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/selection_state.dart b/patterns/state/manipulator_state/states/selections/selection_state.dart new file mode 100644 index 0000000..26fb82d --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/selection_state.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/shape.dart'; +import '../free_sate.dart'; + +class SelectionState extends FreeState { + final TShape selectedShape; + + SelectionState({required this.selectedShape}); + + @override + void mouseDown(double x, double y) { + final isShapeNotSelected = !tryToSelectAndStartMovingShape(x, y); + + if (isShapeNotSelected) { + context.changeState(FreeState()); + } + } + + @override + void paint(Canvas canvas) { + super.paint(canvas); + context.paintStyle.paintSelection(canvas, selectedShape); + } + + @override + String toString() { + return '${super.toString()} + Selection State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart b/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart new file mode 100644 index 0000000..ed0f11b --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart @@ -0,0 +1,31 @@ +import 'package:flutter/services.dart'; + +class KeyboardActions { + final Map actions; + final void Function(String) inputCharAction; + + KeyboardActions({ + required this.actions, + required this.inputCharAction, + }); + + void keyDown(KeyEvent keyEvent) { + final isNotKeyDown = + !(keyEvent is KeyDownEvent || keyEvent is KeyRepeatEvent); + + if (isNotKeyDown) { + return; + } + + final foundEvent = actions[keyEvent.physicalKey]; + + if (foundEvent != null) { + foundEvent.call(); + return; + } + + if (keyEvent.character != null) { + inputCharAction.call(keyEvent.character!); + } + } +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_cursor.dart b/patterns/state/manipulator_state/states/selections/text/text_cursor.dart new file mode 100644 index 0000000..b09f9b8 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_cursor.dart @@ -0,0 +1,89 @@ +import 'dart:ui'; + +import '../../../shapes/text_shape.dart'; + +class TextCursor { + TextCursor(this._textShape) : _xPosition = _textShape.x; + + double get xCoordinate => _xPosition; + + void changePosition(double x) { + x = x - _textShape.x; + + final pos = + _textShape.paragraph.getPositionForOffset(Offset(x, _textShape.y)); + + _charIndex = pos.offset; + _xPosition = _textShape.x; + + final range = _textShape.paragraph.getBoxesForRange( + pos.offset - 1, + pos.offset, + ); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void inputText(String char) { + _changeText(char: char); + moveRight(); + } + + void backspace() { + if (_charIndex <= 0) { + return; + } + + _changeText(removeChars: -1); + moveLeft(); + } + + void moveLeft() { + _charIndex--; + _xPosition = _textShape.x; + + if (_charIndex <= 0) { + _charIndex = 0; + return; + } + + final range = + _textShape.paragraph.getBoxesForRange(_charIndex - 1, _charIndex); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void moveRight() { + _charIndex++; + _xPosition = _textShape.x; + + if (_charIndex >= _textShape.text.length) { + _charIndex = _textShape.text.length; + _xPosition += _textShape.width; + return; + } + + final range = + _textShape.paragraph.getBoxesForRange(_charIndex - 1, _charIndex); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void _changeText({String char = '', int removeChars = 0}) { + final start = _textShape.text.substring(0, _charIndex + removeChars); + final end = _textShape.text.length > start.length + ? _textShape.text.substring(_charIndex) + : ''; + _textShape.text = '$start$char$end'; + } + + final TextShape _textShape; + int _charIndex = 0; + late double _xPosition = 0; +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart b/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart new file mode 100644 index 0000000..22b6bee --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +class TextCursorAnimation { + final Duration speed; + final void Function() onBlink; + + TextCursorAnimation({ + required this.speed, + required this.onBlink, + }) { + _timer = Timer.periodic(speed, (_) { + _isShowCursor = !_isShowCursor; + onBlink.call(); + }); + } + + bool get isVisible => _isShowCursor; + + void touch() { + _isShowCursor = true; + onBlink.call(); + } + + void dispose() { + _timer.cancel(); + } + + bool _isShowCursor = true; + late Timer _timer; +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart b/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart new file mode 100644 index 0000000..05c250f --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; + +import '../../../shapes/text_shape.dart'; +import '../../_/marker.dart'; + +class TextSizeMarkerState extends Marker { + TextSizeMarkerState({required super.parentState}); + + @override + void mouseDragAction(double x, double y) { + final newHeight = + y - selectedShape.y - (selectedShape.height - selectedShape.userHeight); + + selectedShape.resize(0, newHeight); + } + + @override + void updatePosition() { + final bottomCenter = selectedShape.rect.bottomCenter; + markerShape.move(bottomCenter.dx, bottomCenter.dy); + } + + @override + MouseCursor get hoverCursor => SystemMouseCursors.resizeUpDown; + + @override + String toString() { + return '${parentState.toString()} + Text Size Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/text_edit_state.dart b/patterns/state/manipulator_state/states/selections/text_edit_state.dart new file mode 100644 index 0000000..c11b288 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text_edit_state.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../shapes/text_shape.dart'; +import 'selection_state.dart'; +import 'text/keyboard_actions.dart'; +import 'text/text_cursor.dart'; +import 'text/text_cursor_animation.dart'; + +class TextEditState extends SelectionState { + TextEditState({ + required Offset startPointer, + required super.selectedShape, + }) : _startPointer = startPointer; + + @override + void init() { + _textCursor = TextCursor(selectedShape)..changePosition(_startPointer.dx); + + _keyboardActions = KeyboardActions( + actions: { + PhysicalKeyboardKey.backspace: _textCursor.backspace, + PhysicalKeyboardKey.arrowLeft: _textCursor.moveLeft, + PhysicalKeyboardKey.arrowRight: _textCursor.moveRight, + }, + inputCharAction: _textCursor.inputText, + ); + + _animationCursor = TextCursorAnimation( + speed: Duration(milliseconds: 400), + onBlink: context.update, + ); + + context.cursor = SystemMouseCursors.text; + context.update(); + } + + @override + void mouseDown(double x, double y) { + if (selectedShape.rect.contains(Offset(x, y))) { + _textCursor.changePosition(x); + _animationCursor.touch(); + return; + } + + _animationCursor.dispose(); + super.mouseDown(x, y); + } + + @override + void keyDown(KeyEvent keyEvent) { + _keyboardActions.keyDown(keyEvent); + _animationCursor.touch(); + } + + @override + void paint(Canvas canvas) { + context.paintStyle.paintSelectedText(selectedShape, canvas); + super.paint(canvas); + + if (_animationCursor.isVisible) { + context.paintStyle.paintTextCursor(_textCursor, selectedShape, canvas); + } + } + + @override + void onHover() { + if (hoverShape == selectedShape) { + context.cursor = SystemMouseCursors.text; + } + } + + @override + void onMouseLeave() { + context.cursor = SystemMouseCursors.basic; + } + + @override + String toString() { + return '${super.toString()} + Text Edit State'; + } + + final Offset _startPointer; + late final TextCursor _textCursor; + late final KeyboardActions _keyboardActions; + late final TextCursorAnimation _animationCursor; +} diff --git a/patterns/state/manipulator_state/states/selections/text_resize_state.dart b/patterns/state/manipulator_state/states/selections/text_resize_state.dart new file mode 100644 index 0000000..c79e4cb --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text_resize_state.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import '../../shapes/text_shape.dart'; +import '../_/sub_states/parent_state.dart'; +import 'text_edit_state.dart'; +import 'text/text_size_marker_state.dart'; + +class TextResizeState extends ParentState { + TextResizeState({required super.selectedShape}) { + addChildren([ + TextSizeMarkerState(parentState: this), + ]); + } + + @override + void mouseDoubleClick(double x, double y) { + context.changeState( + TextEditState( + startPointer: Offset(x, y), + selectedShape: selectedShape, + ), + ); + } + + @override + String toString() { + return '${super.toString()} + Text Resize State'; + } +} diff --git a/patterns/state/manipulator_state/widgets/current_state.dart b/patterns/state/manipulator_state/widgets/current_state.dart new file mode 100644 index 0000000..458c06d --- /dev/null +++ b/patterns/state/manipulator_state/widgets/current_state.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/panel.dart'; +import '../pattern/manipulator.dart'; + +class CurrentState extends StatelessWidget { + final Manipulator manipulator; + + const CurrentState({ + Key? key, + required this.manipulator, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 12, + left: 300, + child: Panel( + thicknessHeight: 64, + direction: Axis.horizontal, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: EventListenableBuilder( + event: manipulator.onStateChange, + builder: (context) { + return EventListenableBuilder( + event: manipulator.onUpdate, + builder: (context) { + return Text( + manipulator.toString(), + style: TextStyle(color: Colors.white, fontSize: 16), + ); + }, + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/patterns/state/manipulator_state/widgets/drawing_board.dart b/patterns/state/manipulator_state/widgets/drawing_board.dart new file mode 100644 index 0000000..2f25926 --- /dev/null +++ b/patterns/state/manipulator_state/widgets/drawing_board.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../app/app.dart'; + +class DrawingBoard extends StatefulWidget { + final App app; + + const DrawingBoard({Key? key, required this.app}) : super(key: key); + + @override + State createState() => _DrawingBoardState(); +} + +class _DrawingBoardState extends State { + late FocusNode focusNode; + late Offset _lastMouseDown; + + @override + void initState() { + focusNode = FocusNode(skipTraversal: true); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return KeyboardListener( + autofocus: true, + focusNode: focusNode, + onKeyEvent: widget.app.manipulator.keyDown, + child: GestureDetector( + onDoubleTap: () => widget.app.manipulator.mouseDoubleClick( + _lastMouseDown.dx, + _lastMouseDown.dy, + ), + child: Listener( + onPointerDown: (e) { + _lastMouseDown = e.localPosition; + FocusScope.of(context).requestFocus(focusNode); + widget.app.manipulator.mouseDown( + e.localPosition.dx, + e.localPosition.dy, + ); + }, + onPointerHover: (e) => widget.app.manipulator.mouseMove( + e.localPosition.dx, + e.localPosition.dy, + ), + onPointerMove: (e) => widget.app.manipulator.mouseMove( + e.localPosition.dx, + e.localPosition.dy, + ), + onPointerUp: (e) => widget.app.manipulator.mouseUp(), + child: Container( + constraints: BoxConstraints.expand(), + color: Color(0xff1f1f1f), + child: EventListenableBuilder( + event: widget.app.shapes.onChange, + builder: (_) { + return EventListenableBuilder( + event: widget.app.manipulator.onUpdate, + builder: (_) { + return MouseRegion( + cursor: widget.app.manipulator.cursor, + child: CustomPaint( + painter: _Painter(widget.app), + ), + ); + }, + ); + }, + ), + ), + ), + ), + ); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } +} + +class _Painter extends CustomPainter { + final App app; + + _Painter(this.app); + + @override + void paint(Canvas canvas, Size size) { + for (final shape in app.shapes) { + shape.paint(canvas); + } + + app.manipulator.paint(canvas); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/patterns/state/manipulator_state/widgets/tool_bar.dart b/patterns/state/manipulator_state/widgets/tool_bar.dart new file mode 100644 index 0000000..b40adaf --- /dev/null +++ b/patterns/state/manipulator_state/widgets/tool_bar.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/panel.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart'; +import '../app/app.dart'; +import '../app/tool.dart'; +import '../states/free_sate.dart'; +import '../states/selections/selection_state.dart'; + +class ToolBar extends StatelessWidget { + final App app; + + const ToolBar({Key? key, required this.app}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 12, + left: 12, + child: Panel( + thicknessHeight: 64, + direction: Axis.horizontal, + child: EventListenableBuilder( + event: app.manipulator.onStateChange, + builder: (_) { + return Row( + children: [ + for (final tool in app.tools) buildButton(tool), + ], + ); + }, + ), + ), + ); + } + + Widget buildButton(Tool tool) { + return ToolButton( + active: isSelected(tool), + icon: Center(child: tool.icon), + onTap: () { + app.manipulator.changeState(tool.state); + }, + ); + } + + bool isSelected(Tool tool) { + final currentState = app.manipulator.state; + + if (currentState is SelectionState) { + if (tool.state is FreeState) { + return true; + } + } + + if (currentState.runtimeType == tool.state.runtimeType) { + return true; + } + + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1e9ed57..b24b6fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,22 @@ name: design_patterns_dart description: Dart examples for all classic GoF design patterns. -version: 0.23.14 +version: 0.24.0 homepage: https://refactoring.guru/design-patterns repository: https://github.com/RefactoringGuru/design-patterns-dart issue_tracker: https://github.com/RefactoringGuru/design-patterns-dart/issue environment: - sdk: '>=2.15.0-68.0.dev <3.0.0' + sdk: ">=2.17.0 <3.0.0" dependencies: collection: ^1.15.0 flutter: sdk: flutter cupertino_icons: ^1.0.2 + material_design_icons_flutter: ^5.0.6595 dev_dependencies: - lints: ^1.0.0 + flutter_lints: ^2.0.0 flutter: uses-material-design: true