├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── build.md
│ ├── chore.md
│ ├── ci.md
│ ├── config.yml
│ ├── documentation.md
│ ├── feature_request.md
│ ├── performance.md
│ ├── refactor.md
│ ├── revert.md
│ ├── style.md
│ └── test.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yaml
└── workflows
│ ├── main.yaml
│ ├── tag-release.yaml
│ └── version.yaml
├── .gitignore
├── .metadata
├── .vscode
├── settings.json
└── tasks.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── build.yaml
├── coverage-total.svg
├── coverage.svg
├── coverage
└── lcov.info
├── example
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── lib
│ └── main.dart
└── pubspec.yaml
├── lib
├── scribble.dart
└── src
│ ├── domain
│ ├── iterable_removed_x.dart
│ └── model
│ │ ├── point
│ │ ├── point.dart
│ │ ├── point.freezed.dart
│ │ └── point.g.dart
│ │ ├── sketch
│ │ ├── sketch.dart
│ │ ├── sketch.freezed.dart
│ │ └── sketch.g.dart
│ │ └── sketch_line
│ │ ├── sketch_line.dart
│ │ ├── sketch_line.freezed.dart
│ │ └── sketch_line.g.dart
│ └── view
│ ├── notifier
│ └── scribble_notifier.dart
│ ├── painting
│ ├── point_to_offset_x.dart
│ ├── scribble_editing_painter.dart
│ ├── scribble_painter.dart
│ └── sketch_line_path_mixin.dart
│ ├── pan_gesture_catcher.dart
│ ├── scribble.dart
│ ├── scribble_sketch.dart
│ ├── simplification
│ └── sketch_simplifier.dart
│ └── state
│ ├── scribble.state.dart
│ ├── scribble.state.freezed.dart
│ └── scribble.state.g.dart
├── melos.yaml
├── packages
├── simpli
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── analysis_options.yaml
│ ├── coverage.svg
│ ├── coverage
│ │ └── lcov.info
│ ├── lib
│ │ ├── simpli.dart
│ │ └── src
│ │ │ ├── data
│ │ │ ├── rdp_simplifier.dart
│ │ │ ├── utils.dart
│ │ │ └── visvalingam_simplifier.dart
│ │ │ ├── domain
│ │ │ └── simplifier.dart
│ │ │ └── simpli.dart
│ ├── pubspec.yaml
│ └── test
│ │ └── src
│ │ └── data
│ │ ├── rdp_simplifier_test.dart
│ │ ├── utils_test.dart
│ │ └── visvalingam_simplifier_test.dart
└── value_notifier_tools
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── analysis_options.yaml
│ ├── coverage.svg
│ ├── coverage
│ └── lcov.info
│ ├── lib
│ ├── src
│ │ ├── history_value_notifier
│ │ │ ├── history_value_notifier.dart
│ │ │ └── history_value_notifier_mixin.dart
│ │ ├── select_value_notifier
│ │ │ └── select_value_notifier.dart
│ │ └── where_value_notifier
│ │ │ ├── where_value_notifier.dart
│ │ │ ├── where_value_notifier_from_parent.dart
│ │ │ └── where_value_notifier_mixin.dart
│ └── value_notifier_tools.dart
│ ├── pubspec.yaml
│ └── test
│ ├── src
│ ├── history_value_notifier
│ │ └── history_value_notifier_test.dart
│ ├── select_value_notifier
│ │ └── select_value_notifier_test.dart
│ └── where_value_notifier
│ │ ├── where_value_notifier_from_parent_test.dart
│ │ └── where_value_notifier_test.dart
│ └── util
│ └── mock_listener.dart
├── pubspec.yaml
├── scribble_demo.gif
└── test
└── src
├── domain
├── iterable_removed_x_test.dart
└── model
│ └── sketch
│ └── sketch_test.dart
└── view
├── notifier
└── scribble_notifier_test.dart
├── scribble_test.dart
└── simplification
└── sketch_simplifier_test.dart
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: "fix: "
5 | labels: bug
6 | ---
7 |
8 | **Description**
9 |
10 | A clear and concise description of what the bug is.
11 |
12 | **Steps To Reproduce**
13 |
14 | 1. Go to '...'
15 | 2. Click on '....'
16 | 3. Scroll down to '....'
17 | 4. See error
18 |
19 | **Expected Behavior**
20 |
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 |
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Additional Context**
28 |
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/build.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Build System
3 | about: Changes that affect the build system or external dependencies
4 | title: "build: "
5 | labels: build
6 | ---
7 |
8 | **Description**
9 |
10 | Describe what changes need to be done to the build system and why.
11 |
12 | **Requirements**
13 |
14 | - [ ] The build system is passing
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/chore.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Chore
3 | about: Other changes that don't modify src or test files
4 | title: "chore: "
5 | labels: chore
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what change is needed and why. If this changes code then please use another issue type.
11 |
12 | **Requirements**
13 |
14 | - [ ] No functional changes to the code
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ci.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Continuous Integration
3 | about: Changes to the CI configuration files and scripts
4 | title: "ci: "
5 | labels: ci
6 | ---
7 |
8 | **Description**
9 |
10 | Describe what changes need to be done to the ci/cd system and why.
11 |
12 | **Requirements**
13 |
14 | - [ ] The ci system is passing
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation
3 | about: Improve the documentation so all collaborators have a common understanding
4 | title: "docs: "
5 | labels: documentation
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what documentation you are looking to add or improve.
11 |
12 | **Requirements**
13 |
14 | - [ ] Requirements go here
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: A new feature to be added to the project
4 | title: "feat: "
5 | labels: feature
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what you are looking to add. The more context the better.
11 |
12 | **Requirements**
13 |
14 | - [ ] Checklist of requirements to be fulfilled
15 |
16 | **Additional Context**
17 |
18 | Add any other context or screenshots about the feature request go here.
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/performance.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Performance Update
3 | about: A code change that improves performance
4 | title: "perf: "
5 | labels: performance
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience.
11 |
12 | **Requirements**
13 |
14 | - [ ] There is no drop in test coverage.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/refactor.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Refactor
3 | about: A code change that neither fixes a bug nor adds a feature
4 | title: "refactor: "
5 | labels: refactor
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize.
11 |
12 | **Requirements**
13 |
14 | - [ ] There is no drop in test coverage.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/revert.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Revert Commit
3 | about: Reverts a previous commit
4 | title: "revert: "
5 | labels: revert
6 | ---
7 |
8 | **Description**
9 |
10 | Provide a link to a PR/Commit that you are looking to revert and why.
11 |
12 | **Requirements**
13 |
14 | - [ ] Change has been reverted
15 | - [ ] No change in test coverage has happened
16 | - [ ] A new ticket is created for any follow on work that needs to happen
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/style.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Style Changes
3 | about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
4 | title: "style: "
5 | labels: style
6 | ---
7 |
8 | **Description**
9 |
10 | Clearly describe what you are looking to change and why.
11 |
12 | **Requirements**
13 |
14 | - [ ] There is no drop in test coverage.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/test.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Test
3 | about: Adding missing tests or correcting existing tests
4 | title: "test: "
5 | labels: test
6 | ---
7 |
8 | **Description**
9 |
10 | List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past.
11 |
12 | **Requirements**
13 |
14 | - [ ] There is no drop in test coverage.
15 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ## Description
10 |
11 |
12 |
13 | ## Checklist
14 |
15 |
16 | - [ ] My PR title is in the style of [conventional commits](https://www.conventionalcommits.org/)
17 | - [ ] All public facing APIs are documented with [dartdoc](https://dart.dev/guides/language/effective-dart/documentation)
18 | - [ ] I have added tests to cover my changes
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | enable-beta-ecosystems: true
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 | - package-ecosystem: "pub"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - main
11 | pull_request:
12 | branches:
13 | - main
14 |
15 | jobs:
16 | semantic_pull_request:
17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1
18 |
19 | flutter-check:
20 | name: Build Check
21 | runs-on: ubuntu-latest
22 | timeout-minutes: 10
23 | steps:
24 | - name: 📚 Checkout
25 | uses: actions/checkout@v4
26 |
27 | - name: 🐦 Setup Flutter
28 | uses: subosito/flutter-action@v2
29 | with:
30 | channel: 'stable'
31 | cache: true
32 |
33 | - name: Ⓜ️ Set up Melos
34 | uses: bluefireteam/melos-action@v3
35 |
36 | - name: 🧪 Run Analyze
37 | run: melos run analyze
38 |
39 | - name: 📝 Run Test
40 | run: melos run coverage
41 |
42 | - name: 📊 Generate Coverage
43 | id: coverage-report
44 | uses: whynotmake-it/dart-coverage-assistant@v1
45 | with:
46 | generate_badges: pr
47 | upper_threshold: 30
48 |
49 | check_generation:
50 | name: Check Code Generation
51 | timeout-minutes: 10
52 | runs-on: ubuntu-latest
53 | steps:
54 | - name: 📚 Checkout
55 | uses: actions/checkout@v4
56 |
57 | - name: 🐦 Setup Flutter
58 | uses: subosito/flutter-action@v2
59 | with:
60 | channel: 'stable'
61 | cache: true
62 |
63 | - name: Ⓜ️ Set up Melos
64 | uses: bluefireteam/melos-action@v3
65 |
66 | - name: 🔨 Generate
67 | run: melos run generate
68 |
69 | - name: 🔎 Check there are no uncommitted changes
70 | run: git add . && git diff --cached --exit-code
71 |
--------------------------------------------------------------------------------
/.github/workflows/tag-release.yaml:
--------------------------------------------------------------------------------
1 | name: Tag release
2 | on:
3 | push:
4 | branches: [main]
5 |
6 | jobs:
7 | publish-packages:
8 | name: Create tag for a release
9 | permissions:
10 | contents: write
11 | runs-on: [ ubuntu-latest ]
12 | if: contains(github.event.head_commit.message, 'chore(release)')
13 | steps:
14 | - name: 📚 Checkout
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: 🐦 Setup Flutter
20 | uses: subosito/flutter-action@v2
21 |
22 | - name: Ⓜ️ Set up Melos
23 | uses: bluefireteam/melos-action@v3
24 | with:
25 | tag: true
26 |
--------------------------------------------------------------------------------
/.github/workflows/version.yaml:
--------------------------------------------------------------------------------
1 | name: Version
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | prerelease:
7 | description: 'Version as prerelease'
8 | required: false
9 | default: false
10 | type: boolean
11 | graduate:
12 | description: 'Graduate prereleases'
13 | required: false
14 | default: false
15 | type: boolean
16 |
17 | jobs:
18 | prepare-release:
19 | name: Prepare release
20 | permissions:
21 | contents: write
22 | pull-requests: write
23 | runs-on: ubuntu-latest
24 | steps:
25 | - name: 📚 Checkout
26 | uses: actions/checkout@v4
27 | with:
28 | fetch-depth: 0
29 |
30 | - name: 🐦 Setup Flutter
31 | uses: subosito/flutter-action@v2
32 |
33 | - name: Ⓜ️ Set up Melos
34 | uses: bluefireteam/melos-action@v3
35 | with:
36 | run-versioning: ${{ inputs.prerelease == false }}
37 | run-versioning-prerelease: ${{ inputs.prerelease == true }}
38 | run-versioning-graduate: ${{ inputs.graduate == true }}
39 | publish-dry-run: true
40 |
41 | - name: 🎋 Create Pull Request
42 | uses: peter-evans/create-pull-request@v7
43 | with:
44 | title: "chore(release): Publish packages"
45 | body: "Prepared all packages to be released to pub.dev"
46 | branch: chore/release
47 | delete-branch: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't include platform code in example
2 | /example/android
3 | /example/ios
4 | /example/linux
5 | /example/macos
6 | /example/web
7 | /example/windows
8 |
9 |
10 | # Miscellaneous
11 | *.class
12 | *.log
13 | *.pyc
14 | *.swp
15 | .DS_Store
16 | .atom/
17 | .buildlog/
18 | .history
19 | .svn/
20 | .mason/
21 | migrate_working_dir/
22 |
23 | # IntelliJ related
24 | *.iml
25 | *.ipr
26 | *.iws
27 | .idea/
28 |
29 | # See https://www.dartlang.org/guides/libraries/private-files
30 |
31 | # Files and directories created by pub
32 | .dart_tool/
33 | .packages
34 | build/
35 | pubspec.lock
36 | pubspec_overrides.yaml
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 4cc385b4b84ac2f816d939a49ea1f328c4e0b48e
8 | channel: stable
9 |
10 | project_type: package
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "conventionalCommits.scopes": [
3 | "value_notifier_tools",
4 | "melos",
5 | "example",
6 | "simpli"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build_runner build",
6 | "type": "shell",
7 | "command": "melos run build_runner",
8 | "group": "build", "presentation": {
9 | "reveal": "silent",
10 | "group": "build"
11 | }
12 | },
13 | {
14 | "label": "melos bullshit",
15 | "type": "shell",
16 | "group": "none",
17 | "command": "melos bs",
18 | "presentation": {
19 | "reveal": "silent"
20 | },
21 | "problemMatcher": []
22 | },
23 | {
24 | "label": "dart fix",
25 | "group": "none",
26 | "type": "shell",
27 | "command": "melos run fix",
28 | "presentation": {
29 | "reveal": "silent"
30 | },
31 | "problemMatcher": []
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.10.0+1
2 |
3 | - **DOCS**: fixed demo image.
4 |
5 | ## 0.10.0
6 |
7 | > Note: This release has breaking changes.
8 |
9 | - **REFACTOR**: cleaned up project structure.
10 | - **REFACTOR**: added CI and documentation.
11 | - **FIX**(value_notifier_tools): fixed type parameters in select extension.
12 | - **FIX**(value_notifier_tools): Added all classes to package exports.
13 | - **FIX**: fixed point.freezed.dart.
14 | - **FIX**(example): fixed dialog title.
15 | - **FEAT**(example): updated example to use `SelectValueNotifier`.
16 | - **FEAT**: use `HistoryValueNotifier` from new package.
17 | - **FEAT**(value_notifier_tools): added `HistoryValueNotifier`.
18 | - **FEAT**(value_notifier_tools): added where_value_notifier.
19 | - **FEAT**(value_notifier_tools): added select value notifier.
20 | - **FEAT**(example): show JSON.
21 | - **FEAT**(example): updated example visuals.
22 | - **FEAT**(example): updated example for new `ScribbleNotifier`.
23 | - **DOCS**: added back demo gif.
24 | - **DOCS**: updated README.
25 | - **DOCS**(value_notifier_tools): added README.
26 | - **DOCS**: documented all public members.
27 | - **BREAKING** **REFACTOR**: made `GestureCatcherRecognizer` private.
28 | - **BREAKING** **REFACTOR**: `ScribbleNotifier` is now a `ValueNotifier`.
29 |
30 | ## 0.9.1
31 | - removed erroneous `simulatePressure`
32 | ## 0.9.0
33 | - Use `perfect_freehand` for much smoother lines (thanks to @mattrussell-sonocent)
34 | - BREAKING: Removed line customization parameters from widgets
35 |
36 | ## 0.4.0
37 | - Upgraded dependencies (thanks to @wxxedu)
38 | - ``ScribbleNotifier.clear()`` clears the drawing without resetting anything else (color, width e.t.c)
39 |
40 | ## 0.3.0
41 | - Upgraded dependencies
42 | - Added ``ScribbleSketch`` widget for just displaying a sketch without input functionality, no notifier needed!
43 | ## 0.2.2
44 | - Upgraded dependencies
45 |
46 | ## 0.2.1
47 | - Updated README to include the newest features
48 | - Downgraded json_serializable due to a bug with freezed
49 |
50 | ## 0.2.0
51 | #### BREAKING:
52 | - Custom ScribbleNotifiers now need to provide a GlobalKey which is used in the renderImage() method to access Scribble's
53 | RepaintBoundary
54 | - Updated example to demonstrate image export.
55 |
56 | #### Image Export:
57 | - You can now export the Scribble to an Image ```ByteData``` using the ScribbleNotifiers ``renderImage()`` method!
58 |
59 | #### Other Changes:
60 | - The pressure on web is overridden so the cursor matches the selected pen width!
61 | - ``ScribbleNotifier`` now extends ``ScribbleNotifierBase`` instead of implementing it as an interface.
62 | - Updated dependencies
63 |
64 | ## 0.1.3
65 | #### Filter for Pointers:
66 | You can now switch between different ``ScribblePointerMode``s, even at runtime.
67 |
68 | This is very helpful for example, if Scribble lives inside a Scrollable and you want users to be able to navigate with their finger while drawing with their pen.
69 |
70 | Check the updated example to try it out!
71 |
72 | #### Other Changes:
73 | * ``ScribbleNotifier`` now has the option to set the sketch from outside after it has been constructed using the ``setSketch()`` method. You can even choose whether you want it to be committed to the undo history.
74 | * Added documentation to ``ScribbleState``
75 | * Updated example
76 | * Updated dependencies
77 |
78 | ## 0.1.2
79 |
80 | * Removed the speed calculation using time due to precision issues
81 |
82 | ## 0.1.1
83 | * Added scaleFactor to support zoomable canvases. This allows you to for example wrap the Scribble Widget in an
84 | InteractiveViewer, so that users can draw finer details.
85 |
86 | ## 0.1.0
87 |
88 | * Points now remember their time to calculate speed more accurately
89 | * Multiple fixes for drawing with real pens or touch
90 | * ``ScribbleState`` can now be serialized to JSON
91 |
92 | ### Breaking:
93 |
94 | * speedFactor's value should now be higher for the same effect, the default value has changed to 0.4
95 | * ``color`` property in state is now an int to allow for easy JSON
96 | ## 0.0.13
97 |
98 | * Draw better line ends
99 | * Removed marker-like blend mode for now due to performance and buggy rendering in some cases
100 |
101 | ## 0.0.12
102 |
103 | * Eraser keeps pen width and the other way around
104 |
105 | ## 0.0.11
106 |
107 | * Eraser doesn't autoselect anymore
108 | * Undo doesn't undo color and stroke selection
109 |
110 | ## 0.0.10
111 |
112 | * Fixed stupid bug with pointer exit
113 |
114 | ## 0.0.9
115 |
116 | * Better behavior on pointer exit
117 |
118 | ## 0.0.8
119 |
120 | * Reduced Dependencies
121 | * Replaced kimchi package with the better suited [history_state_notifier](https://pub.dev/packages/history_state_notifier)
122 | * Fixed a bug with redo queue clearing
123 |
124 | ## 0.0.7
125 |
126 | * **BREAKING:** The ``drawPointer`` parameter is now called ``drawPen``
127 | * You can now obtain the current sketch from the notifier.
128 | If you want to store it somewhere for example you can call ```toJson()``` on it.
129 | * You can now pass a sketch to a ``ScribbleNotifier`` constructor to initialize it with an existing
130 | drawing.
131 | * Added ``ScribbleNotifierBase`` interface so you can write your own notifier that works with the ``Scribble``widget
132 | * Added pressure curve support to the notifier
133 | * Allows more customization in the scribble widget for how the lines are rendered
134 |
135 |
136 | ## 0.0.6
137 |
138 | * Upped minimum flutter version to 2.5
139 |
140 | ## 0.0.5
141 |
142 | * Back to flutter 2.2.3
143 |
144 | ## 0.0.4
145 |
146 | * Upped minimum flutter version to 2.3
147 |
148 | ## 0.0.3
149 |
150 | * meta dependency to hopefully work with analysis
151 |
152 | ## 0.0.2
153 |
154 | * Added documentation
155 | * Fixed dependencies
156 |
157 | ## 0.0.1
158 |
159 | * Initial release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jesper Bellenbaum, Tim Lehmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scribble
2 |
3 | [](https://github.com/felangel/mason)
4 | [](https://github.com/invertase/melos)
5 | 
6 |
7 | Scribble is a lightweight library for freehand drawing in Flutter supporting pressure, variable line width and more!
8 |
9 | 
10 |
11 | ## Installation 💻
12 |
13 | **❗ In order to start using Scribble you must have the [Dart SDK][dart_install_link] installed on your machine.**
14 |
15 | Install via `dart pub add`:
16 |
17 | ```sh
18 | dart pub add scribble
19 | ```
20 |
21 | ---
22 |
23 | ## Features
24 |
25 | * Variable line width
26 | * Image Export
27 | * Pen and touch pressure support
28 | * Line simplification for making sketch files smaller
29 | * Choose which pointers can draw (touch, pen, mouse, etc.)
30 | * Lines get slimmer when the pen is moved more quickly
31 | * Line eraser support
32 | * Full undo/redo support using [value_notifier_tools](https://pub.dev/packages/value_notifier_tools)
33 | * Sketches are fully serializable to JSON
34 | * Export Sketches to PNG
35 |
36 | ## Usage
37 |
38 | > You can find a full working example in the [example](./example) directory
39 |
40 | You can create a drawing surface by adding the `Scribble` widget to your widget tree and passing in
41 | a `ScribbleNotifier`.
42 |
43 | ```dart
44 | import 'package:flutter/material.dart';
45 | import 'package:flutter_riverpod/flutter_riverpod.dart';
46 |
47 | class App extends StatelessWidget {
48 | @override
49 | Widget build(BuildContext context) {
50 | return Scaffold(
51 | body: Scribble(
52 | notifier: notifier,
53 | ),
54 | );
55 | }
56 | }
57 | ```
58 |
59 | Use the public methods on `ScribbleNotifier` to control the behavior (for example from a button in the UI):
60 |
61 | ```dart
62 | notifier = ScribbleNotifier();
63 |
64 |
65 | // Set color
66 | notifier.setColor(Colors.black);
67 |
68 | // Clear
69 | notifier.clear();
70 |
71 | // Undo
72 | notifier.undo();
73 |
74 | // Export to Image
75 | notifier.renderImage(pixelRatio: 2.0);
76 |
77 | // Line details will be simplified to save space from now on
78 | notifier.setSimplificationFactor(2);
79 |
80 | // Simplify the entire existing sketch
81 | notifier.simplify();
82 |
83 | // And more ...
84 | ```
85 |
86 | ## Additional information
87 |
88 | As mentioned above, the package is still under development, but we already use it in the app we are currently
89 | developing.
90 |
91 | Feel free to contribute, or open issues in our [GitHub repo](https://github.com/timcreatedit/scribble).
92 |
93 |
94 | [dart_install_link]: https://dart.dev/get-dart
95 | [github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
96 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
97 | [license_link]: https://opensource.org/licenses/MIT
98 | [mason_link]: https://github.com/felangel/mason
99 | [very_good_ventures_link]: https://verygood.ventures
100 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lintervention/analysis_options.yaml
2 |
--------------------------------------------------------------------------------
/build.yaml:
--------------------------------------------------------------------------------
1 | targets:
2 | $default:
3 | builders:
4 | json_serializable:
5 | options:
6 | explicit_to_json: true
--------------------------------------------------------------------------------
/coverage-total.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/coverage/lcov.info:
--------------------------------------------------------------------------------
1 | SF:lib/src/domain/model/sketch/sketch.dart
2 | DA:19,2
3 | LF:1
4 | LH:1
5 | end_of_record
6 | SF:lib/src/domain/model/sketch/sketch.g.dart
7 | DA:9,2
8 | DA:10,1
9 | DA:11,3
10 | DA:12,1
11 | DA:15,0
12 | DA:16,0
13 | DA:17,0
14 | LF:7
15 | LH:4
16 | end_of_record
17 | SF:lib/src/view/notifier/scribble_notifier.dart
18 | DA:23,2
19 | DA:59,0
20 | DA:63,0
21 | DA:66,0
22 | DA:71,0
23 | DA:72,0
24 | DA:84,2
25 | DA:100,2
26 | DA:101,2
27 | DA:103,3
28 | DA:109,2
29 | DA:114,2
30 | DA:131,3
31 | DA:135,1
32 | DA:136,1
33 | DA:144,1
34 | DA:150,2
35 | DA:151,1
36 | DA:164,1
37 | DA:168,3
38 | DA:172,1
39 | DA:174,1
40 | DA:179,0
41 | DA:180,0
42 | DA:181,0
43 | DA:185,0
44 | DA:192,0
45 | DA:193,0
46 | DA:199,0
47 | DA:200,0
48 | DA:201,0
49 | DA:202,0
50 | DA:203,0
51 | DA:204,0
52 | DA:205,0
53 | DA:211,0
54 | DA:212,0
55 | DA:221,0
56 | DA:222,0
57 | DA:223,0
58 | DA:229,0
59 | DA:230,0
60 | DA:231,0
61 | DA:232,0
62 | DA:233,0
63 | DA:234,0
64 | DA:235,0
65 | DA:237,0
66 | DA:238,0
67 | DA:239,0
68 | DA:240,0
69 | DA:241,0
70 | DA:242,0
71 | DA:243,0
72 | DA:259,1
73 | DA:260,4
74 | DA:270,1
75 | DA:271,2
76 | DA:272,2
77 | DA:273,2
78 | DA:276,4
79 | DA:278,4
80 | DA:283,0
81 | DA:285,0
82 | DA:286,0
83 | DA:288,0
84 | DA:293,1
85 | DA:295,4
86 | DA:296,1
87 | DA:299,3
88 | DA:300,0
89 | DA:301,0
90 | DA:303,0
91 | DA:304,0
92 | DA:305,0
93 | DA:308,0
94 | DA:310,2
95 | DA:311,3
96 | DA:312,1
97 | DA:313,1
98 | DA:314,2
99 | DA:315,2
100 | DA:316,5
101 | DA:320,3
102 | DA:321,4
103 | DA:326,1
104 | DA:328,4
105 | DA:329,2
106 | DA:330,0
107 | DA:335,2
108 | DA:336,5
109 | DA:337,1
110 | DA:339,0
111 | DA:340,0
112 | DA:344,0
113 | DA:345,0
114 | DA:349,0
115 | DA:350,0
116 | DA:357,1
117 | DA:359,4
118 | DA:361,2
119 | DA:362,2
120 | DA:363,6
121 | DA:366,7
122 | DA:368,0
123 | DA:369,0
124 | DA:372,0
125 | DA:374,0
126 | DA:375,0
127 | DA:376,0
128 | DA:380,0
129 | DA:382,0
130 | DA:383,0
131 | DA:384,0
132 | DA:391,0
133 | DA:393,0
134 | DA:394,0
135 | DA:395,0
136 | DA:398,0
137 | DA:400,0
138 | DA:401,0
139 | DA:404,0
140 | DA:406,0
141 | DA:407,0
142 | DA:408,0
143 | DA:412,0
144 | DA:414,0
145 | DA:415,0
146 | DA:416,0
147 | DA:422,0
148 | DA:424,0
149 | DA:425,0
150 | DA:428,0
151 | DA:432,1
152 | DA:433,2
153 | DA:434,2
154 | DA:435,1
155 | DA:436,2
156 | DA:438,6
157 | DA:439,3
158 | DA:440,2
159 | DA:441,2
160 | DA:442,1
161 | DA:443,1
162 | DA:444,1
163 | DA:450,0
164 | DA:451,0
165 | DA:452,0
166 | DA:453,0
167 | DA:454,0
168 | DA:455,0
169 | DA:456,0
170 | DA:459,0
171 | DA:461,0
172 | DA:465,0
173 | DA:466,0
174 | DA:473,1
175 | DA:474,3
176 | DA:476,0
177 | DA:477,0
178 | DA:478,1
179 | DA:479,2
180 | DA:480,2
181 | DA:481,2
182 | DA:485,1
183 | DA:486,2
184 | DA:487,2
185 | DA:489,3
186 | DA:490,1
187 | DA:491,2
188 | DA:492,2
189 | DA:494,1
190 | LF:172
191 | LH:76
192 | end_of_record
193 | SF:lib/src/view/scribble.dart
194 | DA:19,1
195 | DA:49,1
196 | DA:51,1
197 | DA:52,1
198 | DA:53,1
199 | DA:55,2
200 | DA:56,1
201 | DA:57,1
202 | DA:58,1
203 | DA:60,1
204 | DA:61,1
205 | DA:62,1
206 | DA:64,1
207 | DA:65,2
208 | DA:66,1
209 | DA:67,1
210 | DA:68,1
211 | DA:69,1
212 | DA:70,1
213 | DA:76,1
214 | DA:78,1
215 | DA:79,1
216 | DA:80,1
217 | DA:82,1
218 | DA:83,1
219 | DA:86,2
220 | DA:87,1
221 | DA:88,2
222 | DA:89,2
223 | DA:90,2
224 | DA:91,2
225 | DA:92,2
226 | LF:32
227 | LH:32
228 | end_of_record
229 | SF:lib/src/view/scribble_sketch.dart
230 | DA:13,0
231 | DA:32,0
232 | DA:34,0
233 | DA:35,0
234 | DA:36,0
235 | DA:37,0
236 | DA:38,0
237 | LF:7
238 | LH:0
239 | end_of_record
240 | SF:lib/src/view/state/scribble.state.dart
241 | DA:108,0
242 | DA:109,0
243 | DA:110,2
244 | DA:114,8
245 | DA:118,0
246 | DA:119,0
247 | DA:120,0
248 | DA:121,0
249 | DA:122,0
250 | DA:127,2
251 | DA:128,2
252 | DA:129,2
253 | DA:130,2
254 | DA:131,0
255 | DA:133,0
256 | DA:138,0
257 | LF:16
258 | LH:6
259 | end_of_record
260 | SF:lib/src/view/state/scribble.state.g.dart
261 | DA:9,0
262 | DA:10,0
263 | DA:11,0
264 | DA:12,0
265 | DA:14,0
266 | DA:15,0
267 | DA:16,0
268 | DA:18,0
269 | DA:19,0
270 | DA:20,0
271 | DA:22,0
272 | DA:24,0
273 | DA:25,0
274 | DA:26,0
275 | DA:27,0
276 | DA:29,0
277 | DA:30,0
278 | DA:33,0
279 | DA:34,0
280 | DA:35,0
281 | DA:36,0
282 | DA:38,0
283 | DA:39,0
284 | DA:40,0
285 | DA:41,0
286 | DA:42,0
287 | DA:43,0
288 | DA:44,0
289 | DA:45,0
290 | DA:55,0
291 | DA:56,0
292 | DA:57,0
293 | DA:58,0
294 | DA:59,0
295 | DA:61,0
296 | DA:62,0
297 | DA:63,0
298 | DA:65,0
299 | DA:67,0
300 | DA:68,0
301 | DA:69,0
302 | DA:71,0
303 | DA:72,0
304 | DA:75,0
305 | DA:76,0
306 | DA:77,0
307 | DA:79,0
308 | DA:80,0
309 | DA:81,0
310 | DA:82,0
311 | DA:83,0
312 | DA:84,0
313 | DA:85,0
314 | LF:53
315 | LH:0
316 | end_of_record
317 | SF:lib/src/domain/iterable_removed_x.dart
318 | DA:8,2
319 | DA:9,8
320 | DA:11,6
321 | DA:16,2
322 | DA:17,2
323 | DA:19,2
324 | DA:20,8
325 | DA:21,2
326 | DA:22,2
327 | DA:24,6
328 | DA:25,2
329 | DA:31,1
330 | DA:32,3
331 | DA:33,2
332 | DA:34,1
333 | LF:15
334 | LH:15
335 | end_of_record
336 | SF:lib/src/domain/model/point/point.dart
337 | DA:18,4
338 | DA:21,2
339 | LF:2
340 | LH:2
341 | end_of_record
342 | SF:lib/src/domain/model/point/point.g.dart
343 | DA:9,2
344 | DA:10,2
345 | DA:11,2
346 | DA:12,2
347 | DA:15,0
348 | DA:16,0
349 | DA:17,0
350 | DA:18,0
351 | DA:19,0
352 | LF:9
353 | LH:4
354 | end_of_record
355 | SF:lib/src/domain/model/sketch_line/sketch_line.dart
356 | DA:25,1
357 | DA:26,1
358 | LF:2
359 | LH:2
360 | end_of_record
361 | SF:lib/src/domain/model/sketch_line/sketch_line.g.dart
362 | DA:9,1
363 | DA:10,1
364 | DA:11,1
365 | DA:12,3
366 | DA:13,1
367 | DA:14,2
368 | DA:15,2
369 | DA:18,0
370 | DA:19,0
371 | DA:20,0
372 | DA:21,0
373 | DA:22,0
374 | LF:12
375 | LH:7
376 | end_of_record
377 | SF:lib/src/view/painting/point_to_offset_x.dart
378 | DA:8,4
379 | LF:1
380 | LH:1
381 | end_of_record
382 | SF:lib/src/view/simplification/sketch_simplifier.dart
383 | DA:11,6
384 | DA:17,1
385 | DA:18,1
386 | DA:19,2
387 | DA:20,1
388 | DA:21,1
389 | DA:22,1
390 | DA:34,6
391 | DA:36,1
392 | DA:38,1
393 | DA:40,7
394 | DA:42,1
395 | DA:46,2
396 | DA:47,2
397 | DA:48,4
398 | LF:15
399 | LH:15
400 | end_of_record
401 | SF:lib/src/view/painting/sketch_line_path_mixin.dart
402 | DA:20,0
403 | DA:24,0
404 | DA:25,0
405 | DA:26,0
406 | DA:27,0
407 | DA:28,0
408 | DA:29,0
409 | DA:30,0
410 | DA:32,0
411 | DA:33,0
412 | DA:37,0
413 | DA:39,0
414 | DA:40,0
415 | DA:41,0
416 | DA:42,0
417 | DA:43,0
418 | DA:48,0
419 | DA:50,0
420 | DA:51,0
421 | DA:52,0
422 | DA:53,0
423 | DA:54,0
424 | DA:55,0
425 | DA:56,0
426 | DA:57,0
427 | LF:25
428 | LH:0
429 | end_of_record
430 | SF:lib/src/view/painting/scribble_editing_painter.dart
431 | DA:12,1
432 | DA:36,1
433 | DA:38,2
434 | DA:40,2
435 | DA:41,2
436 | DA:42,0
437 | DA:45,0
438 | DA:47,0
439 | DA:50,0
440 | DA:51,0
441 | DA:55,2
442 | DA:56,0
443 | DA:58,0
444 | DA:59,0
445 | DA:60,0
446 | DA:62,0
447 | DA:63,0
448 | DA:64,0
449 | DA:66,0
450 | DA:67,0
451 | DA:68,0
452 | DA:69,0
453 | DA:75,0
454 | DA:77,0
455 | DA:78,0
456 | LF:25
457 | LH:6
458 | end_of_record
459 | SF:lib/src/view/painting/scribble_painter.dart
460 | DA:8,1
461 | DA:23,1
462 | DA:25,2
463 | DA:27,4
464 | DA:28,0
465 | DA:29,0
466 | DA:30,0
467 | DA:35,0
468 | DA:36,0
469 | DA:40,0
470 | DA:42,0
471 | DA:43,0
472 | DA:44,0
473 | LF:13
474 | LH:4
475 | end_of_record
476 | SF:lib/src/view/pan_gesture_catcher.dart
477 | DA:12,1
478 | DA:24,1
479 | DA:26,1
480 | DA:27,2
481 | DA:28,1
482 | DA:30,1
483 | DA:31,2
484 | DA:33,1
485 | DA:35,1
486 | DA:38,1
487 | DA:45,1
488 | DA:48,1
489 | DA:50,0
490 | DA:53,0
491 | DA:56,0
492 | DA:58,0
493 | LF:16
494 | LH:12
495 | end_of_record
496 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Symbolication related
35 | app.*.symbols
36 |
37 | # Obfuscation related
38 | app.*.map.json
39 |
40 | # Android Studio will place build artifacts here
41 | /android/app/debug
42 | /android/app/profile
43 | /android/app/release
44 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "68bfaea224880b488c617afe30ab12091ea8fa4e"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e
17 | base_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e
18 | - platform: macos
19 | create_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e
20 | base_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # example
2 |
3 | A new Flutter project.
4 |
--------------------------------------------------------------------------------
/example/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:scribble/scribble.dart';
5 | import 'package:value_notifier_tools/value_notifier_tools.dart';
6 |
7 | void main() {
8 | runApp(const MyApp());
9 | }
10 |
11 | class MyApp extends StatelessWidget {
12 | const MyApp({super.key});
13 |
14 | // This widget is the root of your application.
15 | @override
16 | Widget build(BuildContext context) {
17 | return MaterialApp(
18 | title: 'Scribble',
19 | theme: ThemeData.from(
20 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple)),
21 | home: const HomePage(title: 'Scribble'),
22 | );
23 | }
24 | }
25 |
26 | class HomePage extends StatefulWidget {
27 | const HomePage({super.key, required this.title});
28 |
29 | final String title;
30 |
31 | @override
32 | State createState() => _HomePageState();
33 | }
34 |
35 | class _HomePageState extends State {
36 | late ScribbleNotifier notifier;
37 |
38 | bool _simulatePressure = true;
39 |
40 | @override
41 | void initState() {
42 | notifier = ScribbleNotifier();
43 | super.initState();
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return Scaffold(
49 | backgroundColor: Theme.of(context).colorScheme.surface,
50 | appBar: AppBar(
51 | title: Text(widget.title),
52 | actions: _buildActions(context),
53 | ),
54 | body: Padding(
55 | padding: const EdgeInsets.symmetric(horizontal: 64),
56 | child: Column(
57 | children: [
58 | Expanded(
59 | child: Card(
60 | clipBehavior: Clip.hardEdge,
61 | margin: EdgeInsets.zero,
62 | color: Colors.white,
63 | surfaceTintColor: Colors.white,
64 | child: Scribble(
65 | notifier: notifier,
66 | drawPen: true,
67 | simulatePressure: _simulatePressure,
68 | ),
69 | ),
70 | ),
71 | Padding(
72 | padding: const EdgeInsets.all(16),
73 | child: Column(
74 | children: [
75 | SizedBox(
76 | width: double.infinity,
77 | child: Wrap(
78 | crossAxisAlignment: WrapCrossAlignment.center,
79 | alignment: WrapAlignment.spaceBetween,
80 | spacing: 16,
81 | runSpacing: 16,
82 | children: [
83 | Row(
84 | mainAxisSize: MainAxisSize.min,
85 | children: [
86 | _buildColorToolbar(context),
87 | const VerticalDivider(width: 32),
88 | _buildStrokeToolbar(context),
89 | ],
90 | ),
91 | Row(
92 | mainAxisSize: MainAxisSize.min,
93 | children: [
94 | Switch.adaptive(
95 | value: _simulatePressure,
96 | onChanged: (v) =>
97 | setState(() => _simulatePressure = v),
98 | ),
99 | const SizedBox(width: 8),
100 | const Text("Simulate Pressure")
101 | ],
102 | ),
103 | _buildPointerModeSwitcher(context),
104 | ],
105 | ),
106 | ),
107 | const Divider(
108 | height: 32,
109 | ),
110 | Row(
111 | children: [
112 | ValueListenableBuilder(
113 | valueListenable: notifier.select((value) => value.lines
114 | .expand((element) => element.points)
115 | .length),
116 | builder: (context, value, child) =>
117 | Text("Simplification:\n($value points)"),
118 | ),
119 | Expanded(
120 | child: ValueListenableBuilder(
121 | valueListenable: notifier
122 | .select((value) => value.simplificationTolerance),
123 | builder: (context, value, child) => Slider(
124 | value: value,
125 | max: 10,
126 | onChanged: notifier.setSimplificationTolerance,
127 | label: "${value.toStringAsFixed(2)} px",
128 | divisions: 100,
129 | ),
130 | ),
131 | ),
132 | TextButton(
133 | onPressed: notifier.simplify,
134 | child: const Text("Simplify"),
135 | ),
136 | ],
137 | ),
138 | ],
139 | ),
140 | )
141 | ],
142 | ),
143 | ),
144 | );
145 | }
146 |
147 | List _buildActions(context) {
148 | return [
149 | ValueListenableBuilder(
150 | valueListenable: notifier,
151 | builder: (context, value, child) => IconButton(
152 | icon: child as Icon,
153 | tooltip: "Undo",
154 | onPressed: notifier.canUndo ? notifier.undo : null,
155 | ),
156 | child: const Icon(Icons.undo),
157 | ),
158 | ValueListenableBuilder(
159 | valueListenable: notifier,
160 | builder: (context, value, child) => IconButton(
161 | icon: child as Icon,
162 | tooltip: "Redo",
163 | onPressed: notifier.canRedo ? notifier.redo : null,
164 | ),
165 | child: const Icon(Icons.redo),
166 | ),
167 | IconButton(
168 | icon: const Icon(Icons.clear),
169 | tooltip: "Clear",
170 | onPressed: notifier.clear,
171 | ),
172 | IconButton(
173 | icon: const Icon(Icons.image),
174 | tooltip: "Show PNG Image",
175 | onPressed: () => _showImage(context),
176 | ),
177 | IconButton(
178 | icon: const Icon(Icons.data_object),
179 | tooltip: "Show JSON",
180 | onPressed: () => _showJson(context),
181 | ),
182 | ];
183 | }
184 |
185 | void _showImage(BuildContext context) async {
186 | final image = notifier.renderImage();
187 | showDialog(
188 | context: context,
189 | builder: (context) => AlertDialog(
190 | title: const Text("Generated Image"),
191 | content: SizedBox.expand(
192 | child: FutureBuilder(
193 | future: image,
194 | builder: (context, snapshot) => snapshot.hasData
195 | ? Image.memory(snapshot.data!.buffer.asUint8List())
196 | : const Center(child: CircularProgressIndicator()),
197 | ),
198 | ),
199 | actions: [
200 | TextButton(
201 | onPressed: Navigator.of(context).pop,
202 | child: const Text("Close"),
203 | )
204 | ],
205 | ),
206 | );
207 | }
208 |
209 | void _showJson(BuildContext context) {
210 | showDialog(
211 | context: context,
212 | builder: (context) => AlertDialog(
213 | title: const Text("Sketch as JSON"),
214 | content: SizedBox.expand(
215 | child: SelectableText(
216 | jsonEncode(notifier.currentSketch.toJson()),
217 | autofocus: true,
218 | ),
219 | ),
220 | actions: [
221 | TextButton(
222 | onPressed: Navigator.of(context).pop,
223 | child: const Text("Close"),
224 | )
225 | ],
226 | ),
227 | );
228 | }
229 |
230 | Widget _buildStrokeToolbar(BuildContext context) {
231 | return ValueListenableBuilder(
232 | valueListenable: notifier,
233 | builder: (context, state, _) => Row(
234 | crossAxisAlignment: CrossAxisAlignment.center,
235 | mainAxisAlignment: MainAxisAlignment.start,
236 | children: [
237 | for (final w in notifier.widths)
238 | _buildStrokeButton(
239 | context,
240 | strokeWidth: w,
241 | state: state,
242 | ),
243 | ],
244 | ),
245 | );
246 | }
247 |
248 | Widget _buildStrokeButton(
249 | BuildContext context, {
250 | required double strokeWidth,
251 | required ScribbleState state,
252 | }) {
253 | final selected = state.selectedWidth == strokeWidth;
254 | return Padding(
255 | padding: const EdgeInsets.all(4),
256 | child: Material(
257 | elevation: selected ? 4 : 0,
258 | shape: const CircleBorder(),
259 | child: InkWell(
260 | onTap: () => notifier.setStrokeWidth(strokeWidth),
261 | customBorder: const CircleBorder(),
262 | child: AnimatedContainer(
263 | duration: kThemeAnimationDuration,
264 | width: strokeWidth * 2,
265 | height: strokeWidth * 2,
266 | decoration: BoxDecoration(
267 | color: state.map(
268 | drawing: (s) => Color(s.selectedColor),
269 | erasing: (_) => Colors.transparent,
270 | ),
271 | border: state.map(
272 | drawing: (_) => null,
273 | erasing: (_) => Border.all(width: 1),
274 | ),
275 | borderRadius: BorderRadius.circular(50.0)),
276 | ),
277 | ),
278 | ),
279 | );
280 | }
281 |
282 | Widget _buildColorToolbar(BuildContext context) {
283 | return Row(
284 | crossAxisAlignment: CrossAxisAlignment.center,
285 | mainAxisAlignment: MainAxisAlignment.start,
286 | children: [
287 | _buildColorButton(context, color: Colors.black),
288 | _buildColorButton(context, color: Colors.red),
289 | _buildColorButton(context, color: Colors.green),
290 | _buildColorButton(context, color: Colors.blue),
291 | _buildColorButton(context, color: Colors.yellow),
292 | _buildEraserButton(context),
293 | ],
294 | );
295 | }
296 |
297 | Widget _buildPointerModeSwitcher(BuildContext context) {
298 | return ValueListenableBuilder(
299 | valueListenable: notifier.select(
300 | (value) => value.allowedPointersMode,
301 | ),
302 | builder: (context, value, child) {
303 | return SegmentedButton(
304 | multiSelectionEnabled: false,
305 | emptySelectionAllowed: false,
306 | onSelectionChanged: (v) => notifier.setAllowedPointersMode(v.first),
307 | segments: const [
308 | ButtonSegment(
309 | value: ScribblePointerMode.all,
310 | icon: Icon(Icons.touch_app),
311 | label: Text("All pointers"),
312 | ),
313 | ButtonSegment(
314 | value: ScribblePointerMode.penOnly,
315 | icon: Icon(Icons.draw),
316 | label: Text("Pen only"),
317 | ),
318 | ],
319 | selected: {value},
320 | );
321 | });
322 | }
323 |
324 | Widget _buildEraserButton(BuildContext context) {
325 | return ValueListenableBuilder(
326 | valueListenable: notifier.select((value) => value is Erasing),
327 | builder: (context, value, child) => ColorButton(
328 | color: Colors.transparent,
329 | outlineColor: Colors.black,
330 | isActive: value,
331 | onPressed: () => notifier.setEraser(),
332 | child: const Icon(Icons.cleaning_services),
333 | ),
334 | );
335 | }
336 |
337 | Widget _buildColorButton(
338 | BuildContext context, {
339 | required Color color,
340 | }) {
341 | return ValueListenableBuilder(
342 | valueListenable: notifier.select((value) =>
343 | value is Drawing && value.selectedColor == color.toARGB32()),
344 | builder: (context, value, child) => Padding(
345 | padding: const EdgeInsets.symmetric(horizontal: 4),
346 | child: ColorButton(
347 | color: color,
348 | isActive: value,
349 | onPressed: () => notifier.setColor(color),
350 | ),
351 | ),
352 | );
353 | }
354 | }
355 |
356 | class ColorButton extends StatelessWidget {
357 | const ColorButton({
358 | required this.color,
359 | required this.isActive,
360 | required this.onPressed,
361 | this.outlineColor,
362 | this.child,
363 | super.key,
364 | });
365 |
366 | final Color color;
367 |
368 | final Color? outlineColor;
369 |
370 | final bool isActive;
371 |
372 | final VoidCallback onPressed;
373 |
374 | final Icon? child;
375 |
376 | @override
377 | Widget build(BuildContext context) {
378 | return AnimatedContainer(
379 | duration: kThemeAnimationDuration,
380 | decoration: ShapeDecoration(
381 | shape: CircleBorder(
382 | side: BorderSide(
383 | color: switch (isActive) {
384 | true => outlineColor ?? color,
385 | false => Colors.transparent,
386 | },
387 | width: 2,
388 | ),
389 | ),
390 | ),
391 | child: IconButton(
392 | style: FilledButton.styleFrom(
393 | backgroundColor: color,
394 | shape: const CircleBorder(),
395 | side: isActive
396 | ? const BorderSide(color: Colors.white, width: 2)
397 | : const BorderSide(color: Colors.transparent),
398 | ),
399 | onPressed: onPressed,
400 | icon: child ?? const SizedBox(),
401 | ),
402 | );
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: example
2 | description: "An example project for scribble."
3 | publish_to: 'none'
4 | version: 0.1.0
5 |
6 | environment:
7 | sdk: '>=3.3.2 <4.0.0'
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | scribble:
13 | path: ..
14 | value_notifier_tools: ^0.1.2
15 |
16 | dev_dependencies:
17 | flutter_test:
18 | sdk: flutter
19 | flutter_lints: ^3.0.0
20 |
21 | flutter:
22 | uses-material-design: true
23 |
--------------------------------------------------------------------------------
/lib/scribble.dart:
--------------------------------------------------------------------------------
1 | /// Scribble is a lightweight library for freehand drawing in Flutter
2 | library scribble;
3 |
4 | export 'package:scribble/src/domain/model/sketch/sketch.dart';
5 | export 'package:scribble/src/view/notifier/scribble_notifier.dart';
6 | export 'package:scribble/src/view/scribble.dart';
7 | export 'package:scribble/src/view/scribble_sketch.dart';
8 | export 'package:scribble/src/view/state/scribble.state.dart';
9 |
--------------------------------------------------------------------------------
/lib/src/domain/iterable_removed_x.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 |
3 | /// An extension on [Iterable] that provides a method to get the indices of the
4 | /// elements that were removed from this iterable in order.
5 | extension IterableRemovedX on Iterable {
6 | /// Returns the indices of the elements that were removed from this iterable
7 | /// in order.
8 | Iterable removedIndices(Iterable other) sync* {
9 | assert(other.length <= length, "The 'after' iterable can't be longer");
10 | assert(
11 | other.every(contains),
12 | "The 'after iterable can't contain elements "
13 | "that are not in this iterable",
14 | );
15 |
16 | final before = Queue.from(this);
17 | final after = Queue.from(other);
18 |
19 | while (before.isNotEmpty) {
20 | if (after.isNotEmpty && before.first == after.first) {
21 | before.removeFirst();
22 | after.removeFirst();
23 | } else {
24 | yield length - before.length;
25 | before.removeFirst();
26 | }
27 | }
28 | }
29 |
30 | /// Returns an iterable without the elements at the given [indices].
31 | Iterable withRemovedIndices(Set indices) sync* {
32 | for (var i = 0; i < length; i++) {
33 | if (indices.contains(i) == false) {
34 | yield elementAt(i);
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/src/domain/model/point/point.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'point.freezed.dart';
4 | part 'point.g.dart';
5 |
6 | /// {@template point}
7 | /// Represents a point in a sketch with an x and y coordinate and an optional
8 | /// pressure value.
9 | /// {@endtemplate}
10 | @Freezed()
11 | class Point with _$Point {
12 | /// {@macro point}
13 | const factory Point(
14 | double x,
15 | double y, {
16 | @Default(0.5) double pressure,
17 | }) = _Point;
18 | const Point._();
19 |
20 | /// Constructs a point from a JSON object.
21 | factory Point.fromJson(Map json) => _$PointFromJson(json);
22 | }
23 |
--------------------------------------------------------------------------------
/lib/src/domain/model/point/point.freezed.dart:
--------------------------------------------------------------------------------
1 | // coverage:ignore-file
2 | // GENERATED CODE - DO NOT MODIFY BY HAND
3 | // ignore_for_file: type=lint
4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
5 |
6 | part of 'point.dart';
7 |
8 | // **************************************************************************
9 | // FreezedGenerator
10 | // **************************************************************************
11 |
12 | T _$identity(T value) => value;
13 |
14 | final _privateConstructorUsedError = UnsupportedError(
15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
16 |
17 | Point _$PointFromJson(Map json) {
18 | return _Point.fromJson(json);
19 | }
20 |
21 | /// @nodoc
22 | mixin _$Point {
23 | double get x => throw _privateConstructorUsedError;
24 | double get y => throw _privateConstructorUsedError;
25 | double get pressure => throw _privateConstructorUsedError;
26 |
27 | /// Serializes this Point to a JSON map.
28 | Map toJson() => throw _privateConstructorUsedError;
29 |
30 | /// Create a copy of Point
31 | /// with the given fields replaced by the non-null parameter values.
32 | @JsonKey(includeFromJson: false, includeToJson: false)
33 | $PointCopyWith get copyWith => throw _privateConstructorUsedError;
34 | }
35 |
36 | /// @nodoc
37 | abstract class $PointCopyWith<$Res> {
38 | factory $PointCopyWith(Point value, $Res Function(Point) then) =
39 | _$PointCopyWithImpl<$Res, Point>;
40 | @useResult
41 | $Res call({double x, double y, double pressure});
42 | }
43 |
44 | /// @nodoc
45 | class _$PointCopyWithImpl<$Res, $Val extends Point>
46 | implements $PointCopyWith<$Res> {
47 | _$PointCopyWithImpl(this._value, this._then);
48 |
49 | // ignore: unused_field
50 | final $Val _value;
51 | // ignore: unused_field
52 | final $Res Function($Val) _then;
53 |
54 | /// Create a copy of Point
55 | /// with the given fields replaced by the non-null parameter values.
56 | @pragma('vm:prefer-inline')
57 | @override
58 | $Res call({
59 | Object? x = null,
60 | Object? y = null,
61 | Object? pressure = null,
62 | }) {
63 | return _then(_value.copyWith(
64 | x: null == x
65 | ? _value.x
66 | : x // ignore: cast_nullable_to_non_nullable
67 | as double,
68 | y: null == y
69 | ? _value.y
70 | : y // ignore: cast_nullable_to_non_nullable
71 | as double,
72 | pressure: null == pressure
73 | ? _value.pressure
74 | : pressure // ignore: cast_nullable_to_non_nullable
75 | as double,
76 | ) as $Val);
77 | }
78 | }
79 |
80 | /// @nodoc
81 | abstract class _$$PointImplCopyWith<$Res> implements $PointCopyWith<$Res> {
82 | factory _$$PointImplCopyWith(
83 | _$PointImpl value, $Res Function(_$PointImpl) then) =
84 | __$$PointImplCopyWithImpl<$Res>;
85 | @override
86 | @useResult
87 | $Res call({double x, double y, double pressure});
88 | }
89 |
90 | /// @nodoc
91 | class __$$PointImplCopyWithImpl<$Res>
92 | extends _$PointCopyWithImpl<$Res, _$PointImpl>
93 | implements _$$PointImplCopyWith<$Res> {
94 | __$$PointImplCopyWithImpl(
95 | _$PointImpl _value, $Res Function(_$PointImpl) _then)
96 | : super(_value, _then);
97 |
98 | /// Create a copy of Point
99 | /// with the given fields replaced by the non-null parameter values.
100 | @pragma('vm:prefer-inline')
101 | @override
102 | $Res call({
103 | Object? x = null,
104 | Object? y = null,
105 | Object? pressure = null,
106 | }) {
107 | return _then(_$PointImpl(
108 | null == x
109 | ? _value.x
110 | : x // ignore: cast_nullable_to_non_nullable
111 | as double,
112 | null == y
113 | ? _value.y
114 | : y // ignore: cast_nullable_to_non_nullable
115 | as double,
116 | pressure: null == pressure
117 | ? _value.pressure
118 | : pressure // ignore: cast_nullable_to_non_nullable
119 | as double,
120 | ));
121 | }
122 | }
123 |
124 | /// @nodoc
125 | @JsonSerializable()
126 | class _$PointImpl extends _Point {
127 | const _$PointImpl(this.x, this.y, {this.pressure = 0.5}) : super._();
128 |
129 | factory _$PointImpl.fromJson(Map json) =>
130 | _$$PointImplFromJson(json);
131 |
132 | @override
133 | final double x;
134 | @override
135 | final double y;
136 | @override
137 | @JsonKey()
138 | final double pressure;
139 |
140 | @override
141 | String toString() {
142 | return 'Point(x: $x, y: $y, pressure: $pressure)';
143 | }
144 |
145 | @override
146 | bool operator ==(Object other) {
147 | return identical(this, other) ||
148 | (other.runtimeType == runtimeType &&
149 | other is _$PointImpl &&
150 | (identical(other.x, x) || other.x == x) &&
151 | (identical(other.y, y) || other.y == y) &&
152 | (identical(other.pressure, pressure) ||
153 | other.pressure == pressure));
154 | }
155 |
156 | @JsonKey(includeFromJson: false, includeToJson: false)
157 | @override
158 | int get hashCode => Object.hash(runtimeType, x, y, pressure);
159 |
160 | /// Create a copy of Point
161 | /// with the given fields replaced by the non-null parameter values.
162 | @JsonKey(includeFromJson: false, includeToJson: false)
163 | @override
164 | @pragma('vm:prefer-inline')
165 | _$$PointImplCopyWith<_$PointImpl> get copyWith =>
166 | __$$PointImplCopyWithImpl<_$PointImpl>(this, _$identity);
167 |
168 | @override
169 | Map toJson() {
170 | return _$$PointImplToJson(
171 | this,
172 | );
173 | }
174 | }
175 |
176 | abstract class _Point extends Point {
177 | const factory _Point(final double x, final double y,
178 | {final double pressure}) = _$PointImpl;
179 | const _Point._() : super._();
180 |
181 | factory _Point.fromJson(Map json) = _$PointImpl.fromJson;
182 |
183 | @override
184 | double get x;
185 | @override
186 | double get y;
187 | @override
188 | double get pressure;
189 |
190 | /// Create a copy of Point
191 | /// with the given fields replaced by the non-null parameter values.
192 | @override
193 | @JsonKey(includeFromJson: false, includeToJson: false)
194 | _$$PointImplCopyWith<_$PointImpl> get copyWith =>
195 | throw _privateConstructorUsedError;
196 | }
197 |
--------------------------------------------------------------------------------
/lib/src/domain/model/point/point.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'point.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$PointImpl _$$PointImplFromJson(Map json) => _$PointImpl(
10 | (json['x'] as num).toDouble(),
11 | (json['y'] as num).toDouble(),
12 | pressure: (json['pressure'] as num?)?.toDouble() ?? 0.5,
13 | );
14 |
15 | Map _$$PointImplToJson(_$PointImpl instance) =>
16 | {
17 | 'x': instance.x,
18 | 'y': instance.y,
19 | 'pressure': instance.pressure,
20 | };
21 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch/sketch.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 | import 'package:scribble/src/domain/model/sketch_line/sketch_line.dart';
3 |
4 | export '../point/point.dart';
5 | export '../sketch_line/sketch_line.dart';
6 |
7 | part 'sketch.freezed.dart';
8 | part 'sketch.g.dart';
9 |
10 | /// Represents a sketch with a list of [SketchLine]s.
11 | @freezed
12 | class Sketch with _$Sketch {
13 | /// Represents a sketch with a list of [SketchLine]s.
14 | const factory Sketch({
15 | required List lines,
16 | }) = _Sketch;
17 |
18 | /// Constructs a sketch from a JSON object.
19 | factory Sketch.fromJson(Map json) => _$SketchFromJson(json);
20 | }
21 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch/sketch.freezed.dart:
--------------------------------------------------------------------------------
1 | // coverage:ignore-file
2 | // GENERATED CODE - DO NOT MODIFY BY HAND
3 | // ignore_for_file: type=lint
4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
5 |
6 | part of 'sketch.dart';
7 |
8 | // **************************************************************************
9 | // FreezedGenerator
10 | // **************************************************************************
11 |
12 | T _$identity(T value) => value;
13 |
14 | final _privateConstructorUsedError = UnsupportedError(
15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
16 |
17 | Sketch _$SketchFromJson(Map json) {
18 | return _Sketch.fromJson(json);
19 | }
20 |
21 | /// @nodoc
22 | mixin _$Sketch {
23 | List get lines => throw _privateConstructorUsedError;
24 |
25 | /// Serializes this Sketch to a JSON map.
26 | Map toJson() => throw _privateConstructorUsedError;
27 |
28 | /// Create a copy of Sketch
29 | /// with the given fields replaced by the non-null parameter values.
30 | @JsonKey(includeFromJson: false, includeToJson: false)
31 | $SketchCopyWith get copyWith => throw _privateConstructorUsedError;
32 | }
33 |
34 | /// @nodoc
35 | abstract class $SketchCopyWith<$Res> {
36 | factory $SketchCopyWith(Sketch value, $Res Function(Sketch) then) =
37 | _$SketchCopyWithImpl<$Res, Sketch>;
38 | @useResult
39 | $Res call({List lines});
40 | }
41 |
42 | /// @nodoc
43 | class _$SketchCopyWithImpl<$Res, $Val extends Sketch>
44 | implements $SketchCopyWith<$Res> {
45 | _$SketchCopyWithImpl(this._value, this._then);
46 |
47 | // ignore: unused_field
48 | final $Val _value;
49 | // ignore: unused_field
50 | final $Res Function($Val) _then;
51 |
52 | /// Create a copy of Sketch
53 | /// with the given fields replaced by the non-null parameter values.
54 | @pragma('vm:prefer-inline')
55 | @override
56 | $Res call({
57 | Object? lines = null,
58 | }) {
59 | return _then(_value.copyWith(
60 | lines: null == lines
61 | ? _value.lines
62 | : lines // ignore: cast_nullable_to_non_nullable
63 | as List,
64 | ) as $Val);
65 | }
66 | }
67 |
68 | /// @nodoc
69 | abstract class _$$SketchImplCopyWith<$Res> implements $SketchCopyWith<$Res> {
70 | factory _$$SketchImplCopyWith(
71 | _$SketchImpl value, $Res Function(_$SketchImpl) then) =
72 | __$$SketchImplCopyWithImpl<$Res>;
73 | @override
74 | @useResult
75 | $Res call({List lines});
76 | }
77 |
78 | /// @nodoc
79 | class __$$SketchImplCopyWithImpl<$Res>
80 | extends _$SketchCopyWithImpl<$Res, _$SketchImpl>
81 | implements _$$SketchImplCopyWith<$Res> {
82 | __$$SketchImplCopyWithImpl(
83 | _$SketchImpl _value, $Res Function(_$SketchImpl) _then)
84 | : super(_value, _then);
85 |
86 | /// Create a copy of Sketch
87 | /// with the given fields replaced by the non-null parameter values.
88 | @pragma('vm:prefer-inline')
89 | @override
90 | $Res call({
91 | Object? lines = null,
92 | }) {
93 | return _then(_$SketchImpl(
94 | lines: null == lines
95 | ? _value._lines
96 | : lines // ignore: cast_nullable_to_non_nullable
97 | as List,
98 | ));
99 | }
100 | }
101 |
102 | /// @nodoc
103 | @JsonSerializable()
104 | class _$SketchImpl implements _Sketch {
105 | const _$SketchImpl({required final List lines}) : _lines = lines;
106 |
107 | factory _$SketchImpl.fromJson(Map json) =>
108 | _$$SketchImplFromJson(json);
109 |
110 | final List _lines;
111 | @override
112 | List get lines {
113 | if (_lines is EqualUnmodifiableListView) return _lines;
114 | // ignore: implicit_dynamic_type
115 | return EqualUnmodifiableListView(_lines);
116 | }
117 |
118 | @override
119 | String toString() {
120 | return 'Sketch(lines: $lines)';
121 | }
122 |
123 | @override
124 | bool operator ==(Object other) {
125 | return identical(this, other) ||
126 | (other.runtimeType == runtimeType &&
127 | other is _$SketchImpl &&
128 | const DeepCollectionEquality().equals(other._lines, _lines));
129 | }
130 |
131 | @JsonKey(includeFromJson: false, includeToJson: false)
132 | @override
133 | int get hashCode =>
134 | Object.hash(runtimeType, const DeepCollectionEquality().hash(_lines));
135 |
136 | /// Create a copy of Sketch
137 | /// with the given fields replaced by the non-null parameter values.
138 | @JsonKey(includeFromJson: false, includeToJson: false)
139 | @override
140 | @pragma('vm:prefer-inline')
141 | _$$SketchImplCopyWith<_$SketchImpl> get copyWith =>
142 | __$$SketchImplCopyWithImpl<_$SketchImpl>(this, _$identity);
143 |
144 | @override
145 | Map toJson() {
146 | return _$$SketchImplToJson(
147 | this,
148 | );
149 | }
150 | }
151 |
152 | abstract class _Sketch implements Sketch {
153 | const factory _Sketch({required final List lines}) = _$SketchImpl;
154 |
155 | factory _Sketch.fromJson(Map json) = _$SketchImpl.fromJson;
156 |
157 | @override
158 | List get lines;
159 |
160 | /// Create a copy of Sketch
161 | /// with the given fields replaced by the non-null parameter values.
162 | @override
163 | @JsonKey(includeFromJson: false, includeToJson: false)
164 | _$$SketchImplCopyWith<_$SketchImpl> get copyWith =>
165 | throw _privateConstructorUsedError;
166 | }
167 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch/sketch.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'sketch.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$SketchImpl _$$SketchImplFromJson(Map json) => _$SketchImpl(
10 | lines: (json['lines'] as List)
11 | .map((e) => SketchLine.fromJson(e as Map))
12 | .toList(),
13 | );
14 |
15 | Map _$$SketchImplToJson(_$SketchImpl instance) =>
16 | {
17 | 'lines': instance.lines.map((e) => e.toJson()).toList(),
18 | };
19 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch_line/sketch_line.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 | import 'package:scribble/src/domain/model/point/point.dart';
3 |
4 | part 'sketch_line.freezed.dart';
5 | part 'sketch_line.g.dart';
6 |
7 | /// {@template sketch_line}
8 | /// Represents a line in a sketch.
9 | /// {@endtemplate}
10 | @freezed
11 | class SketchLine with _$SketchLine {
12 | /// {@macro sketch_line}
13 | const factory SketchLine({
14 | /// The points that make up the line
15 | required List points,
16 |
17 | /// The color of the line in hexadecimal format (ARGB)
18 | required int color,
19 |
20 | /// The width of the line
21 | required double width,
22 | }) = _SketchLine;
23 |
24 | /// Constructs a sketch line from a JSON object.
25 | factory SketchLine.fromJson(Map json) =>
26 | _$SketchLineFromJson(json);
27 | }
28 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch_line/sketch_line.freezed.dart:
--------------------------------------------------------------------------------
1 | // coverage:ignore-file
2 | // GENERATED CODE - DO NOT MODIFY BY HAND
3 | // ignore_for_file: type=lint
4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
5 |
6 | part of 'sketch_line.dart';
7 |
8 | // **************************************************************************
9 | // FreezedGenerator
10 | // **************************************************************************
11 |
12 | T _$identity(T value) => value;
13 |
14 | final _privateConstructorUsedError = UnsupportedError(
15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
16 |
17 | SketchLine _$SketchLineFromJson(Map json) {
18 | return _SketchLine.fromJson(json);
19 | }
20 |
21 | /// @nodoc
22 | mixin _$SketchLine {
23 | /// The points that make up the line
24 | List get points => throw _privateConstructorUsedError;
25 |
26 | /// The color of the line in hexadecimal format (ARGB)
27 | int get color => throw _privateConstructorUsedError;
28 |
29 | /// The width of the line
30 | double get width => throw _privateConstructorUsedError;
31 |
32 | /// Serializes this SketchLine to a JSON map.
33 | Map toJson() => throw _privateConstructorUsedError;
34 |
35 | /// Create a copy of SketchLine
36 | /// with the given fields replaced by the non-null parameter values.
37 | @JsonKey(includeFromJson: false, includeToJson: false)
38 | $SketchLineCopyWith get copyWith =>
39 | throw _privateConstructorUsedError;
40 | }
41 |
42 | /// @nodoc
43 | abstract class $SketchLineCopyWith<$Res> {
44 | factory $SketchLineCopyWith(
45 | SketchLine value, $Res Function(SketchLine) then) =
46 | _$SketchLineCopyWithImpl<$Res, SketchLine>;
47 | @useResult
48 | $Res call({List points, int color, double width});
49 | }
50 |
51 | /// @nodoc
52 | class _$SketchLineCopyWithImpl<$Res, $Val extends SketchLine>
53 | implements $SketchLineCopyWith<$Res> {
54 | _$SketchLineCopyWithImpl(this._value, this._then);
55 |
56 | // ignore: unused_field
57 | final $Val _value;
58 | // ignore: unused_field
59 | final $Res Function($Val) _then;
60 |
61 | /// Create a copy of SketchLine
62 | /// with the given fields replaced by the non-null parameter values.
63 | @pragma('vm:prefer-inline')
64 | @override
65 | $Res call({
66 | Object? points = null,
67 | Object? color = null,
68 | Object? width = null,
69 | }) {
70 | return _then(_value.copyWith(
71 | points: null == points
72 | ? _value.points
73 | : points // ignore: cast_nullable_to_non_nullable
74 | as List,
75 | color: null == color
76 | ? _value.color
77 | : color // ignore: cast_nullable_to_non_nullable
78 | as int,
79 | width: null == width
80 | ? _value.width
81 | : width // ignore: cast_nullable_to_non_nullable
82 | as double,
83 | ) as $Val);
84 | }
85 | }
86 |
87 | /// @nodoc
88 | abstract class _$$SketchLineImplCopyWith<$Res>
89 | implements $SketchLineCopyWith<$Res> {
90 | factory _$$SketchLineImplCopyWith(
91 | _$SketchLineImpl value, $Res Function(_$SketchLineImpl) then) =
92 | __$$SketchLineImplCopyWithImpl<$Res>;
93 | @override
94 | @useResult
95 | $Res call({List points, int color, double width});
96 | }
97 |
98 | /// @nodoc
99 | class __$$SketchLineImplCopyWithImpl<$Res>
100 | extends _$SketchLineCopyWithImpl<$Res, _$SketchLineImpl>
101 | implements _$$SketchLineImplCopyWith<$Res> {
102 | __$$SketchLineImplCopyWithImpl(
103 | _$SketchLineImpl _value, $Res Function(_$SketchLineImpl) _then)
104 | : super(_value, _then);
105 |
106 | /// Create a copy of SketchLine
107 | /// with the given fields replaced by the non-null parameter values.
108 | @pragma('vm:prefer-inline')
109 | @override
110 | $Res call({
111 | Object? points = null,
112 | Object? color = null,
113 | Object? width = null,
114 | }) {
115 | return _then(_$SketchLineImpl(
116 | points: null == points
117 | ? _value._points
118 | : points // ignore: cast_nullable_to_non_nullable
119 | as List,
120 | color: null == color
121 | ? _value.color
122 | : color // ignore: cast_nullable_to_non_nullable
123 | as int,
124 | width: null == width
125 | ? _value.width
126 | : width // ignore: cast_nullable_to_non_nullable
127 | as double,
128 | ));
129 | }
130 | }
131 |
132 | /// @nodoc
133 | @JsonSerializable()
134 | class _$SketchLineImpl implements _SketchLine {
135 | const _$SketchLineImpl(
136 | {required final List points,
137 | required this.color,
138 | required this.width})
139 | : _points = points;
140 |
141 | factory _$SketchLineImpl.fromJson(Map json) =>
142 | _$$SketchLineImplFromJson(json);
143 |
144 | /// The points that make up the line
145 | final List _points;
146 |
147 | /// The points that make up the line
148 | @override
149 | List get points {
150 | if (_points is EqualUnmodifiableListView) return _points;
151 | // ignore: implicit_dynamic_type
152 | return EqualUnmodifiableListView(_points);
153 | }
154 |
155 | /// The color of the line in hexadecimal format (ARGB)
156 | @override
157 | final int color;
158 |
159 | /// The width of the line
160 | @override
161 | final double width;
162 |
163 | @override
164 | String toString() {
165 | return 'SketchLine(points: $points, color: $color, width: $width)';
166 | }
167 |
168 | @override
169 | bool operator ==(Object other) {
170 | return identical(this, other) ||
171 | (other.runtimeType == runtimeType &&
172 | other is _$SketchLineImpl &&
173 | const DeepCollectionEquality().equals(other._points, _points) &&
174 | (identical(other.color, color) || other.color == color) &&
175 | (identical(other.width, width) || other.width == width));
176 | }
177 |
178 | @JsonKey(includeFromJson: false, includeToJson: false)
179 | @override
180 | int get hashCode => Object.hash(
181 | runtimeType, const DeepCollectionEquality().hash(_points), color, width);
182 |
183 | /// Create a copy of SketchLine
184 | /// with the given fields replaced by the non-null parameter values.
185 | @JsonKey(includeFromJson: false, includeToJson: false)
186 | @override
187 | @pragma('vm:prefer-inline')
188 | _$$SketchLineImplCopyWith<_$SketchLineImpl> get copyWith =>
189 | __$$SketchLineImplCopyWithImpl<_$SketchLineImpl>(this, _$identity);
190 |
191 | @override
192 | Map toJson() {
193 | return _$$SketchLineImplToJson(
194 | this,
195 | );
196 | }
197 | }
198 |
199 | abstract class _SketchLine implements SketchLine {
200 | const factory _SketchLine(
201 | {required final List points,
202 | required final int color,
203 | required final double width}) = _$SketchLineImpl;
204 |
205 | factory _SketchLine.fromJson(Map json) =
206 | _$SketchLineImpl.fromJson;
207 |
208 | /// The points that make up the line
209 | @override
210 | List get points;
211 |
212 | /// The color of the line in hexadecimal format (ARGB)
213 | @override
214 | int get color;
215 |
216 | /// The width of the line
217 | @override
218 | double get width;
219 |
220 | /// Create a copy of SketchLine
221 | /// with the given fields replaced by the non-null parameter values.
222 | @override
223 | @JsonKey(includeFromJson: false, includeToJson: false)
224 | _$$SketchLineImplCopyWith<_$SketchLineImpl> get copyWith =>
225 | throw _privateConstructorUsedError;
226 | }
227 |
--------------------------------------------------------------------------------
/lib/src/domain/model/sketch_line/sketch_line.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'sketch_line.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$SketchLineImpl _$$SketchLineImplFromJson(Map json) =>
10 | _$SketchLineImpl(
11 | points: (json['points'] as List)
12 | .map((e) => Point.fromJson(e as Map))
13 | .toList(),
14 | color: (json['color'] as num).toInt(),
15 | width: (json['width'] as num).toDouble(),
16 | );
17 |
18 | Map _$$SketchLineImplToJson(_$SketchLineImpl instance) =>
19 | {
20 | 'points': instance.points.map((e) => e.toJson()).toList(),
21 | 'color': instance.color,
22 | 'width': instance.width,
23 | };
24 |
--------------------------------------------------------------------------------
/lib/src/view/notifier/scribble_notifier.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui' as ui;
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/gestures.dart';
5 | import 'package:flutter/rendering.dart';
6 | import 'package:flutter/widgets.dart';
7 | import 'package:scribble/scribble.dart';
8 | import 'package:scribble/src/view/painting/point_to_offset_x.dart';
9 | import 'package:scribble/src/view/simplification/sketch_simplifier.dart';
10 | import 'package:value_notifier_tools/value_notifier_tools.dart';
11 |
12 | /// {@template scribble_notifier_base}
13 | /// The base class for a notifier that controls the state of a [Scribble]
14 | /// widget.
15 | ///
16 | /// This class is meant to be extended by a concrete implementation that
17 | /// provides the actual behavior.
18 | ///
19 | /// See [ScribbleNotifier] for the default implementation.
20 | /// {@endtemplate}
21 | abstract class ScribbleNotifierBase extends ValueNotifier {
22 | /// {@macro scribble_notifier_base}
23 | ScribbleNotifierBase(super.state);
24 |
25 | /// You need to provide a key that the [RepaintBoundary] can use so you can
26 | /// access it from the [renderImage] method.
27 | GlobalKey get repaintBoundaryKey;
28 |
29 | /// Should be called when the pointer hovers over the canvas with the
30 | /// corresponding [event].
31 | void onPointerHover(PointerHoverEvent event);
32 |
33 | /// Should be called when the pointer is pressed down on the canvas with the
34 | /// corresponding [event].
35 | void onPointerDown(PointerDownEvent event);
36 |
37 | /// Should be called when the pointer is moved on the canvas with the
38 | /// corresponding [event].
39 | void onPointerUpdate(PointerMoveEvent event);
40 |
41 | /// Should be called when the pointer is lifted from the canvas with the
42 | /// corresponding [event].
43 | void onPointerUp(PointerUpEvent event);
44 |
45 | /// Should be called when the pointer is canceled with the corresponding
46 | /// [event].
47 | void onPointerCancel(PointerCancelEvent event);
48 |
49 | /// Should be called when the pointer exits the canvas with the corresponding
50 | /// [event].
51 | void onPointerExit(PointerExitEvent event);
52 |
53 | /// Used to render the image to ByteData which can then be stored or reused
54 | /// for example in an [Image.memory] widget.
55 | ///
56 | /// Use [pixelRatio] to increase the resolution of the resulting image.
57 | /// You can specify a different [format], by default this method
58 | /// generates pngs.
59 | Future renderImage({
60 | double pixelRatio = 1.0,
61 | ui.ImageByteFormat format = ui.ImageByteFormat.png,
62 | }) async {
63 | final renderObject = repaintBoundaryKey.currentContext?.findRenderObject()
64 | as RenderRepaintBoundary?;
65 | if (renderObject == null) {
66 | throw StateError(
67 | "Tried to convert Scribble to Image, but no valid RenderObject was "
68 | "found!",
69 | );
70 | }
71 | final img = await renderObject.toImage(pixelRatio: pixelRatio);
72 | return (await img.toByteData(format: format))!;
73 | }
74 | }
75 |
76 | /// {@template scribble_notifier}
77 | /// The default implementation of a [ScribbleNotifierBase].
78 | ///
79 | /// This class controls the state and behavior for a [Scribble] widget.
80 | /// {@endtemplate}
81 | class ScribbleNotifier extends ScribbleNotifierBase
82 | with HistoryValueNotifierMixin {
83 | /// {@macro scribble_notifier}
84 | ScribbleNotifier({
85 | /// If you pass a sketch here, the notifier will use that sketch as a
86 | /// starting point.
87 | Sketch? sketch,
88 |
89 | /// Which pointers can be drawn with and are captured.
90 | ScribblePointerMode allowedPointersMode = ScribblePointerMode.all,
91 |
92 | /// How many states you want stored in the undo history, 30 by default.
93 | int maxHistoryLength = 30,
94 | this.widths = const [5, 10, 15],
95 | this.pressureCurve = Curves.linear,
96 | this.simplifier = const VisvalingamSimplifier(),
97 |
98 | /// {@macro view.state.scribble_state.simplification_tolerance}
99 | double simplificationTolerance = 0,
100 | }) : super(
101 | ScribbleState.drawing(
102 | sketch: switch (sketch) {
103 | Sketch() => simplifier.simplifySketch(
104 | sketch,
105 | pixelTolerance: simplificationTolerance,
106 | ),
107 | null => const Sketch(lines: []),
108 | },
109 | selectedWidth: widths[0],
110 | allowedPointersMode: allowedPointersMode,
111 | simplificationTolerance: simplificationTolerance,
112 | ),
113 | ) {
114 | this.maxHistoryLength = maxHistoryLength;
115 | }
116 |
117 | /// The supported widths, mainly useful for rendering UI, you can still set
118 | /// the width to any arbitrary value from code.
119 | ///
120 | /// The first entry in this list will be the starting width.
121 | final List widths;
122 |
123 | /// The curve that's used to map pen pressure to the pressure value when
124 | /// recording, by default it's linear.
125 | final Curve pressureCurve;
126 |
127 | /// The state of the sketch at this moment.
128 | ///
129 | /// If you want to store it somewhere you can call ``.toJson()`` on it to
130 | /// receive a map.
131 | Sketch get currentSketch => value.sketch;
132 |
133 | final GlobalKey _repaintBoundaryKey = GlobalKey();
134 |
135 | @override
136 | GlobalKey get repaintBoundaryKey => _repaintBoundaryKey;
137 |
138 | /// The [SketchSimplifier] that is used to simplify the lines of the sketch.
139 | ///
140 | /// Defaults to [VisvalingamSimplifier], but you can implement your own.
141 | final SketchSimplifier simplifier;
142 |
143 | /// Only apply the sketch from the undo history, otherwise keep current state
144 | @override
145 | @protected
146 | ScribbleState transformHistoryValue(
147 | ScribbleState historyValue,
148 | ScribbleState currentState,
149 | ) {
150 | return currentState.copyWith(
151 | sketch: historyValue.sketch,
152 | );
153 | }
154 |
155 | /// Can be used to update the state of the Sketch externally (e.g. when
156 | /// fetching from a server) to what is passed in as [sketch];
157 | ///
158 | /// By default, this state of the sketch gets added to the undo history. If
159 | /// this is not desired, set [addToUndoHistory] to `false`.
160 | ///
161 | /// The sketch will be simplified using the currently set simplification
162 | /// tolerance. If you don't want simplification, call
163 | /// [setSimplificationTolerance] to set it to 0.
164 | void setSketch({
165 | required Sketch sketch,
166 | bool addToUndoHistory = true,
167 | }) {
168 | final newState = value.copyWith(
169 | sketch: sketch,
170 | );
171 | if (addToUndoHistory) {
172 | value = newState;
173 | } else {
174 | temporaryValue = newState;
175 | }
176 | }
177 |
178 | /// Clear the entire drawing.
179 | void clear() {
180 | value = switch (value) {
181 | final Drawing d => d.copyWith(
182 | sketch: const Sketch(lines: []),
183 | activeLine: null,
184 | ),
185 | final Erasing e => e.copyWith(
186 | sketch: const Sketch(lines: []),
187 | ),
188 | };
189 | }
190 |
191 | /// Sets the width of the next line
192 | void setStrokeWidth(double strokeWidth) {
193 | temporaryValue = value.copyWith(
194 | selectedWidth: strokeWidth,
195 | );
196 | }
197 |
198 | /// Switches to eraser mode
199 | void setEraser() {
200 | temporaryValue = ScribbleState.erasing(
201 | sketch: value.sketch,
202 | selectedWidth: value.selectedWidth,
203 | scaleFactor: value.scaleFactor,
204 | allowedPointersMode: value.allowedPointersMode,
205 | activePointerIds: value.activePointerIds,
206 | );
207 | }
208 |
209 | /// Sets the current mode of allowed pointers to the given
210 | /// [ScribblePointerMode]
211 | void setAllowedPointersMode(ScribblePointerMode allowedPointersMode) {
212 | temporaryValue = value.copyWith(
213 | allowedPointersMode: allowedPointersMode,
214 | );
215 | }
216 |
217 | /// Sets the zoom factor to allow for adjusting line width.
218 | ///
219 | /// If the factor is 2 for example, lines will be drawn half as thick as
220 | /// actually selected to allow for drawing details. Has to be greater than 0.
221 | void setScaleFactor(double factor) {
222 | assert(factor > 0, "The scale factor must be greater than 0.");
223 | temporaryValue = value.copyWith(
224 | scaleFactor: factor,
225 | );
226 | }
227 |
228 | /// Sets the color of the pen to the given color.
229 | void setColor(Color color) {
230 | temporaryValue = value.map(
231 | drawing: (s) => ScribbleState.drawing(
232 | sketch: s.sketch,
233 | selectedColor: color.toARGB32(),
234 | selectedWidth: s.selectedWidth,
235 | allowedPointersMode: s.allowedPointersMode,
236 | ),
237 | erasing: (s) => ScribbleState.drawing(
238 | sketch: s.sketch,
239 | selectedColor: color.toARGB32(),
240 | selectedWidth: s.selectedWidth,
241 | allowedPointersMode: s.allowedPointersMode,
242 | scaleFactor: value.scaleFactor,
243 | activePointerIds: value.activePointerIds,
244 | ),
245 | );
246 | }
247 |
248 | /// Sets the simplification degree for the sketch in logical pixels.
249 | ///
250 | /// 0 means no simplification, 1px is a good starting point for most sketches.
251 | /// The higher the degree, the more the details will be eroded.
252 | ///
253 | /// **Info:** Simplification quickly breaks simulated pressure, since it
254 | /// removes points that are close together first, so pressure simulation
255 | /// assumes a more even speed of the pen.
256 | ///
257 | /// Changing this value by itself will only affect future lines. If you want
258 | /// to simplify existing lines, see [simplify].
259 | void setSimplificationTolerance(double degree) {
260 | temporaryValue = value.copyWith(
261 | simplificationTolerance: degree,
262 | );
263 | }
264 |
265 | /// Simplifies the current sketch to the current simplification degree using
266 | /// [simplifier].
267 | ///
268 | /// This will simplify all lines. If [addToUndoHistory] is true, this step
269 | /// will be added to the undo history
270 | void simplify({bool addToUndoHistory = true}) {
271 | final newSketch = simplifier.simplifySketch(
272 | value.sketch,
273 | pixelTolerance: value.simplificationTolerance,
274 | );
275 | if (addToUndoHistory) {
276 | value = value.copyWith(sketch: newSketch);
277 | } else {
278 | temporaryValue = value.copyWith(sketch: newSketch);
279 | }
280 | }
281 |
282 | /// Used by the Listener callback to display the pen if desired
283 | @override
284 | void onPointerHover(PointerHoverEvent event) {
285 | if (!value.supportedPointerKinds.contains(event.kind)) return;
286 | temporaryValue = value.copyWith(
287 | pointerPosition:
288 | event.distance > 10000 ? null : _getPointFromEvent(event),
289 | );
290 | }
291 |
292 | /// Used by the Listener callback to start drawing
293 | @override
294 | void onPointerDown(PointerDownEvent event) {
295 | if (!value.supportedPointerKinds.contains(event.kind)) return;
296 | var s = value;
297 |
298 | // Are there already pointers on the screen?
299 | if (value.activePointerIds.isNotEmpty) {
300 | s = value.map(
301 | drawing: (s) =>
302 | // If the current line already contains something
303 | (s.activeLine != null && s.activeLine!.points.length > 2)
304 | ? _finishLineForState(s)
305 | : s.copyWith(
306 | activeLine: null,
307 | ),
308 | erasing: (s) => s,
309 | );
310 | } else if (value is Drawing) {
311 | s = (value as Drawing).copyWith(
312 | pointerPosition: _getPointFromEvent(event),
313 | activeLine: SketchLine(
314 | points: [_getPointFromEvent(event)],
315 | color: (value as Drawing).selectedColor,
316 | width: value.selectedWidth / value.scaleFactor,
317 | ),
318 | );
319 | }
320 | temporaryValue = s.copyWith(
321 | activePointerIds: [...value.activePointerIds, event.pointer],
322 | );
323 | }
324 |
325 | /// Used by the Listener callback to update the drawing
326 | @override
327 | void onPointerUpdate(PointerMoveEvent event) {
328 | if (!value.supportedPointerKinds.contains(event.kind)) return;
329 | if (!value.active) {
330 | temporaryValue = value.copyWith(
331 | pointerPosition: null,
332 | );
333 | return;
334 | }
335 | if (value is Drawing) {
336 | temporaryValue = _addPoint(event, value).copyWith(
337 | pointerPosition: _getPointFromEvent(event),
338 | );
339 | } else if (value is Erasing) {
340 | final erasedState = _erasePoint(event);
341 | // Check if content was actually erased
342 | if (erasedState != null) {
343 | // Content was actually erased, add to undo stack
344 | value = erasedState.copyWith(
345 | pointerPosition: _getPointFromEvent(event),
346 | );
347 | } else {
348 | // No content erased, only update pointer position
349 | temporaryValue = value.copyWith(
350 | pointerPosition: _getPointFromEvent(event),
351 | );
352 | }
353 | }
354 | }
355 |
356 | /// Used by the Listener callback to finish a line
357 | @override
358 | void onPointerUp(PointerUpEvent event) {
359 | if (!value.supportedPointerKinds.contains(event.kind)) return;
360 | final pos =
361 | event.kind == PointerDeviceKind.mouse ? value.pointerPosition : null;
362 | if (value is Drawing) {
363 | value = _finishLineForState(_addPoint(event, value)).copyWith(
364 | pointerPosition: pos,
365 | activePointerIds:
366 | value.activePointerIds.where((id) => id != event.pointer).toList(),
367 | );
368 | } else if (value is Erasing) {
369 | final erasedState = _erasePoint(event);
370 | // Only update value when content was actually erased (affects undo stack)
371 | if (erasedState != null) {
372 | value = erasedState.copyWith(
373 | pointerPosition: pos,
374 | activePointerIds: value.activePointerIds
375 | .where((id) => id != event.pointer)
376 | .toList(),
377 | );
378 | } else {
379 | // No content erased, only update pointer position
380 | temporaryValue = value.copyWith(
381 | pointerPosition: pos,
382 | activePointerIds: value.activePointerIds
383 | .where((id) => id != event.pointer)
384 | .toList(),
385 | );
386 | }
387 | }
388 | }
389 |
390 | /// Used by the Listener callback to stop displaying the cursor
391 | @override
392 | void onPointerCancel(PointerCancelEvent event) {
393 | if (!value.supportedPointerKinds.contains(event.kind)) return;
394 | if (value is Drawing) {
395 | value = _finishLineForState(_addPoint(event, value)).copyWith(
396 | pointerPosition: null,
397 | activePointerIds:
398 | value.activePointerIds.where((id) => id != event.pointer).toList(),
399 | );
400 | } else if (value is Erasing) {
401 | final erasedState = _erasePoint(event);
402 | // Only update value when content was actually erased (affects undo stack)
403 | if (erasedState != null) {
404 | value = erasedState.copyWith(
405 | pointerPosition: null,
406 | activePointerIds: value.activePointerIds
407 | .where((id) => id != event.pointer)
408 | .toList(),
409 | );
410 | } else {
411 | // No content erased, only update pointer position
412 | temporaryValue = value.copyWith(
413 | pointerPosition: null,
414 | activePointerIds: value.activePointerIds
415 | .where((id) => id != event.pointer)
416 | .toList(),
417 | );
418 | }
419 | }
420 | }
421 |
422 | @override
423 | void onPointerExit(PointerExitEvent event) {
424 | if (!value.supportedPointerKinds.contains(event.kind)) return;
425 | temporaryValue = _finishLineForState(value).copyWith(
426 | pointerPosition: null,
427 | activePointerIds:
428 | value.activePointerIds.where((id) => id != event.pointer).toList(),
429 | );
430 | }
431 |
432 | ScribbleState _addPoint(PointerEvent event, ScribbleState s) {
433 | if (s is Erasing || !s.active) return s;
434 | if (s is Drawing && s.activeLine == null) return s;
435 | final currentLine = (s as Drawing).activeLine!;
436 | final distanceToLast = currentLine.points.isEmpty
437 | ? double.infinity
438 | : (currentLine.points.last.asOffset - event.localPosition).distance;
439 | if (distanceToLast <= kPrecisePointerPanSlop / s.scaleFactor) return s;
440 | return s.copyWith(
441 | activeLine: currentLine.copyWith(
442 | points: [
443 | ...currentLine.points,
444 | _getPointFromEvent(event),
445 | ],
446 | ),
447 | );
448 | }
449 |
450 | ScribbleState? _erasePoint(PointerEvent event) {
451 | final filteredLines = value.sketch.lines
452 | .where(
453 | (l) => l.points.every(
454 | (p) =>
455 | (event.localPosition - p.asOffset).distance >
456 | l.width + value.selectedWidth,
457 | ),
458 | )
459 | .toList();
460 | // If no lines were erased, return null to avoid unnecessary state updates
461 | if (filteredLines.length == value.sketch.lines.length) {
462 | return null;
463 | }
464 |
465 | return value.copyWith(
466 | sketch: value.sketch.copyWith(
467 | lines: filteredLines,
468 | ),
469 | );
470 | }
471 |
472 | /// Converts a pointer event to the [Point] on the canvas.
473 | Point _getPointFromEvent(PointerEvent event) {
474 | final p = event.pressureMin == event.pressureMax
475 | ? 0.5
476 | : (event.pressure - event.pressureMin) /
477 | (event.pressureMax - event.pressureMin);
478 | return Point(
479 | event.localPosition.dx,
480 | event.localPosition.dy,
481 | pressure: pressureCurve.transform(p),
482 | );
483 | }
484 |
485 | ScribbleState _finishLineForState(ScribbleState s) {
486 | if (s case Drawing(activeLine: final activeLine?)) {
487 | return s.copyWith(
488 | activeLine: null,
489 | sketch: s.sketch.copyWith(
490 | lines: [
491 | ...s.sketch.lines,
492 | simplifier.simplify(
493 | activeLine,
494 | pixelTolerance: s.simplificationTolerance,
495 | ),
496 | ],
497 | ),
498 | );
499 | }
500 | return s;
501 | }
502 | }
503 |
--------------------------------------------------------------------------------
/lib/src/view/painting/point_to_offset_x.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:scribble/src/domain/model/sketch/sketch.dart';
4 |
5 | /// Extension on [Point] to convert it to an [Offset].
6 | extension PointToOffsetX on Point {
7 | /// Converts a [Point] to a [Offset].
8 | Offset get asOffset => Offset(x, y);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/src/view/painting/scribble_editing_painter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/rendering.dart';
2 | import 'package:scribble/scribble.dart';
3 | import 'package:scribble/src/view/painting/point_to_offset_x.dart';
4 | import 'package:scribble/src/view/painting/sketch_line_path_mixin.dart';
5 |
6 | /// {@template scribble_editing_painter}
7 | /// A painter for drawing the currently active line of a scribble sketch, as
8 | /// well as the pointer when in drawing or erasing mode, if desired.
9 | /// {@endtemplate}
10 | class ScribbleEditingPainter extends CustomPainter with SketchLinePathMixin {
11 | /// {@macro scribble_editing_painter}
12 | ScribbleEditingPainter({
13 | required this.state,
14 | required this.drawPointer,
15 | required this.drawEraser,
16 | required this.simulatePressure,
17 | });
18 |
19 | /// The current state of the scribble sketch
20 | final ScribbleState state;
21 |
22 | /// Whether to draw the pointer when in drawing mode.
23 | ///
24 | /// The pointer will be drawn as a filled circle with the currently selected
25 | /// color.
26 | final bool drawPointer;
27 |
28 | /// Whether to draw the pointer when in erasing mode
29 | ///
30 | /// The pointer will be drawn as a transparent circle with a black border.
31 | final bool drawEraser;
32 |
33 | @override
34 | final bool simulatePressure;
35 |
36 | @override
37 | void paint(Canvas canvas, Size size) {
38 | final paint = Paint()..style = PaintingStyle.fill;
39 |
40 | final activeLine = state.map(
41 | drawing: (s) => s.activeLine,
42 | erasing: (_) => null,
43 | );
44 | if (activeLine != null) {
45 | final path = getPathForLine(
46 | activeLine,
47 | scaleFactor: state.scaleFactor,
48 | );
49 | if (path != null) {
50 | paint.color = Color(activeLine.color);
51 | canvas.drawPath(path, paint);
52 | }
53 | }
54 |
55 | if (state.pointerPosition != null &&
56 | (state is Drawing && drawPointer || state is Erasing && drawEraser)) {
57 | paint
58 | ..style = state.map(
59 | drawing: (_) => PaintingStyle.fill,
60 | erasing: (_) => PaintingStyle.stroke,
61 | )
62 | ..color = state.map(
63 | drawing: (s) => Color(s.selectedColor),
64 | erasing: (s) => const Color(0xFF000000),
65 | )
66 | ..strokeWidth = 1;
67 | canvas.drawCircle(
68 | state.pointerPosition!.asOffset,
69 | state.selectedWidth / state.scaleFactor,
70 | paint,
71 | );
72 | }
73 | }
74 |
75 | @override
76 | bool shouldRepaint(ScribbleEditingPainter oldDelegate) {
77 | return oldDelegate.state != state ||
78 | oldDelegate.simulatePressure != simulatePressure;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/src/view/painting/scribble_painter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/rendering.dart';
2 | import 'package:scribble/scribble.dart';
3 | import 'package:scribble/src/view/painting/sketch_line_path_mixin.dart';
4 |
5 | /// A painter for drawing a scribble sketch.
6 | class ScribblePainter extends CustomPainter with SketchLinePathMixin {
7 | /// Creates a new [ScribblePainter] instance.
8 | ScribblePainter({
9 | required this.sketch,
10 | required this.scaleFactor,
11 | required this.simulatePressure,
12 | });
13 |
14 | /// The [Sketch] to draw.
15 | final Sketch sketch;
16 |
17 | /// {@macro view.state.scribble_state.scale_factor}
18 | final double scaleFactor;
19 |
20 | @override
21 | final bool simulatePressure;
22 |
23 | @override
24 | void paint(Canvas canvas, Size size) {
25 | final paint = Paint()..style = PaintingStyle.fill;
26 |
27 | for (var i = 0; i < sketch.lines.length; ++i) {
28 | final path = getPathForLine(
29 | sketch.lines[i],
30 | scaleFactor: scaleFactor,
31 | );
32 | if (path == null) {
33 | continue;
34 | }
35 | paint.color = Color(sketch.lines[i].color);
36 | canvas.drawPath(path, paint);
37 | }
38 | }
39 |
40 | @override
41 | bool shouldRepaint(ScribblePainter oldDelegate) {
42 | return oldDelegate.sketch != sketch ||
43 | oldDelegate.simulatePressure != simulatePressure ||
44 | oldDelegate.scaleFactor != scaleFactor;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/src/view/painting/sketch_line_path_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:perfect_freehand/perfect_freehand.dart' as pf;
4 | import 'package:scribble/src/domain/model/sketch/sketch.dart';
5 |
6 | /// A mixin for generating a [Path] from a [SketchLine].
7 | ///
8 | /// Provides the method [getPathForLine] which generates a smooth [Path] from a
9 | /// [SketchLine].
10 | mixin SketchLinePathMixin {
11 | /// {@macro scribble.simulate_pressure}
12 | bool get simulatePressure;
13 |
14 | /// Generates a [Path] from a [SketchLine].
15 | ///
16 | /// The [scaleFactor] is used to scale the line width.
17 | ///
18 | /// If [simulatePressure] is true, the line will be drawn as if it had
19 | /// pressure information, if all its points have the same pressure.
20 | Path? getPathForLine(
21 | SketchLine line, {
22 | double scaleFactor = 1.0,
23 | }) {
24 | final needSimulate = simulatePressure &&
25 | line.points.length > 1 &&
26 | line.points.every((p) => p.pressure == line.points.first.pressure);
27 | final points = line.points
28 | .map((point) => pf.PointVector(point.x, point.y, point.pressure))
29 | .toList();
30 | final outlinePoints = pf.getStroke(
31 | points,
32 | options: pf.StrokeOptions(
33 | size: line.width * 2 * scaleFactor,
34 | simulatePressure: needSimulate,
35 | ),
36 | );
37 | if (outlinePoints.isEmpty) {
38 | return null;
39 | } else if (outlinePoints.length < 2) {
40 | return Path()
41 | ..addOval(
42 | Rect.fromCircle(
43 | center: Offset(outlinePoints[0].dx, outlinePoints[0].dy),
44 | radius: 1,
45 | ),
46 | );
47 | } else {
48 | final path = Path()..moveTo(outlinePoints[0].dx, outlinePoints[0].dy);
49 |
50 | for (var i = 1; i < outlinePoints.length - 1; i++) {
51 | final p0 = outlinePoints[i];
52 | final p1 = outlinePoints[i + 1];
53 | path.quadraticBezierTo(
54 | p0.dx,
55 | p0.dy,
56 | (p0.dx + p1.dx) / 2,
57 | (p0.dy + p1.dy) / 2,
58 | );
59 | }
60 | return path;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/src/view/pan_gesture_catcher.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/gestures.dart';
2 | import 'package:flutter/widgets.dart';
3 |
4 | /// {@template gesture_catcher}
5 | /// A widget that catches gestures for a given set of pointer kinds.
6 | ///
7 | /// Any gestures of pointers contained in [pointerKindsToCatch] will be caught
8 | /// by this widget and not be passed to any other widgets in the widget tree.
9 | /// {@endtemplate}
10 | class GestureCatcher extends StatelessWidget {
11 | /// {@macro gesture_catcher}
12 | const GestureCatcher({
13 | required this.pointerKindsToCatch,
14 | required this.child,
15 | super.key,
16 | });
17 |
18 | /// The pointer kinds to catch.
19 | final Set pointerKindsToCatch;
20 |
21 | /// The child widget.
22 | final Widget child;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return RawGestureDetector(
27 | key: ValueKey(pointerKindsToCatch),
28 | gestures: {
29 | _GestureCatcherRecognizer:
30 | GestureRecognizerFactoryWithHandlers<_GestureCatcherRecognizer>(
31 | () => _GestureCatcherRecognizer(
32 | debugOwner: this,
33 | pointerKindsToCatch: pointerKindsToCatch,
34 | ),
35 | (_GestureCatcherRecognizer instance) {},
36 | ),
37 | },
38 | child: child,
39 | );
40 | }
41 | }
42 |
43 | class _GestureCatcherRecognizer extends OneSequenceGestureRecognizer {
44 | /// Create a gesture recognizer for tracking movement on a plane.
45 | _GestureCatcherRecognizer({
46 | required Set pointerKindsToCatch,
47 | super.debugOwner,
48 | }) : super(supportedDevices: pointerKindsToCatch);
49 |
50 | @override
51 | String get debugDescription => 'pan catcher';
52 |
53 | @override
54 | void didStopTrackingLastPointer(int pointer) {}
55 |
56 | @override
57 | void handleEvent(PointerEvent event) {
58 | resolve(GestureDisposition.accepted);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/src/view/scribble.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:flutter/gestures.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:scribble/src/view/notifier/scribble_notifier.dart';
6 | import 'package:scribble/src/view/painting/scribble_editing_painter.dart';
7 | import 'package:scribble/src/view/painting/scribble_painter.dart';
8 | import 'package:scribble/src/view/pan_gesture_catcher.dart';
9 | import 'package:scribble/src/view/state/scribble.state.dart';
10 |
11 | /// {@template scribble}
12 | /// This Widget represents a canvas on which users can draw with any pointer.
13 | ///
14 | /// You can control its behavior from code using the [notifier] instance you
15 | /// pass in.
16 | /// {@endtemplate}
17 | class Scribble extends StatelessWidget {
18 | /// {@macro scribble}
19 | const Scribble({
20 | /// The notifier that controls this canvas.
21 | required this.notifier,
22 |
23 | /// Whether to draw the pointer when in drawing mode
24 | this.drawPen = true,
25 |
26 | /// Whether to draw the pointer when in erasing mode
27 | this.drawEraser = true,
28 | this.simulatePressure = true,
29 | super.key,
30 | });
31 |
32 | /// The notifier that controls this canvas.
33 | final ScribbleNotifierBase notifier;
34 |
35 | /// Whether to draw the pointer when in drawing mode
36 | final bool drawPen;
37 |
38 | /// Whether to draw the pointer when in erasing mode
39 | final bool drawEraser;
40 |
41 | /// {@template scribble.simulate_pressure}
42 | /// Whether to simulate pressure when drawing lines that don't have pressure
43 | /// information (all points have the same pressure).
44 | ///
45 | /// Defaults to `true`.
46 | /// {@endtemplate}
47 | final bool simulatePressure;
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | return ValueListenableBuilder(
52 | valueListenable: notifier,
53 | builder: (context, state, _) {
54 | final drawCurrentTool =
55 | drawPen && state is Drawing || drawEraser && state is Erasing;
56 | final child = SizedBox.expand(
57 | child: CustomPaint(
58 | foregroundPainter: ScribbleEditingPainter(
59 | state: state,
60 | drawPointer: drawPen,
61 | drawEraser: drawEraser,
62 | simulatePressure: simulatePressure,
63 | ),
64 | child: RepaintBoundary(
65 | key: notifier.repaintBoundaryKey,
66 | child: CustomPaint(
67 | painter: ScribblePainter(
68 | sketch: state.sketch,
69 | scaleFactor: state.scaleFactor,
70 | simulatePressure: simulatePressure,
71 | ),
72 | ),
73 | ),
74 | ),
75 | );
76 | return !state.active
77 | ? child
78 | : GestureCatcher(
79 | pointerKindsToCatch: state.supportedPointerKinds,
80 | child: MouseRegion(
81 | cursor: drawCurrentTool &&
82 | state.supportedPointerKinds
83 | .contains(PointerDeviceKind.mouse)
84 | ? SystemMouseCursors.none
85 | : MouseCursor.defer,
86 | onExit: notifier.onPointerExit,
87 | child: Listener(
88 | onPointerDown: notifier.onPointerDown,
89 | onPointerMove: notifier.onPointerUpdate,
90 | onPointerUp: notifier.onPointerUp,
91 | onPointerHover: notifier.onPointerHover,
92 | onPointerCancel: notifier.onPointerCancel,
93 | child: child,
94 | ),
95 | ),
96 | );
97 | },
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib/src/view/scribble_sketch.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:scribble/src/domain/model/sketch/sketch.dart';
3 | import 'package:scribble/src/view/painting/scribble_painter.dart';
4 |
5 | /// {@template scribble_sketch}
6 | /// A widget for displaying a scribble sketch without any input functionalities.
7 | ///
8 | /// The sketch is expected to not have any active line, i.e. all lines are
9 | /// considered finished, the sketch is complete.
10 | /// {@endtemplate}
11 | class ScribbleSketch extends StatelessWidget {
12 | /// {@macro scribble_sketch}
13 | const ScribbleSketch({
14 | required this.sketch,
15 | this.scaleFactor = 1,
16 | this.simulatePressure = true,
17 | super.key,
18 | });
19 |
20 | /// The sketch to display
21 | final Sketch sketch;
22 |
23 | /// How much the widget is scaled at the moment.
24 | ///
25 | /// Can be used if zoom functionality is needed
26 | /// (e.g. through InteractiveViewer) so that the pen width remains the same.
27 | final double scaleFactor;
28 |
29 | /// {@macro scribble.simulate_pressure}
30 | final bool simulatePressure;
31 |
32 | @override
33 | Widget build(BuildContext context) {
34 | return CustomPaint(
35 | painter: ScribblePainter(
36 | sketch: sketch,
37 | scaleFactor: scaleFactor,
38 | simulatePressure: simulatePressure,
39 | ),
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/src/view/simplification/sketch_simplifier.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 | import 'package:scribble/scribble.dart';
3 | import 'package:scribble/src/domain/iterable_removed_x.dart';
4 | import 'package:simpli/simpli.dart';
5 |
6 | /// {@template sketch_simplifier}
7 | /// An interface for simplifying a sketch and it's lines.
8 | /// {@endtemplate}
9 | abstract class SketchSimplifier {
10 | /// {@macro sketch_simplifier}
11 | const SketchSimplifier();
12 |
13 | /// Simplifies the given list of points.
14 | SketchLine simplify(SketchLine line, {required double pixelTolerance});
15 |
16 | /// Simplifies an entire sketch by simplifying each line in the sketch.
17 | Sketch simplifySketch(Sketch sketch, {required double pixelTolerance}) {
18 | if (pixelTolerance == 0) return sketch;
19 | return sketch.copyWith(
20 | lines: [
21 | for (final l in sketch.lines)
22 | simplify(l, pixelTolerance: pixelTolerance),
23 | ],
24 | );
25 | }
26 | }
27 |
28 | /// {@template visvalingam_simplifier}
29 | /// A [SketchSimplifier], that uses the Visvalingam algorithm to simplify a list
30 | /// of points.
31 | /// {@endtemplate}
32 | class VisvalingamSimplifier extends SketchSimplifier {
33 | /// {@macro visvalingam_simplifier}
34 | const VisvalingamSimplifier();
35 |
36 | @override
37 | SketchLine simplify(SketchLine line, {required double pixelTolerance}) {
38 | if (pixelTolerance == 0) return line;
39 |
40 | final mathPoints = line.points.map((e) => math.Point(e.x, e.y)).toList();
41 |
42 | final simplified = Simpli.visvalingam(
43 | mathPoints,
44 | pixelTolerance: pixelTolerance,
45 | );
46 | final removedIndices = mathPoints.removedIndices(simplified.cast());
47 | return line.copyWith(
48 | points: line.points.withRemovedIndices(removedIndices.toSet()).toList(),
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/src/view/state/scribble.state.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:freezed_annotation/freezed_annotation.dart';
4 | import 'package:scribble/src/domain/model/sketch/sketch.dart';
5 |
6 | part 'scribble.state.freezed.dart';
7 |
8 | part 'scribble.state.g.dart';
9 |
10 | /// Which pointers are allowed for drawing and will be captured by the scribble
11 | /// widget.
12 | enum ScribblePointerMode {
13 | /// Allow drawing with all pointers.
14 | all,
15 |
16 | /// Allow drawing with mouse only.
17 | mouseOnly,
18 |
19 | /// Allow drawing with pen only.
20 | ///
21 | /// This is useful if you want to place the scribble widget in an
22 | /// `InteractiveViewer` for example, so that it can be zoomed in and out
23 | /// without drawing on it.
24 | penOnly,
25 |
26 | /// Allow drawing with both mouse and pen.
27 | mouseAndPen,
28 | }
29 |
30 | /// Represents the state of the scribble widget.
31 | @freezed
32 | sealed class ScribbleState with _$ScribbleState {
33 | /// The state of the scribble widget when it is being drawn on.
34 | const factory ScribbleState.drawing({
35 | /// The current state of the sketch
36 | required Sketch sketch,
37 |
38 | /// The line that is currently being drawn
39 | SketchLine? activeLine,
40 |
41 | /// Which pointers are allowed for drawing and will be captured by the
42 | /// scribble widget.
43 | @Default(ScribblePointerMode.all) ScribblePointerMode allowedPointersMode,
44 |
45 | /// The ids of all supported pointers that are currently interacting with
46 | /// the widget.
47 | @Default([]) List activePointerIds,
48 |
49 | /// The current position of the pointer
50 | Point? pointerPosition,
51 |
52 | /// The color that is currently being drawn with
53 | @Default(0xFF000000) int selectedColor,
54 |
55 | /// The current width of the pen
56 | @Default(5) double selectedWidth,
57 |
58 | /// {@template view.state.scribble_state.scale_factor}
59 | /// How much the widget is scaled at the moment.
60 | ///
61 | /// Can be used if zoom functionality is needed
62 | /// (e.g. through InteractiveViewer) so that the pen width remains the same.
63 | /// {@endtemplate}
64 | @Default(1) double scaleFactor,
65 |
66 | /// {@template view.state.scribble_state.simplification_tolerance}
67 | /// The current tolerance of simplification, in pixels.
68 | ///
69 | /// Lines will be simplified when they are finished. A value of 0 (default)
70 | /// will mean no simplification.
71 | /// {@endtemplate}
72 | @Default(0) double simplificationTolerance,
73 | }) = Drawing;
74 |
75 | /// The state of the scribble widget when the user is currently erasing.
76 | const factory ScribbleState.erasing({
77 | /// The current state of the sketch
78 | required Sketch sketch,
79 |
80 | /// Which pointers are allowed for drawing and will be captured by the
81 | /// scribble widget.
82 | @Default(ScribblePointerMode.all) ScribblePointerMode allowedPointersMode,
83 |
84 | /// The ids of all supported pointers that are currently interacting with
85 | /// the widget.
86 | @Default([]) List activePointerIds,
87 |
88 | /// The current position of the pointer
89 | Point? pointerPosition,
90 |
91 | /// The current width of the pen
92 | @Default(5) double selectedWidth,
93 |
94 | /// How much the widget is scaled at the moment.
95 | ///
96 | /// Can be used if zoom functionality is needed
97 | /// (e.g. through InteractiveViewer) so that the pen width remains the same.
98 | @Default(1) double scaleFactor,
99 |
100 | /// The current tolerance of simplification, in pixels.
101 | ///
102 | /// Lines will be simplified when they are finished. A value of 0 (default)
103 | /// will mean no simplification.
104 | @Default(0) double simplificationTolerance,
105 | }) = Erasing;
106 |
107 | /// Constructs a [ScribbleState] from a JSON object.
108 | factory ScribbleState.fromJson(Map json) =>
109 | _$ScribbleStateFromJson(json);
110 | const ScribbleState._();
111 |
112 | /// Returns whether the widget is currently active, meaning that only one
113 | /// pointer is interacting with the widget.
114 | bool get active => activePointerIds.length <= 1;
115 |
116 | /// Returns the list of lines that should be drawn on the canvas by
117 | /// combining the sketches lines with the current active line if it exists.
118 | List get lines => map(
119 | drawing: (d) => d.activeLine == null
120 | ? sketch.lines
121 | : [...sketch.lines, d.activeLine!],
122 | erasing: (d) => d.sketch.lines,
123 | );
124 |
125 | /// Returns a set of [PointerDeviceKind] that represents the currently
126 | /// supported devices, depending on [ScribbleState.allowedPointersMode].
127 | Set get supportedPointerKinds {
128 | switch (allowedPointersMode) {
129 | case ScribblePointerMode.all:
130 | return Set.from(PointerDeviceKind.values);
131 | case ScribblePointerMode.mouseOnly:
132 | return const {PointerDeviceKind.mouse};
133 | case ScribblePointerMode.penOnly:
134 | return const {
135 | PointerDeviceKind.stylus,
136 | PointerDeviceKind.invertedStylus,
137 | };
138 | case ScribblePointerMode.mouseAndPen:
139 | return const {
140 | PointerDeviceKind.mouse,
141 | PointerDeviceKind.stylus,
142 | PointerDeviceKind.invertedStylus,
143 | };
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/lib/src/view/state/scribble.state.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'scribble.state.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$DrawingImpl _$$DrawingImplFromJson(Map json) =>
10 | _$DrawingImpl(
11 | sketch: Sketch.fromJson(json['sketch'] as Map),
12 | activeLine: json['activeLine'] == null
13 | ? null
14 | : SketchLine.fromJson(json['activeLine'] as Map),
15 | allowedPointersMode: $enumDecodeNullable(
16 | _$ScribblePointerModeEnumMap, json['allowedPointersMode']) ??
17 | ScribblePointerMode.all,
18 | activePointerIds: (json['activePointerIds'] as List?)
19 | ?.map((e) => (e as num).toInt())
20 | .toList() ??
21 | const [],
22 | pointerPosition: json['pointerPosition'] == null
23 | ? null
24 | : Point.fromJson(json['pointerPosition'] as Map),
25 | selectedColor: (json['selectedColor'] as num?)?.toInt() ?? 0xFF000000,
26 | selectedWidth: (json['selectedWidth'] as num?)?.toDouble() ?? 5,
27 | scaleFactor: (json['scaleFactor'] as num?)?.toDouble() ?? 1,
28 | simplificationTolerance:
29 | (json['simplificationTolerance'] as num?)?.toDouble() ?? 0,
30 | $type: json['runtimeType'] as String?,
31 | );
32 |
33 | Map _$$DrawingImplToJson(_$DrawingImpl instance) =>
34 | {
35 | 'sketch': instance.sketch.toJson(),
36 | 'activeLine': instance.activeLine?.toJson(),
37 | 'allowedPointersMode':
38 | _$ScribblePointerModeEnumMap[instance.allowedPointersMode]!,
39 | 'activePointerIds': instance.activePointerIds,
40 | 'pointerPosition': instance.pointerPosition?.toJson(),
41 | 'selectedColor': instance.selectedColor,
42 | 'selectedWidth': instance.selectedWidth,
43 | 'scaleFactor': instance.scaleFactor,
44 | 'simplificationTolerance': instance.simplificationTolerance,
45 | 'runtimeType': instance.$type,
46 | };
47 |
48 | const _$ScribblePointerModeEnumMap = {
49 | ScribblePointerMode.all: 'all',
50 | ScribblePointerMode.mouseOnly: 'mouseOnly',
51 | ScribblePointerMode.penOnly: 'penOnly',
52 | ScribblePointerMode.mouseAndPen: 'mouseAndPen',
53 | };
54 |
55 | _$ErasingImpl _$$ErasingImplFromJson(Map json) =>
56 | _$ErasingImpl(
57 | sketch: Sketch.fromJson(json['sketch'] as Map),
58 | allowedPointersMode: $enumDecodeNullable(
59 | _$ScribblePointerModeEnumMap, json['allowedPointersMode']) ??
60 | ScribblePointerMode.all,
61 | activePointerIds: (json['activePointerIds'] as List?)
62 | ?.map((e) => (e as num).toInt())
63 | .toList() ??
64 | const [],
65 | pointerPosition: json['pointerPosition'] == null
66 | ? null
67 | : Point.fromJson(json['pointerPosition'] as Map),
68 | selectedWidth: (json['selectedWidth'] as num?)?.toDouble() ?? 5,
69 | scaleFactor: (json['scaleFactor'] as num?)?.toDouble() ?? 1,
70 | simplificationTolerance:
71 | (json['simplificationTolerance'] as num?)?.toDouble() ?? 0,
72 | $type: json['runtimeType'] as String?,
73 | );
74 |
75 | Map _$$ErasingImplToJson(_$ErasingImpl instance) =>
76 | {
77 | 'sketch': instance.sketch.toJson(),
78 | 'allowedPointersMode':
79 | _$ScribblePointerModeEnumMap[instance.allowedPointersMode]!,
80 | 'activePointerIds': instance.activePointerIds,
81 | 'pointerPosition': instance.pointerPosition?.toJson(),
82 | 'selectedWidth': instance.selectedWidth,
83 | 'scaleFactor': instance.scaleFactor,
84 | 'simplificationTolerance': instance.simplificationTolerance,
85 | 'runtimeType': instance.$type,
86 | };
87 |
--------------------------------------------------------------------------------
/melos.yaml:
--------------------------------------------------------------------------------
1 | name: history_value_notifier_workspace
2 |
3 | packages:
4 | - .
5 | - packages/*
6 | - example
7 |
8 | command:
9 | version:
10 | updateGitTagRefs: true
11 | workspaceChangelog: false
12 |
13 | scripts:
14 | analyze:
15 | run: |
16 | dart analyze . --fatal-infos
17 | exec:
18 | # We are setting the concurrency to 1 because a higher concurrency can crash
19 | # the analysis server on low performance machines (like GitHub Actions).
20 | concurrency: 1
21 | description: |
22 | Run `dart analyze` in all packages.
23 | - Note: you can also rely on your IDEs Dart Analysis / Issues window.
24 |
25 | test:select:
26 | run: flutter test
27 | exec:
28 | failFast: true
29 | concurrency: 6
30 | packageFilters:
31 | dirExists: test
32 | description: Run `flutter test test` for selected packages.
33 |
34 | test:
35 | run: melos run test:select --no-select
36 | description: Run all tests in this project.
37 |
38 | coverage:select:
39 | run: |
40 | flutter test --coverage
41 | exec:
42 | failFast: true
43 | concurrency: 6
44 | packageFilters:
45 | dirExists: test
46 | description: Generate coverage for the selected package.
47 |
48 | coverage:
49 | run: melos run coverage:select --no-select
50 | description: Generate coverage for all packages.
51 |
52 | generate:select:
53 | description: Run code generation for selected packages.
54 | run: dart run build_runner build --delete-conflicting-outputs
55 | exec:
56 | concurrency: 1
57 | failFast: true
58 | packageFilters:
59 | dependsOn:
60 | - build_runner
61 |
62 | generate:
63 | description: Run code generation for all packages.
64 | run: melos run generate:select --no-select
--------------------------------------------------------------------------------
/packages/simpli/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | .mason/
12 | migrate_working_dir/
13 |
14 | # IntelliJ related
15 | *.iml
16 | *.ipr
17 | *.iws
18 | .idea/
19 |
20 | # See https://www.dartlang.org/guides/libraries/private-files
21 |
22 | # Files and directories created by pub
23 | .dart_tool/
24 | .packages
25 | build/
26 | pubspec.lock
27 | pubspec_overrides.yaml
--------------------------------------------------------------------------------
/packages/simpli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.1+2
2 |
3 | - **FIX**: remove emoji from pubspec.
4 | - **FIX**: updated meta dependency constraints.
5 |
6 | ## 0.1.1+1
7 |
8 | - **FIX**: updated meta dependency constraints.
9 |
10 | ## 0.1.1
11 |
12 | - **FEAT**: added simpli package (#50).
13 |
14 | ## 0.1.0+1
15 |
16 | - feat: initial commit 🎉
17 |
--------------------------------------------------------------------------------
/packages/simpli/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Tim Lehmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/simpli/README.md:
--------------------------------------------------------------------------------
1 | # Simpli
2 |
3 | [](https://github.com/felangel/mason)
4 | [](https://github.com/invertase/melos)
5 | 
6 |
7 |
8 | Polyline simplification algorithms, made simple! Sporting exciting algorithms such as ✨Ramer-Douglas-Peucker✨ and ✨Visvalingam✨.
9 |
10 | ## Installation 💻
11 |
12 | **❗ In order to start using Simpli you must have the [Dart SDK][dart_install_link] installed on your machine.**
13 |
14 | Install via `dart pub add`:
15 |
16 | ```sh
17 | dart pub add simpli
18 | ```
19 |
20 |
21 | ## Usage 🚀
22 |
23 | ```dart
24 | import 'package:simpli/simpli.dart';
25 |
26 | void main() {
27 | final points = [
28 | Point(0, 0),
29 | Point(1, 1),
30 | Point(2, 0),
31 | Point(3, 3),
32 | Point(4, 0),
33 | ];
34 |
35 | final rdpSimplified = Simpli.ramerDouglasPeucker(points, pixelTolerance: 50.0);
36 | final visvalingamSimplified = Simpli.visvalingam(points, pixelTolerance: 50.0);
37 |
38 | print(simplifiedPoints);
39 | print(visvalingamSimplified);
40 | }
41 | ```
42 |
43 | ---
44 |
45 |
46 | [dart_install_link]: https://dart.dev/get-dart
47 | [github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
48 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
49 | [license_link]: https://opensource.org/licenses/MIT
50 | [mason_link]: https://github.com/felangel/mason
51 | [very_good_ventures_link]: https://verygood.ventures
52 |
--------------------------------------------------------------------------------
/packages/simpli/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lintervention/analysis_options.yaml
2 |
--------------------------------------------------------------------------------
/packages/simpli/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/simpli/coverage/lcov.info:
--------------------------------------------------------------------------------
1 | SF:lib/src/data/visvalingam_simplifier.dart
2 | DA:18,1
3 | DA:20,1
4 | DA:25,3
5 | DA:28,1
6 | DA:29,1
7 | DA:31,2
8 | DA:35,1
9 | DA:36,1
10 | DA:37,5
11 | DA:38,2
12 | DA:42,2
13 | DA:44,1
14 | DA:48,2
15 | DA:53,2
16 | DA:57,1
17 | DA:58,1
18 | DA:59,1
19 | DA:62,1
20 | DA:64,1
21 | DA:67,1
22 | DA:70,1
23 | DA:71,1
24 | DA:77,1
25 | DA:79,4
26 | DA:80,1
27 | DA:82,1
28 | DA:83,2
29 | DA:84,1
30 | DA:85,2
31 | DA:87,1
32 | DA:95,1
33 | DA:102,1
34 | DA:103,1
35 | DA:104,3
36 | DA:109,1
37 | DA:112,1
38 | DA:114,1
39 | DA:115,1
40 | DA:116,1
41 | DA:123,1
42 | DA:124,1
43 | DA:125,1
44 | LF:42
45 | LH:42
46 | end_of_record
47 | SF:lib/src/data/rdp_simplifier.dart
48 | DA:12,1
49 | DA:14,1
50 | DA:19,2
51 | DA:24,4
52 | DA:25,1
53 | DA:26,1
54 | DA:27,1
55 | DA:28,1
56 | DA:30,1
57 | DA:36,4
58 | DA:38,1
59 | DA:39,2
60 | DA:42,1
61 | DA:43,1
62 | DA:47,1
63 | DA:48,3
64 | DA:49,1
65 | DA:50,1
66 | LF:18
67 | LH:18
68 | end_of_record
69 | SF:lib/src/data/utils.dart
70 | DA:10,2
71 | DA:15,16
72 | DA:16,14
73 | DA:17,2
74 | DA:19,20
75 | DA:20,2
76 | DA:24,1
77 | DA:29,17
78 | DA:30,1
79 | DA:31,1
80 | LF:10
81 | LH:10
82 | end_of_record
83 |
--------------------------------------------------------------------------------
/packages/simpli/lib/simpli.dart:
--------------------------------------------------------------------------------
1 | /// Polyline simplification algorithms, made simple!
2 | /// Sporting Ramer-Douglas-Peucker and Visvalingam
3 | library simpli;
4 |
5 | export 'src/domain/simplifier.dart';
6 | export 'src/simpli.dart';
7 |
--------------------------------------------------------------------------------
/packages/simpli/lib/src/data/rdp_simplifier.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/utils.dart';
4 | import 'package:simpli/src/domain/simplifier.dart';
5 |
6 | /// {@template rdp_simplifier}
7 | /// A simplifier that uses the Ramer-Douglas-Peucker algorithm to simplify a
8 | /// list of points.
9 | /// {@endtemplate}
10 | class RdpSimplifier implements Simplifier {
11 | /// {@macro rdp_simplifier}
12 | const RdpSimplifier();
13 |
14 | @override
15 | List> simplify(
16 | List> points, {
17 | double pixelTolerance = 50.0,
18 | }) {
19 | if (points.length < 3) {
20 | return points;
21 | }
22 | var maxDistance = 0.0;
23 | var index = 0;
24 | for (var i = 1; i < points.length - 1; i++) {
25 | final distance = Utils.perpendicularDistance(
26 | point: points[i],
27 | lineStart: points.first,
28 | lineEnd: points.last,
29 | );
30 | if (distance > maxDistance) {
31 | maxDistance = distance;
32 | index = i;
33 | }
34 | }
35 |
36 | if (maxDistance <= pixelTolerance) return [points.first, points.last];
37 |
38 | final firstPart = simplify(
39 | points.sublist(0, index + 1),
40 | pixelTolerance: pixelTolerance,
41 | );
42 | final secondPart = simplify(
43 | points.sublist(index),
44 | pixelTolerance: pixelTolerance,
45 | );
46 |
47 | return [
48 | ...firstPart.sublist(0, firstPart.length - 1),
49 | points[index],
50 | ...secondPart.sublist(1),
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/simpli/lib/src/data/utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | /// A collection of utility functions.
4 | abstract class Utils {
5 | const Utils._();
6 |
7 | /// Calculates the perpendicular distance from a point to a line segment.
8 | ///
9 | /// From https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
10 | static double perpendicularDistance({
11 | required Point point,
12 | required Point lineStart,
13 | required Point lineEnd,
14 | }) {
15 | final numerator = ((lineEnd.x - lineStart.x) * (lineStart.y - point.y) -
16 | (lineStart.x - point.x) * (lineEnd.y - lineStart.y))
17 | .abs();
18 | final denominator =
19 | sqrt(pow(lineEnd.x - lineStart.x, 2) + pow(lineEnd.y - lineStart.y, 2));
20 | return numerator / denominator;
21 | }
22 |
23 | /// Calculates the are of a triangle given its three points.
24 | static double triangleArea({
25 | required Point a,
26 | required Point b,
27 | required Point c,
28 | }) {
29 | return (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y))
30 | .toDouble()
31 | .abs();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/simpli/lib/src/data/visvalingam_simplifier.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:collection/collection.dart';
4 | import 'package:meta/meta.dart';
5 | import 'package:simpli/src/data/utils.dart';
6 | import 'package:simpli/src/domain/simplifier.dart';
7 |
8 | /// A point with an optional area
9 | @visibleForTesting
10 | typedef PointWithArea = (Point, double?);
11 |
12 | /// {@template visvalingam_simplifier}
13 | /// A simplifier that uses the Visvalingam algorithm to simplify a list of
14 | /// points.
15 | /// {@endtemplate}
16 | class VisvalingamSimplifier implements Simplifier {
17 | /// {@macro visvalingam_simplifier}
18 | const VisvalingamSimplifier();
19 |
20 | @override
21 | List> simplify(
22 | List> points, {
23 | double pixelTolerance = 50,
24 | }) {
25 | if (points.length < 3 || pixelTolerance <= 0) {
26 | return points;
27 | }
28 | final simplified = [...points];
29 | final heap = HeapPriorityQueue(
30 | // Points with lower area should be the first to be removed.
31 | (p0, p1) => p0.$2!.compareTo(p1.$2!),
32 | );
33 |
34 | // Contains all points besides the first and last, together with their area
35 | final pointsWithArea = [
36 | (points.first, null),
37 | for (var i = 1; i < points.length - 1; i++) pointWithArea(points, i),
38 | (points.last, null),
39 | ];
40 |
41 | // Add all points that have an area to the heap
42 | for (final p in pointsWithArea) {
43 | if (p.$2 != null) {
44 | heap.add(p);
45 | }
46 | }
47 |
48 | for (var point = heap.pop(); point != null; point = heap.pop()) {
49 | final area = point.$2;
50 | if (area == null) continue;
51 |
52 | // If the points area is larger than the tolerance squared, we can stop.
53 | if (area >= pixelTolerance * pixelTolerance) {
54 | break;
55 | }
56 |
57 | final index = pointsWithArea.indexOf(point);
58 | if (index < 0) continue;
59 | tryRecompute(
60 | pointsWithArea: pointsWithArea,
61 | heap: heap,
62 | index: index - 1,
63 | );
64 | tryRecompute(
65 | pointsWithArea: pointsWithArea,
66 | heap: heap,
67 | index: index + 1,
68 | );
69 |
70 | simplified.removeAt(index);
71 | pointsWithArea.removeAt(index);
72 | }
73 | return simplified;
74 | }
75 |
76 | /// Calculates the area of a point in a list of points.
77 | @visibleForTesting
78 | PointWithArea pointWithArea(List> points, int index) {
79 | if (index == 0 || index >= points.length - 1) {
80 | throw Exception('Index out of bounds for pointWithArea.');
81 | }
82 | final area = Utils.triangleArea(
83 | a: points[index - 1],
84 | b: points[index],
85 | c: points[index + 1],
86 | );
87 | return (points[index], area);
88 | }
89 |
90 | /// Recomputes the area of a point in a list of [pointsWithArea] and updates
91 | /// it in the [heap].
92 | ///
93 | /// This is not a pure function, it will mutate the [pointsWithArea] and
94 | /// [heap].
95 | @visibleForTesting
96 | void tryRecompute({
97 | required List pointsWithArea,
98 | required HeapPriorityQueue heap,
99 | required int index,
100 | }) {
101 | try {
102 | final oldPoint = pointsWithArea[index];
103 | final newPointWithArea = pointWithArea(
104 | pointsWithArea.map((p) => p.$1).toList(),
105 | index,
106 | );
107 | final newPoint = (
108 | newPointWithArea.$1,
109 | max(oldPoint.$2!, newPointWithArea.$2!),
110 | );
111 |
112 | pointsWithArea[index] = newPoint;
113 | heap
114 | ..remove(oldPoint)
115 | ..add(newPoint);
116 | } on Exception {
117 | // Do nothing
118 | }
119 | }
120 | }
121 |
122 | extension on PriorityQueue {
123 | T? pop() {
124 | if (isEmpty) return null;
125 | return removeFirst();
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/packages/simpli/lib/src/domain/simplifier.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/rdp_simplifier.dart';
4 | import 'package:simpli/src/data/visvalingam_simplifier.dart';
5 |
6 | /// Represents a line simplificatoin algorithm.
7 | abstract interface class Simplifier {
8 | const factory Simplifier.ramerDouglasPeucker() = RdpSimplifier;
9 | const factory Simplifier.visvalingam() = VisvalingamSimplifier;
10 |
11 | /// Simplifies a list of points using a sensible default simplification.
12 | ///
13 | /// The [pixelTolerance] parameter is used to determine the tolerance for
14 | /// simplification. A value of 0.0 will result in no simplification.
15 | /// Defaults to 50px.
16 | /// The exact effect of the value will depend on the implementation.
17 | List simplify(
18 | List points, {
19 | double pixelTolerance = 50.0,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/packages/simpli/lib/src/simpli.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/rdp_simplifier.dart';
4 | import 'package:simpli/src/data/visvalingam_simplifier.dart';
5 |
6 | /// {@template simpli}
7 | /// Line simplification algorithms, made simple! Sporting Ramer-Douglas-Peucker
8 | /// and Visvalingam
9 | /// {@endtemplate}
10 | abstract class Simpli {
11 | /// {@macro simpli}
12 | const Simpli._();
13 |
14 | /// Simplifies a list of points using the Ramer-Douglas-Peucker algorithm
15 | /// using the given [pixelTolerance].
16 | static List ramerDouglasPeucker(
17 | List points, {
18 | double pixelTolerance = 50.0,
19 | }) {
20 | return const RdpSimplifier()
21 | .simplify(points, pixelTolerance: pixelTolerance);
22 | }
23 |
24 | /// Simplifies a list of points using the Visvalingam algorithm
25 | /// using the given [pixelTolerance].
26 | static List> visvalingam(
27 | List> points, {
28 | double pixelTolerance = 50.0,
29 | }) {
30 | return const VisvalingamSimplifier()
31 | .simplify(points, pixelTolerance: pixelTolerance);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/simpli/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: simpli
2 | description: Polyline simplification algorithms, made simple! Sporting exciting algorithms such as Ramer-Douglas-Peucker and Visvalingam.
3 | version: 0.1.1+2
4 | repository: https://github.com/timcreatedit/scribble/tree/main/packages/simpli
5 | homepage: https://whynotmake.it
6 |
7 |
8 | environment:
9 | sdk: ">=3.0.0 <4.0.0"
10 |
11 | dependencies:
12 | collection: ">=1.0.0 <2.0.0"
13 | meta: ">= 1.0.0 <2.0.0"
14 |
15 | dev_dependencies:
16 | lintervention: ^0.1.1
17 | mocktail: ^1.0.3
18 | test: ^1.25.2
19 |
20 |
--------------------------------------------------------------------------------
/packages/simpli/test/src/data/rdp_simplifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/rdp_simplifier.dart';
4 | import 'package:test/test.dart';
5 |
6 | void main() {
7 | group('RdpSimplifier', () {
8 | late RdpSimplifier sut;
9 | setUp(() {
10 | sut = const RdpSimplifier();
11 | });
12 |
13 | const path = [
14 | Point(0, 0),
15 | Point(1, 1),
16 | Point(2, 0),
17 | Point(3, 2),
18 | Point(4, 0),
19 | Point(5, 1),
20 | Point(6, 0),
21 | Point(7, 2),
22 | Point(8, 0),
23 | Point(9, 1),
24 | Point(10, 0),
25 | ];
26 |
27 | group('simplify', () {
28 | test('does not simplify when tolerance is zero', () async {
29 | final simplified = sut.simplify(path, pixelTolerance: 0);
30 | expect(simplified, path);
31 | });
32 |
33 | test('does not simplify when path has less than 3 points', () async {
34 | const shortLine = [Point(0, 0), Point(1, 1)];
35 | final simplified = sut.simplify(shortLine, pixelTolerance: 1);
36 | expect(simplified, shortLine);
37 | });
38 |
39 | test('strips all points if tolerance is infinite', () async {
40 | final simplified = sut.simplify(path, pixelTolerance: double.infinity);
41 | expect(simplified, [path.first, path.last]);
42 | });
43 |
44 | test('simplifies path', () async {
45 | final simplified = sut.simplify(path, pixelTolerance: 1);
46 | final pathOnlyOutliers =
47 | path.where((p) => p.y == 2 || p.y == 0).toList();
48 | expect(simplified, pathOnlyOutliers);
49 | });
50 | });
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/packages/simpli/test/src/data/utils_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/utils.dart';
4 | import 'package:test/test.dart';
5 |
6 | void main() {
7 | setUp(() {});
8 |
9 | group('Utils', () {
10 | group('perpendicularDistance', () {
11 | test('0 if point equals start', () async {
12 | final distance = Utils.perpendicularDistance(
13 | point: const Point(0, 0),
14 | lineStart: const Point(0, 0),
15 | lineEnd: const Point(2, 0),
16 | );
17 | expect(distance, 0);
18 | });
19 | test('0 if point equals end', () async {
20 | final distance = Utils.perpendicularDistance(
21 | point: const Point(0, 0),
22 | lineStart: const Point(0, 0),
23 | lineEnd: const Point(2, 0),
24 | );
25 | expect(distance, 0);
26 | });
27 | test('0 if point is on line', () async {
28 | final distance = Utils.perpendicularDistance(
29 | point: const Point(1, 0),
30 | lineStart: const Point(0, 0),
31 | lineEnd: const Point(2, 0),
32 | );
33 | expect(distance, 0);
34 | });
35 |
36 | test('correct distance if point is above line', () async {
37 | final distance = Utils.perpendicularDistance(
38 | point: const Point(1, 1),
39 | lineStart: const Point(0, 0),
40 | lineEnd: const Point(2, 0),
41 | );
42 | expect(distance, 1);
43 | });
44 |
45 | test('correct distance if point is next to line', () async {
46 | final distance = Utils.perpendicularDistance(
47 | point: const Point(4, 0),
48 | lineStart: const Point(0, 0),
49 | lineEnd: const Point(2, 0),
50 | );
51 | expect(distance, 0);
52 | });
53 |
54 | test('correct distance if point is below line', () async {
55 | final distance = Utils.perpendicularDistance(
56 | point: const Point(1, -1),
57 | lineStart: const Point(0, 0),
58 | lineEnd: const Point(2, 0),
59 | );
60 | expect(distance, 1);
61 | });
62 |
63 | test('works for angled lines', () async {
64 | final distance = Utils.perpendicularDistance(
65 | point: const Point(1, 1),
66 | lineStart: const Point(0, 0),
67 | lineEnd: const Point(2, 2),
68 | );
69 | expect(distance, 0);
70 | });
71 | });
72 | });
73 | }
74 |
--------------------------------------------------------------------------------
/packages/simpli/test/src/data/visvalingam_simplifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:simpli/src/data/visvalingam_simplifier.dart';
4 | import 'package:test/test.dart';
5 |
6 | void main() {
7 | group('VisvalingamSimplifier', () {
8 | late VisvalingamSimplifier sut;
9 | setUp(() {
10 | sut = const VisvalingamSimplifier();
11 | });
12 |
13 | const path = [
14 | Point(0, 0),
15 | Point(1, 1),
16 | Point(2, 0),
17 | Point(3, 2),
18 | Point(4, 0),
19 | Point(5, 1),
20 | Point(6, 0),
21 | Point(7, 2),
22 | Point(8, 0),
23 | Point(9, 1),
24 | Point(10, 2),
25 | ];
26 |
27 | group('simplify', () {
28 | test('does not simplify when tolerance == 0', () async {
29 | final simplified = sut.simplify(path, pixelTolerance: 0);
30 | expect(simplified, path);
31 | });
32 |
33 | test('does not simplify when path has less than 3 points', () async {
34 | const shortLine = [Point(0, 0), Point(1, 1)];
35 | final simplified = sut.simplify(shortLine, pixelTolerance: 1);
36 | expect(simplified, shortLine);
37 | });
38 |
39 | test('strips all points if tolerance is infinity', () async {
40 | final simplified = sut.simplify(path, pixelTolerance: double.infinity);
41 | expect(simplified, [path.first, path.last]);
42 | });
43 |
44 | test('simplifies path', () async {
45 | final simplified = sut.simplify(path, pixelTolerance: 1.5);
46 | final pathOnlyOutliers =
47 | path.where((p) => p.y == 2 || p.y == 0).toList();
48 | expect(simplified, pathOnlyOutliers);
49 | });
50 | });
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | .mason/
12 | migrate_working_dir/
13 |
14 | # IntelliJ related
15 | *.iml
16 | *.ipr
17 | *.iws
18 | .idea/
19 |
20 | # See https://www.dartlang.org/guides/libraries/private-files
21 |
22 | # Files and directories created by pub
23 | .dart_tool/
24 | .packages
25 | build/
26 | pubspec.lock
27 | pubspec_overrides.yaml
--------------------------------------------------------------------------------
/packages/value_notifier_tools/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.2
2 |
3 | - **FIX**(value_notifier_tools): fixed type parameters in select extension.
4 | - **FEAT**(example): updated example to use `SelectValueNotifier`.
5 |
6 | ## 0.1.1
7 |
8 | - **FIX**(value_notifier_tools): Added all classes to package exports.
9 | - **FEAT**: use `HistoryValueNotifier` from new package.
10 | - **FEAT**(value_notifier_tools): added `HistoryValueNotifier`.
11 | - **FEAT**(value_notifier_tools): added where_value_notifier.
12 | - **FEAT**(value_notifier_tools): added select value notifier.
13 | - **DOCS**(value_notifier_tools): added README.
14 |
15 | ## 0.1.0+1
16 |
17 | - feat: initial commit 🎉
18 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Tim Lehmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/README.md:
--------------------------------------------------------------------------------
1 | # Value Notifier Tools
2 |
3 | [](https://github.com/felangel/mason)
4 | [](https://github.com/invertase/melos)
5 | 
6 |
7 | Helpful lightweight tools for working with `ValueNotifier`s
8 |
9 | ## Installation 💻
10 |
11 | **❗ In order to start using History Value Notifier you must have the [Dart SDK][dart_install_link] installed on your machine.**
12 |
13 | Install via `dart pub add`:
14 |
15 | ```sh
16 | dart pub add value_notifier_tools
17 | ```
18 |
19 | ## Features
20 | This package adds helpful tools for working with `ValueNotifier`s. Currently, it offers the following:
21 |
22 | * 🕐 [HistoryValueNotifier](#historyvaluenotifier) allows you to undo and redo changes to the state of the notifier. This is useful for implementing undo/redo functionality in your app.
23 | * 🎯 [SelectValueNotifier](#selectvaluenotifier) allows you to select a subset of the state of a `ValueNotifier`. This is useful for when you only want to listen to a specific part of the state of and rebuild a widget when that changes.
24 | * 🔎 [WhereValueNotifier](#wherevaluenotifier) allows you to provide a custom predicate to determine whether a state change should be propagated to listeners. This is useful for when only certain state transitions should cause a rebuild.
25 | * 🪶 No dependencies on any other packages and super lightweight.
26 | * 🧩 Easy to use and integrate into your existing projects.
27 | * 🧪 100% test coverage
28 |
29 |
30 | ## HistoryValueNotifier
31 |
32 | * ↩️ Add `undo()` and `redo()` to `ValueNotifier`
33 | * 🕐 Limit the size of your history
34 | * 💕 Offers both a mixin that can be added to your existing `ValueNotifier`s and a class that you can extend
35 | * 🔎 Choose which states get stored to the history
36 | * 🔄 Transform states before applying them from the history
37 |
38 | ### Usage
39 | Getting started is easy! There are three main ways in which you can add `HistoryValueNotifier` to your project:
40 |
41 | #### Use it as-is
42 | If you don't need any extra functionality, you can use `HistoryValueNotifier` as-is.
43 |
44 | ```dart
45 | import 'package:history_value_notifier/history_value_notifier.dart';
46 |
47 | final notifier = HistoryValueNotifier(0);
48 | notifier.value = 1;
49 | notifier.undo(); // 0
50 | notifier.redo(); // 1
51 | ```
52 |
53 | #### Upgrade an existing `ValueNotifier`
54 |
55 | ```dart
56 | class CounterNotifier extends ValueNotifier
57 | with HistoryValueNotifierMixin {
58 | CounterNotifier() : super(0) {
59 | // This is how you limit the size of your history.
60 | // Set it to null to keep all state (default)
61 | maxHistoryLength = 30;
62 | }
63 |
64 | void increment() => ++state;
65 |
66 | void decrement() => --state;
67 |
68 | // By using temporaryState setter, the change won't be stored in history
69 | void reset() => temporaryState = 0;
70 |
71 | // You can override this function to apply a transformation to a state
72 | // from the history before it gets applied.
73 | @override
74 | int transformHistoryState(int newState, int currentState) {
75 | return newState;
76 | }
77 | }
78 | ```
79 |
80 | #### Create a `HistoryValueNotifier`
81 |
82 | If you prefer to create a `HistoryValueNotifier` directly, you can do this instead:
83 |
84 | ```dart
85 | class CounterNotifier extends HistoryValueNotifier {
86 | // ... Same as above
87 | }
88 | ```
89 |
90 | #### Use It!
91 |
92 | You can now use the full functionality of the `HistoryValueNotifier`!
93 |
94 | ```dart
95 | // Obtain a reference however you wish (Provider, GetIt, etc.)
96 | final CounterNotifier notifier = context.read(counterNotifier);
97 |
98 | notifier.increment(); // 1
99 | notifier.undo(); // 0
100 | notifier.redo(); // 1
101 |
102 | notifier.decrement(); // 0
103 | notifier.undo(); // 1
104 | notifier.canRedo // true
105 | notifier.increment // 2
106 | notifier.canRedo // false
107 |
108 | // ...
109 | ```
110 |
111 | ## SelectValueNotifier
112 |
113 | * 🎯 Select a subset of the state of a `ValueNotifier`
114 | * 🧩 Only listen to the parts of the state that you care about
115 | * 🏃 Micromanage rebuilds for maximum performance
116 |
117 | ### Usage
118 |
119 | Using `SelectValueNotifier` is super easy:
120 |
121 | #### Use the convenient `select` extension method
122 | This allows you to select a subset of the state of a `ValueNotifier` by providing a selector function anywhere you need.
123 |
124 | ```dart
125 | final notifier = ValueNotifier({'a': 1, 'b': 2});
126 |
127 | return ValueListenableBuilder(
128 | valueListenable: notifier.select((value) => value['a']),
129 | builder: (context, value, child) {
130 | return Text(value.toString());
131 | },
132 | );
133 | ```
134 | Your `selectNotifier` will now only notify listeners when the value of `'a'` changes.
135 |
136 | ## WhereValueNotifier
137 |
138 | * 🔎 Provide a custom predicate to determine whether a state change should be propagated to listeners
139 | * 🧩 Only listen to the state changes that you care about
140 |
141 | ### Usage
142 | There are two main ways to use `WhereValueNotifier`:
143 |
144 |
145 | #### Use the `where` extension method
146 | This allows you to dynamically filter the state changes that you care about from another notifier.
147 |
148 | ```dart
149 | final notifier = ValueNotifier(0);
150 |
151 | return ValueListenableBuilder(
152 | valueListenable: notifier.where((oldState, newState) => newState > oldState),
153 | builder: (context, value, child) {
154 | return Text(value.toString());
155 | },
156 | );
157 | ```
158 |
159 | Now, your widget will only rebuild when the new state is greater than the old state.
160 |
161 | #### Extend it for your own custom classes
162 | You can also extend it and provide your own `updateShouldNotify` function.
163 |
164 | ```dart
165 | class IncreasingValueNotifier extends WhereValueNotifier {
166 | IncreasingValueNotifier(super.value);
167 |
168 | @override
169 | bool updateShouldNotify(T oldState, T newState) {
170 | return oldState < newState;
171 | }
172 | }
173 | ```
174 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lintervention/analysis_options.yaml
2 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/coverage/lcov.info:
--------------------------------------------------------------------------------
1 | SF:lib/src/where_value_notifier/where_value_notifier_mixin.dart
2 | DA:8,4
3 | DA:10,2
4 | DA:11,2
5 | DA:13,2
6 | DA:15,2
7 | DA:16,2
8 | DA:17,4
9 | LF:7
10 | LH:7
11 | end_of_record
12 | SF:lib/src/where_value_notifier/where_value_notifier.dart
13 | DA:28,2
14 | LF:1
15 | LH:1
16 | end_of_record
17 | SF:lib/src/history_value_notifier/history_value_notifier.dart
18 | DA:17,1
19 | LF:1
20 | LH:1
21 | end_of_record
22 | SF:lib/src/history_value_notifier/history_value_notifier_mixin.dart
23 | DA:13,2
24 | DA:14,1
25 | DA:16,2
26 | DA:19,1
27 | DA:21,3
28 | DA:22,3
29 | DA:23,4
30 | DA:27,4
31 | DA:29,2
32 | DA:30,3
33 | DA:37,1
34 | DA:39,1
35 | DA:40,2
36 | DA:41,3
37 | DA:42,2
38 | DA:43,1
39 | DA:44,4
40 | DA:45,4
41 | DA:49,1
42 | DA:57,1
43 | DA:59,1
44 | DA:63,6
45 | DA:66,3
46 | DA:70,1
47 | DA:77,1
48 | DA:85,1
49 | DA:97,1
50 | DA:99,1
51 | DA:103,1
52 | DA:104,2
53 | DA:105,7
54 | DA:110,1
55 | DA:111,2
56 | DA:112,7
57 | DA:117,1
58 | DA:118,2
59 | DA:119,1
60 | DA:120,2
61 | DA:127,1
62 | DA:128,1
63 | DA:129,2
64 | DA:132,1
65 | DA:133,1
66 | DA:134,6
67 | DA:135,1
68 | LF:45
69 | LH:45
70 | end_of_record
71 | SF:lib/src/select_value_notifier/select_value_notifier.dart
72 | DA:15,1
73 | DA:18,3
74 | DA:19,3
75 | DA:30,1
76 | DA:33,1
77 | DA:36,1
78 | DA:37,5
79 | DA:40,1
80 | DA:42,3
81 | DA:43,1
82 | DA:55,1
83 | DA:58,1
84 | LF:12
85 | LH:12
86 | end_of_record
87 | SF:lib/src/where_value_notifier/where_value_notifier_from_parent.dart
88 | DA:13,1
89 | DA:17,2
90 | DA:18,3
91 | DA:27,1
92 | DA:29,2
93 | DA:32,1
94 | DA:33,3
95 | DA:36,1
96 | DA:38,2
97 | DA:41,1
98 | DA:43,3
99 | DA:44,1
100 | DA:52,1
101 | DA:53,1
102 | LF:14
103 | LH:14
104 | end_of_record
105 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier_mixin.dart';
3 |
4 | /// {@template history_value_notifier}
5 | /// Works like a [ValueNotifier] with the added benefit of maintaining an
6 | /// internal undo history that can be navigated through.
7 | ///
8 | /// All states that get set using the [value] setter will automatically be
9 | /// remembered for up to [maxHistoryLength] entries. If you want to update
10 | /// the state without adding it to the history, use the [temporaryValue] setter
11 | /// instead.
12 | /// > Note: The initial undo history will start with the initial [value].
13 | /// {@endtemplate}
14 | class HistoryValueNotifier extends ValueNotifier
15 | with HistoryValueNotifierMixin {
16 | /// {@macro history_value_notifier}
17 | HistoryValueNotifier(super._value);
18 | }
19 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier.dart';
3 |
4 | /// Use this mixin on any [ValueNotifier] to add a history functionality to it.
5 | ///
6 | /// {@macro history_value_notifier}
7 | mixin HistoryValueNotifierMixin on ValueNotifier {
8 | int? _maxHistoryLength;
9 |
10 | /// How many values to keep track of.
11 | ///
12 | /// If null, all values will be stored.
13 | int? get maxHistoryLength => _maxHistoryLength;
14 | set maxHistoryLength(int? value) {
15 | assert(
16 | value == null || value >= 0,
17 | "The maxHistoryLength can't be negative!",
18 | );
19 | _maxHistoryLength = value;
20 | if (value == null) return;
21 | if (_undoHistory.length > value) {
22 | _undoHistory = _undoHistory.sublist(0, value);
23 | _undoIndex = _undoIndex >= value ? value - 1 : _undoIndex;
24 | }
25 | }
26 |
27 | bool get _keepAny => _maxHistoryLength == null || _maxHistoryLength! > 0;
28 |
29 | late List _undoHistory = [
30 | if (includeInitialValueInHistory && _keepAny) value,
31 | ];
32 |
33 | int _undoIndex = 0;
34 |
35 | /// Sets the value of this [HistoryValueNotifier], notifies listeners,
36 | /// and adds the new value to the undo history.
37 | @override
38 | set value(T value) {
39 | _internalClearRedoQueue();
40 | if (_undoHistory.isEmpty ||
41 | shouldInsertValueIntoQueue(value, _undoHistory[0])) {
42 | _undoHistory.insert(0, value);
43 | if (_maxHistoryLength != null &&
44 | _undoHistory.length > _maxHistoryLength!) {
45 | _undoHistory = _undoHistory.sublist(0, _maxHistoryLength);
46 | }
47 | }
48 |
49 | super.value = value;
50 | }
51 |
52 | /// Sets the current "value" of this [HistoryValueNotifier] **without** adding
53 | /// the new value to the undo history.
54 | ///
55 | /// This is helpful for loading values or in general any other value that the
56 | /// user should not be able to undo to.
57 | @protected
58 | set temporaryValue(T value) {
59 | super.value = value;
60 | }
61 |
62 | /// Whether currently an undo operation is possible.
63 | bool get canUndo => _undoIndex + 1 < _undoHistory.length;
64 |
65 | /// Whether a redo operation is currently possible.
66 | bool get canRedo => _undoIndex > 0;
67 |
68 | /// You can override this to prevent undo/redo operations in certain cases
69 | /// (e.g. when in a loading value)
70 | @protected
71 | bool get allowOperations => true;
72 |
73 | /// Whether to include the initial [value] in the undo history.
74 | ///
75 | /// `true` by default, override and set to false if the initial value should
76 | /// be treated like [temporaryValue].
77 | @protected
78 | bool get includeInitialValueInHistory => true;
79 |
80 | /// You can override this function if you want to transform values from the
81 | /// history before they get applied.
82 | ///
83 | /// This can be useful if your value contains values that aren't supposed
84 | /// to be changed upon undoing for example.
85 | @protected
86 | T transformHistoryValue(T newValue, T currentValue) {
87 | return newValue;
88 | }
89 |
90 | /// You can override this function if you want to filter certain values before
91 | /// adding them to the history.
92 | ///
93 | /// By default, this uses value equality, but you could for example always
94 | /// return true in case your value doesn't support value equality.
95 | /// [newValue] holds the value that's supposed to be added, [lastInQueue] is
96 | /// the value that's currently last in the undo queue.
97 | @protected
98 | bool shouldInsertValueIntoQueue(T newValue, T lastInQueue) {
99 | return newValue != lastInQueue;
100 | }
101 |
102 | /// Returns to the previous value in the history.
103 | void undo() {
104 | if (canUndo && allowOperations) {
105 | temporaryValue = transformHistoryValue(_undoHistory[++_undoIndex], value);
106 | }
107 | }
108 |
109 | /// Proceeds to the next value in the history.
110 | void redo() {
111 | if (canRedo && allowOperations) {
112 | temporaryValue = transformHistoryValue(_undoHistory[--_undoIndex], value);
113 | }
114 | }
115 |
116 | /// Removes all history items from the queue.
117 | void clearQueue() {
118 | _undoHistory = [];
119 | _undoIndex = 0;
120 | temporaryValue = value;
121 | }
122 |
123 | /// Removes all history items that happened after the current undo position.
124 | ///
125 | /// Internally this is used whenever a change occurs, but you might want to
126 | /// use it for something else.
127 | void clearRedoQueue() {
128 | _internalClearRedoQueue();
129 | temporaryValue = value;
130 | }
131 |
132 | void _internalClearRedoQueue() {
133 | if (canRedo) {
134 | _undoHistory = _undoHistory.sublist(_undoIndex, _undoHistory.length);
135 | _undoIndex = 0;
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/select_value_notifier/select_value_notifier.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | /// A function that maps a value of type [FromT] to a value of type [ToT].
4 | typedef Selector = ToT Function(FromT value);
5 |
6 | /// {@template selected_value_notifier}
7 | /// A [ValueNotifier] that wraps another [ValueNotifier] and selects updates
8 | /// based on a provided function.
9 | ///
10 | /// Updates will only be sent to listeners when the new value is different from
11 | /// the previous value.
12 | /// {@endtemplate}
13 | class SelectValueNotifier extends ValueNotifier {
14 | /// {@macro selected_value_notifier}
15 | SelectValueNotifier({
16 | required this.parentNotifier,
17 | required this.selector,
18 | }) : super(selector(parentNotifier.value)) {
19 | parentNotifier.addListener(_updateFromParent);
20 | }
21 |
22 | /// The [ValueNotifier] that this [SelectValueNotifier] listens to and
23 | /// filters updates from.
24 | final ValueNotifier parentNotifier;
25 |
26 | /// A function that maps the value of the parent notifier to the value of this
27 | /// notifier.
28 | final Selector selector;
29 |
30 | @override
31 | @protected
32 | set value(ToT newValue) {
33 | throw UnsupportedError('Cannot set value on SelectValueNotifier');
34 | }
35 |
36 | void _updateFromParent() {
37 | super.value = selector(parentNotifier.value);
38 | }
39 |
40 | @override
41 | void dispose() {
42 | parentNotifier.removeListener(_updateFromParent);
43 | super.dispose();
44 | }
45 | }
46 |
47 | /// An extension on [ValueNotifier] that provides a `select` method to create a
48 | /// [SelectValueNotifier] from the notifier.
49 | extension SelectValueNotifierX on ValueNotifier {
50 | /// Selects updates from this notifier using the provided [selector]
51 | /// function.
52 | ///
53 | /// The [selector] function is called whenever the parent notifier updates and
54 | /// the result is used as the new value for the [SelectValueNotifier].
55 | SelectValueNotifier select(
56 | ToT Function(FromT value) selector,
57 | ) {
58 | return SelectValueNotifier(
59 | parentNotifier: this,
60 | selector: selector,
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier_mixin.dart';
3 |
4 | /// {@template where_value_notifier}
5 | /// A [ValueNotifier] that provides a custom [updateShouldNotify] function to
6 | /// determine whether the listener should be notified.
7 | ///
8 | /// Extend this to create a custom [ValueNotifier] that notifies listeners based
9 | /// on the provided [updateShouldNotify] function.
10 | ///
11 | /// **Example:**
12 | /// In this example, the MyValueNotifier notifies listeners when the new value
13 | /// is less than the previous value.
14 | /// ```dart
15 | /// class MyValueNotifier extends WhereValueNotifier {
16 | /// MyValueNotifier(int value) : super(value);
17 | ///
18 | /// @override
19 | /// bool updateShouldNotify(int previous, int next) {
20 | /// return previous > next;
21 | /// }
22 | /// }
23 | /// ```
24 | /// {@endtemplate}
25 | abstract class WhereValueNotifier extends ValueNotifier
26 | with WhereValueNotifierMixin {
27 | /// {@macro where_value_notifier}
28 | WhereValueNotifier(super.value);
29 | }
30 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_from_parent.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier.dart';
3 |
4 | /// A function that compares the previous value with the new value and returns
5 | /// whether the listener should be notified.
6 | typedef WhereFilter = bool Function(T previous, T next);
7 |
8 | ///
9 | /// A [WhereValueNotifier] that listens to a parent [ValueNotifier] and notifies
10 | /// listeners based on the provided [updateShouldNotify] function.
11 | class WhereValueNotifierFromParent extends WhereValueNotifier {
12 | /// {@macro selected_value_notifier}
13 | WhereValueNotifierFromParent({
14 | required this.parentNotifier,
15 | required WhereFilter updateShouldNotify,
16 | }) : filter = updateShouldNotify,
17 | super(parentNotifier.value) {
18 | parentNotifier.addListener(_parentListener);
19 | }
20 |
21 | /// The parent notifier to listen to.
22 | final ValueNotifier parentNotifier;
23 |
24 | /// The function that determines whether the listeners should be notified.
25 | final WhereFilter filter;
26 |
27 | @override
28 | set value(T newValue) {
29 | parentNotifier.value = newValue;
30 | }
31 |
32 | void _parentListener() {
33 | super.value = parentNotifier.value;
34 | }
35 |
36 | @override
37 | bool updateShouldNotify(T previous, T next) {
38 | return filter(previous, next);
39 | }
40 |
41 | @override
42 | void dispose() {
43 | parentNotifier.removeListener(_parentListener);
44 | super.dispose();
45 | }
46 | }
47 |
48 | /// An extension on [ValueNotifier] that provides a `where` method to create a
49 | /// [WhereValueNotifier] from the notifier.
50 | extension SelectedValueNotifierX on ValueNotifier {
51 | /// Creates a [WhereValueNotifier] from the notifier.
52 | WhereValueNotifier where(WhereFilter updateShouldNotify) {
53 | return WhereValueNotifierFromParent(
54 | parentNotifier: this,
55 | updateShouldNotify: updateShouldNotify,
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | /// Use this mixin on any [ValueNotifier] to add a custom [updateShouldNotify]
4 | /// function to determine whether the listeners should be notified.
5 | ///
6 | /// {@macro where_value_notifier}
7 | mixin WhereValueNotifierMixin on ValueNotifier {
8 | late T _value = super.value;
9 |
10 | @override
11 | T get value => _value;
12 |
13 | @override
14 | set value(T newValue) {
15 | final previous = _value;
16 | _value = newValue;
17 | if (updateShouldNotify(previous, newValue)) notifyListeners();
18 | }
19 |
20 | /// The function that determines whether the listeners should be notified.
21 | ///
22 | /// If this function returns `true`, the listener will be notified.
23 | bool updateShouldNotify(T previous, T next);
24 | }
25 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/lib/value_notifier_tools.dart:
--------------------------------------------------------------------------------
1 | /// Helpful lightweight tools for working with ValueNotifiers
2 | library value_notifier_tools;
3 |
4 | export 'src/history_value_notifier/history_value_notifier.dart';
5 | export 'src/history_value_notifier/history_value_notifier_mixin.dart';
6 | export 'src/select_value_notifier/select_value_notifier.dart';
7 | export 'src/where_value_notifier/where_value_notifier.dart';
8 | export 'src/where_value_notifier/where_value_notifier_from_parent.dart';
9 | export 'src/where_value_notifier/where_value_notifier_mixin.dart';
10 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: value_notifier_tools
2 | description: Helpful lightweight tools for working with ValueNotifiers
3 | version: 0.1.2
4 | repository: https://github.com/timcreatedit/scribble/tree/main/packages/value_notifier_tools
5 | homepage: https://whynotmake.it
6 |
7 | environment:
8 | sdk: ">=3.0.0 <4.0.0"
9 | flutter: ">=3.10.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 |
15 | dev_dependencies:
16 | flutter_test:
17 | sdk: flutter
18 | lintervention: ^0.1.1
19 | mocktail: ^1.0.3
20 |
21 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/test/src/history_value_notifier/history_value_notifier_test.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mocktail/mocktail.dart';
4 | import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier.dart';
5 |
6 | class _MockNotifier extends HistoryValueNotifier with Mock {
7 | _MockNotifier(super.value);
8 | }
9 |
10 | void main() {
11 | group('HistoryValueNotifier', () {
12 | test('can be instantiated', () {
13 | expect(HistoryValueNotifier(1), isNotNull);
14 | expect(HistoryValueNotifier(1).value, 1);
15 | });
16 |
17 | group('as-is', () {
18 | late HistoryValueNotifier sut;
19 |
20 | setUp(() {
21 | sut = HistoryValueNotifier(0);
22 | });
23 | test('can undo to initial state', () {
24 | expect(sut.value, 0);
25 | expect(sut.canUndo, false);
26 | expect(sut.canRedo, false);
27 | sut.value = 1;
28 | expect(sut.value, 1);
29 | expect(sut.canUndo, true);
30 | expect(sut.canRedo, false);
31 | sut.undo();
32 | expect(sut.value, 0);
33 | expect(sut.canUndo, false);
34 | expect(sut.canRedo, true);
35 | sut.redo();
36 | expect(sut.value, 1);
37 | expect(sut.canUndo, true);
38 | expect(sut.canRedo, false);
39 | });
40 |
41 | test('clears redo queue when setting value while undone', () {
42 | expect(sut.value, 0);
43 | expect(sut.canUndo, false);
44 | expect(sut.canRedo, false);
45 | sut.value = 1;
46 | expect(sut.value, 1);
47 | expect(sut.canUndo, true);
48 | expect(sut.canRedo, false);
49 | sut.undo();
50 | expect(sut.value, 0);
51 | expect(sut.canUndo, false);
52 | expect(sut.canRedo, true);
53 | sut.value = 2;
54 | expect(sut.value, 2);
55 | expect(sut.canUndo, true);
56 | expect(sut.canRedo, false);
57 | });
58 |
59 | test('clearRedoQueue works', () {
60 | sut.value = 1;
61 | expect(sut.value, 1);
62 | sut.value = 2;
63 | expect(sut.canUndo, true);
64 | expect(sut.canRedo, false);
65 | sut.undo();
66 | expect(sut.value, 1);
67 | expect(sut.canUndo, true);
68 | expect(sut.canRedo, true);
69 | sut.clearRedoQueue();
70 | expect(sut.canRedo, false);
71 | });
72 | });
73 |
74 | group('as extension', () {
75 | late _MockNotifier sut;
76 |
77 | setUp(() {
78 | sut = _MockNotifier(0);
79 | });
80 |
81 | test('can undo to initial state', () {
82 | expect(sut.value, 0);
83 | expect(sut.canUndo, false);
84 | expect(sut.canRedo, false);
85 | sut.value = 1;
86 | expect(sut.value, 1);
87 | expect(sut.canUndo, true);
88 | expect(sut.canRedo, false);
89 | sut.undo();
90 | expect(sut.value, 0);
91 | expect(sut.canUndo, false);
92 | expect(sut.canRedo, true);
93 | sut.redo();
94 | expect(sut.value, 1);
95 | expect(sut.canUndo, true);
96 | expect(sut.canRedo, false);
97 | });
98 |
99 | test('clears redo queue when setting value while undone', () {
100 | expect(sut.value, 0);
101 | expect(sut.canUndo, false);
102 | expect(sut.canRedo, false);
103 | sut.value = 1;
104 | expect(sut.value, 1);
105 | expect(sut.canUndo, true);
106 | expect(sut.canRedo, false);
107 | sut.undo();
108 | expect(sut.value, 0);
109 | expect(sut.canUndo, false);
110 | expect(sut.canRedo, true);
111 | sut.value = 2;
112 | expect(sut.value, 2);
113 | expect(sut.canUndo, true);
114 | expect(sut.canRedo, false);
115 | });
116 |
117 | test('clearRedoQueue works', () {
118 | sut.value = 1;
119 | expect(sut.value, 1);
120 | sut.value = 2;
121 | expect(sut.canUndo, true);
122 | expect(sut.canRedo, false);
123 | sut.undo();
124 | expect(sut.value, 1);
125 | expect(sut.canUndo, true);
126 | expect(sut.canRedo, true);
127 | sut.clearRedoQueue();
128 | expect(sut.canRedo, false);
129 | });
130 |
131 | group('maxHistoryLength', () {
132 | const changeCount = 10;
133 | const maxHistoryLength = 5;
134 |
135 | test('drops entries that where there already', () async {
136 | const changeCount = 10;
137 |
138 | for (var i = 0; i < changeCount; i++) {
139 | sut.value++;
140 | }
141 |
142 | expect(sut.canUndo, true);
143 | expect(sut.canRedo, false);
144 |
145 | sut.maxHistoryLength = maxHistoryLength;
146 |
147 | expect(sut.canUndo, true);
148 | expect(sut.canRedo, false);
149 |
150 | // We should be able to undo maxHistoryLength - 1 times
151 | for (var i = 0; i < maxHistoryLength - 1; i++) {
152 | expect(sut.canUndo, true, reason: 'iteration $i');
153 | expect(sut.value, changeCount - i);
154 | sut.undo();
155 | }
156 | expect(sut.canUndo, false);
157 | });
158 |
159 | test('only collects the last entries', () async {
160 | sut.maxHistoryLength = maxHistoryLength;
161 | for (var i = 0; i < changeCount; i++) {
162 | sut.value++;
163 | }
164 |
165 | expect(sut.canUndo, true);
166 | expect(sut.canRedo, false);
167 |
168 | // We should be able to undo historyLength - 1 times
169 | for (var i = 0; i < sut.maxHistoryLength! - 1; i++) {
170 | expect(sut.canUndo, true, reason: 'iteration $i');
171 | expect(sut.value, changeCount - i);
172 | sut.undo();
173 | }
174 | expect(sut.canUndo, false);
175 | });
176 | });
177 |
178 | group('clear', () {
179 | test('clears history', () {
180 | sut
181 | ..value = 1
182 | ..value = 2
183 | ..value = 3;
184 | expect(sut.canUndo, true);
185 | expect(sut.canRedo, false);
186 | sut.clearQueue();
187 | expect(sut.canUndo, false);
188 | expect(sut.canRedo, false);
189 | });
190 | });
191 | });
192 | });
193 | }
194 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/test/src/select_value_notifier/select_value_notifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mocktail/mocktail.dart';
4 | import 'package:value_notifier_tools/src/select_value_notifier/select_value_notifier.dart';
5 |
6 | import '../../util/mock_listener.dart';
7 |
8 | typedef _Model = ({int interesting, int uninteresting});
9 |
10 | void main() {
11 | group('SelectValueNotifier', () {
12 | late ValueNotifier<_Model> notifier;
13 | late MockListener notifierListener;
14 | late SelectValueNotifier<_Model, int> sut;
15 | late MockListener sutListener;
16 |
17 | setUp(() {
18 | notifier = ValueNotifier((interesting: 0, uninteresting: 0));
19 | notifierListener = MockListener();
20 | notifier.addListener(notifierListener.call);
21 | addTearDown(() => notifier.removeListener(notifierListener.call));
22 |
23 | sut = SelectValueNotifier(
24 | parentNotifier: notifier,
25 | selector: (model) => model.interesting,
26 | );
27 | sutListener = MockListener();
28 |
29 | sut.addListener(sutListener.call);
30 | });
31 |
32 | group('notifyListeners()', () {
33 | test(
34 | 'should notify listeners when the new value is different from the '
35 | 'previous value', () {
36 | notifier.value = (interesting: 1, uninteresting: 1);
37 | verify(() => notifierListener.call());
38 | verify(() => sutListener.call());
39 | expect(sut.value, 1);
40 | });
41 |
42 | test(
43 | 'should not notify listeners when the new value is the same as the '
44 | 'previous value', () {
45 | notifier.value = (interesting: 0, uninteresting: 1);
46 | verify(() => notifierListener.call());
47 | verifyNever(() => sutListener.call());
48 | expect(sut.value, 0);
49 | });
50 | });
51 |
52 | group('set value', () {
53 | test('should throw an UnsupportedError', () {
54 | // ignore: invalid_use_of_protected_member
55 | expect(() => sut.value = 1, throwsUnsupportedError);
56 | });
57 | });
58 |
59 | group('dispose', () {
60 | test('should remove the listener from the parent notifier', () {
61 | notifier.value = (interesting: 1, uninteresting: 1);
62 | verify(() => sutListener.call());
63 | sut.dispose();
64 | notifier.value = (interesting: 2, uninteresting: 1);
65 | verifyNever(() => sutListener.call());
66 | });
67 | });
68 | });
69 |
70 | group('SelectValueNotifierX', () {
71 | late ValueNotifier<_Model> notifier;
72 |
73 | setUp(() {
74 | notifier = ValueNotifier((interesting: 0, uninteresting: 0));
75 | });
76 |
77 | group('select()', () {
78 | test('should return a SelectValueNotifier that is set up correctly', () {
79 | // ignore: omit_local_variable_types, prefer_function_declarations_over_variables
80 | final Selector<_Model, int> selector = (model) => model.interesting;
81 | final sut = notifier.select(selector);
82 |
83 | expect(sut.parentNotifier, notifier);
84 | expect(sut.selector, selector);
85 | });
86 | });
87 | });
88 | }
89 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_from_parent_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mocktail/mocktail.dart';
4 | import 'package:value_notifier_tools/value_notifier_tools.dart';
5 |
6 | import '../../util/mock_listener.dart';
7 |
8 | class _MockUpdateShouldNotify extends Mock {
9 | bool call(int previous, int next);
10 | }
11 |
12 | void main() {
13 | group('WhereValueNotifierFromParent', () {
14 | late ValueNotifier notifier;
15 | late MockListener notifierListener;
16 | late WhereValueNotifierFromParent sut;
17 | late MockListener sutListener;
18 | late _MockUpdateShouldNotify updateShouldNotify;
19 | setUp(() {
20 | notifier = ValueNotifier(0);
21 | notifierListener = MockListener();
22 | notifier.addListener(notifierListener.call);
23 | addTearDown(() => notifier.removeListener(notifierListener.call));
24 |
25 | updateShouldNotify = _MockUpdateShouldNotify();
26 | sut = WhereValueNotifierFromParent(
27 | parentNotifier: notifier,
28 | updateShouldNotify: updateShouldNotify.call,
29 | );
30 | sutListener = MockListener();
31 | sut.addListener(sutListener.call);
32 | addTearDown(() => sut.removeListener(sutListener.call));
33 | });
34 |
35 | group('notifyListeners()', () {
36 | test('should notify listeners when updateShouldNotify is true', () {
37 | when(() => updateShouldNotify.call(any(), any())).thenReturn(true);
38 |
39 | notifier.value = -1;
40 | verify(() => notifierListener.call());
41 | verify(() => sut.updateShouldNotify(0, -1));
42 | verify(() => sutListener.call());
43 | expect(sut.value, -1);
44 | });
45 |
46 | test('should not notify listeners when updateShouldNotify is false', () {
47 | when(() => sut.updateShouldNotify(any(), any())).thenReturn(false);
48 |
49 | notifier.value = 1;
50 | verify(() => notifierListener.call());
51 | verify(() => sut.updateShouldNotify(0, 1));
52 | verifyNever(() => sutListener.call());
53 | expect(sut.value, 1);
54 | });
55 | });
56 |
57 | group('set value', () {
58 | test('should set the parent notifier value', () {
59 | when(() => updateShouldNotify.call(any(), any())).thenReturn(true);
60 | sut.value = 2;
61 | expect(notifier.value, 2);
62 | });
63 |
64 | test('should notify listeners when updateShouldNotify is true', () {
65 | when(() => updateShouldNotify.call(any(), any())).thenReturn(true);
66 |
67 | sut.value = -1;
68 | verify(() => notifierListener.call());
69 | verify(() => sut.updateShouldNotify(0, -1));
70 | verify(() => sutListener.call());
71 | expect(sut.value, -1);
72 | });
73 |
74 | test('should not notify listeners when updateShouldNotify is false', () {
75 | when(() => sut.updateShouldNotify(any(), any())).thenReturn(false);
76 |
77 | sut.value = 1;
78 | verify(() => notifierListener.call());
79 | verify(() => sut.updateShouldNotify(0, 1));
80 | verifyNever(() => sutListener.call());
81 | expect(sut.value, 1);
82 | });
83 | });
84 |
85 | group('dispose()', () {
86 | test('should remove the listener from the parent notifier', () {
87 | when(() => updateShouldNotify.call(any(), any())).thenReturn(true);
88 |
89 | notifier.value = -1;
90 | verify(() => notifierListener.call());
91 | verify(() => sutListener.call());
92 | sut.dispose();
93 | notifier.value = 2;
94 | verifyNever(() => sutListener.call());
95 | });
96 | });
97 |
98 | group('SelectedValueNotifierX', () {
99 | test('should create a WhereValueNotifierFromParent', () {
100 | when(() => updateShouldNotify.call(any(), any())).thenReturn(true);
101 |
102 | final where = notifier.where(updateShouldNotify.call);
103 | expect(where, isA>());
104 | where.updateShouldNotify(0, 1);
105 | verify(() => updateShouldNotify.call(0, 1));
106 | });
107 | });
108 | });
109 | }
110 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:mocktail/mocktail.dart';
3 | import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier.dart';
4 |
5 | import '../../util/mock_listener.dart';
6 |
7 | class _TestNotifier extends WhereValueNotifier with Mock {
8 | _TestNotifier(super.value);
9 |
10 | @override
11 | bool updateShouldNotify(int previous, int next);
12 | }
13 |
14 | void main() {
15 | group('WhereValueNotifier', () {
16 | late _TestNotifier sut;
17 | late MockListener listener;
18 | setUp(() {
19 | sut = _TestNotifier(0);
20 | listener = MockListener();
21 | sut.addListener(listener.call);
22 | addTearDown(() => sut.removeListener(listener.call));
23 | });
24 |
25 | group('notifyListeners()', () {
26 | test('should notify listeners when updateShouldNotify is true', () {
27 | when(() => sut.updateShouldNotify(any(), any())).thenReturn(true);
28 |
29 | sut.value = -1;
30 | verify(() => listener.call());
31 | verify(() => sut.updateShouldNotify(0, -1));
32 | expect(sut.value, -1);
33 | });
34 |
35 | test('should not notify listeners when updateShouldNotify is false', () {
36 | when(() => sut.updateShouldNotify(any(), any())).thenReturn(false);
37 |
38 | sut.value = 1;
39 | verifyNever(() => listener.call());
40 | verify(() => sut.updateShouldNotify(0, 1));
41 | expect(sut.value, 1);
42 | });
43 | });
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/packages/value_notifier_tools/test/util/mock_listener.dart:
--------------------------------------------------------------------------------
1 | import 'package:mocktail/mocktail.dart';
2 |
3 | /// A simple listener that can be used in tests to verify that it was called.
4 | class MockListener extends Mock {
5 | void call();
6 | }
7 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: scribble
2 | description: Scribble is a lightweight library for freehand drawing in Flutter
3 | supporting pressure, variable line width and more!
4 | version: 0.10.0+1
5 | repository: https://github.com/timcreatedit/scribble
6 | issue_tracker: https://github.com/timcreatedit/scribble/issues
7 |
8 | environment:
9 | sdk: ">=3.0.0 <4.0.0"
10 | flutter: ">=3.10.0"
11 |
12 | dependencies:
13 | flutter:
14 | sdk: flutter
15 | freezed_annotation: ">=2.4.1 <3.0.0"
16 | perfect_freehand: ^2.3.2
17 | simpli: ^0.1.1
18 | value_notifier_tools: ^0.1.2
19 |
20 | dev_dependencies:
21 | build_runner: ^2.4.9
22 | flutter_test:
23 | sdk: flutter
24 | freezed: ^2.4.7
25 | json_serializable: ^6.9.5
26 | lintervention: ^0.3.1
27 | melos: ^6.3.3
28 | mocktail: ^1.0.3
29 |
30 | flutter:
31 | uses-material-design: true
--------------------------------------------------------------------------------
/scribble_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timcreatedit/scribble/c4f5b43ffbbb18d3cf4efdcdde4a6901bd8c63c5/scribble_demo.gif
--------------------------------------------------------------------------------
/test/src/domain/iterable_removed_x_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:scribble/src/domain/iterable_removed_x.dart';
3 |
4 | void main() {
5 | setUp(() {});
6 |
7 | group('IterableRemovedX', () {
8 | group('removedIndices', () {
9 | const list = [1, 2, 3, 4, 5, 3];
10 | test('returns empty list if iterables are the same', () async {
11 | final removedIndices = list.removedIndices(list);
12 | expect(removedIndices, isEmpty);
13 | });
14 |
15 | test('returns indices of one removed element', () async {
16 | final removedIndices = list.removedIndices([2, 3, 4, 5, 3]);
17 | expect(removedIndices, equals([0]));
18 | });
19 |
20 | test('works with duplicates', () async {
21 | final removedIndices = list.removedIndices([1, 2, 4, 5, 3]);
22 | expect(removedIndices, equals([2]));
23 | });
24 |
25 | test('works with multiple removed elements', () async {
26 | final removedIndices = list.removedIndices([1, 2, 4, 5]);
27 | expect(removedIndices.toList(), [2, 5]);
28 | });
29 | });
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/test/src/view/notifier/scribble_notifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mocktail/mocktail.dart';
4 | import 'package:scribble/scribble.dart';
5 | import 'package:scribble/src/view/simplification/sketch_simplifier.dart';
6 |
7 | class _MockSimplifier extends Mock implements SketchSimplifier {}
8 |
9 | void main() {
10 | setUp(() {
11 | registerFallbackValue(const Sketch(lines: []));
12 | registerFallbackValue(
13 | const SketchLine(points: [], color: 0xFFFFFF, width: 2),
14 | );
15 | });
16 |
17 | group('ScribbleNotifier', () {
18 | late ScribbleNotifier sut;
19 | late _MockSimplifier simplifier;
20 |
21 | const line = SketchLine(points: [Point(1, 1)], color: 0xFFFFFFFF, width: 2);
22 | const emptyLine = SketchLine(points: [], color: 0x0, width: 0);
23 | const sketch = Sketch(lines: [line]);
24 | const emptySketch = Sketch(lines: []);
25 |
26 | setUp(() {
27 | simplifier = _MockSimplifier();
28 |
29 | when(
30 | () => simplifier.simplify(
31 | any(),
32 | pixelTolerance: any(named: "pixelTolerance"),
33 | ),
34 | ).thenReturn(emptyLine);
35 |
36 | when(
37 | () => simplifier.simplifySketch(
38 | any(),
39 | pixelTolerance: any(named: "pixelTolerance"),
40 | ),
41 | ).thenAnswer(
42 | (invocation) => invocation.namedArguments[#pixelTolerance] == 0
43 | ? invocation.positionalArguments[0] as Sketch
44 | : emptySketch,
45 | );
46 |
47 | sut = ScribbleNotifier(
48 | sketch: sketch,
49 | simplifier: simplifier,
50 | );
51 | });
52 |
53 | group("constructor", () {
54 | test('initializes with the given sketch', () async {
55 | expect(sut.value.sketch, sketch);
56 | verify(() => simplifier.simplifySketch(sketch, pixelTolerance: 0));
57 | });
58 | });
59 |
60 | group('setSketch', () {
61 | test('is undoable by default if nothing happened before', () async {
62 | expect(sut.canUndo, false);
63 | expect(sut.canRedo, false);
64 | sut.setSketch(sketch: emptySketch);
65 | expect(sut.canUndo, true);
66 | expect(sut.canRedo, false);
67 | sut.undo();
68 | expect(sut.currentSketch, sketch);
69 | });
70 |
71 | test('is undoable by default if something happened before', () async {
72 | sut
73 | ..onPointerDown(const PointerDownEvent())
74 | ..onPointerUpdate(const PointerMoveEvent(position: Offset(100, 100)))
75 | ..onPointerUp(const PointerUpEvent(position: Offset(100, 100)));
76 |
77 | final newSketch = sut.currentSketch;
78 | expect(sut.canUndo, true);
79 |
80 | sut.setSketch(sketch: emptySketch);
81 | expect(sut.canUndo, true);
82 | expect(sut.canRedo, false);
83 | sut.undo();
84 | expect(sut.currentSketch, newSketch);
85 | });
86 |
87 | test('is not undoable if set so', () async {
88 | expect(sut.canUndo, false);
89 | expect(sut.canRedo, false);
90 | sut.setSketch(sketch: emptySketch, addToUndoHistory: false);
91 | expect(sut.canUndo, false);
92 | expect(sut.canRedo, false);
93 | sut.undo();
94 | expect(sut.currentSketch, emptySketch);
95 | });
96 |
97 | test('is not undoable if it changed nothing', () async {
98 | expect(sut.canUndo, false);
99 | expect(sut.canRedo, false);
100 | sut.setSketch(sketch: sketch);
101 | expect(sut.canUndo, false);
102 | expect(sut.canRedo, false);
103 | });
104 | });
105 |
106 | group("simplify", () {
107 | test('calls simplifier method', () async {
108 | sut.setSimplificationTolerance(2);
109 | expect(sut.value.simplificationTolerance, 2);
110 | sut.simplify();
111 | verify(() => simplifier.simplifySketch(sketch, pixelTolerance: 2));
112 | expect(sut.value.sketch, emptySketch);
113 | });
114 |
115 | test('is undoable by default', () async {
116 | expect(sut.canUndo, false);
117 | sut
118 | ..setSimplificationTolerance(2)
119 | ..simplify();
120 | expect(sut.canUndo, true);
121 | });
122 |
123 | test('is not undoable if set so', () async {
124 | expect(sut.canUndo, false);
125 | sut
126 | ..setSimplificationTolerance(2)
127 | ..simplify(addToUndoHistory: false);
128 | expect(sut.canUndo, false);
129 | });
130 |
131 | test('is not undoable if it did nothing', () async {
132 | expect(sut.canUndo, false);
133 | sut
134 | ..setSimplificationTolerance(0)
135 | ..simplify();
136 | expect(sut.canUndo, false);
137 | });
138 | });
139 | });
140 | }
141 |
--------------------------------------------------------------------------------
/test/src/view/scribble_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:scribble/scribble.dart';
4 | import 'package:scribble/src/view/painting/scribble_editing_painter.dart';
5 | import 'package:scribble/src/view/painting/scribble_painter.dart';
6 |
7 | void main() {
8 | setUp(() {});
9 |
10 | group('Scribble', () {
11 | group('simulatePressure', () {
12 | Widget build({required bool simulatePressure}) {
13 | return MaterialApp(
14 | home: Scribble(
15 | notifier: ScribbleNotifier(),
16 | simulatePressure: simulatePressure,
17 | ),
18 | );
19 | }
20 |
21 | testWidgets(
22 | 'sets simulatePressure on ScribbleEditingPainter',
23 | (WidgetTester tester) async {
24 | await tester.pumpWidget(build(simulatePressure: true));
25 | final finder = find.byType(CustomPaint);
26 | final widgets =
27 | finder.evaluate().map((e) => e.widget).cast();
28 | final painters = widgets.map((e) => e.foregroundPainter).toList();
29 | final painter = painters.whereType().first;
30 | expect(painter.simulatePressure, isTrue);
31 | },
32 | );
33 |
34 | testWidgets(
35 | 'sets simulatePressure on ScribblePainter',
36 | (WidgetTester tester) async {
37 | await tester.pumpWidget(build(simulatePressure: true));
38 | final finder = find.byType(CustomPaint);
39 | final widgets =
40 | finder.evaluate().map((e) => e.widget).cast();
41 | final painters = widgets.map((e) => e.painter).toList();
42 | final painter = painters.whereType().first;
43 | expect(painter.simulatePressure, isTrue);
44 | },
45 | );
46 | });
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/test/src/view/simplification/sketch_simplifier_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:scribble/scribble.dart';
3 | import 'package:scribble/src/view/simplification/sketch_simplifier.dart';
4 |
5 | void main() {
6 | setUp(() {});
7 |
8 | group('VisvalingamSimplifier', () {
9 | late VisvalingamSimplifier sut;
10 |
11 | setUp(() {
12 | sut = const VisvalingamSimplifier();
13 | });
14 | group('simplify', () {
15 | const points = [
16 | Point(0, 0),
17 | Point(1, 1),
18 | Point(2, 0),
19 | Point(3, 3),
20 | Point(4, 0),
21 | ];
22 | const line = SketchLine(points: points, color: 0xFF000000, width: 2);
23 | test('simplifies the given list of points', () async {
24 | final simplified = sut.simplify(line, pixelTolerance: 2);
25 |
26 | expect(
27 | simplified,
28 | line.copyWith(
29 | points: [
30 | points[0],
31 | points[2],
32 | points[3],
33 | points[4],
34 | ],
35 | ),
36 | );
37 | });
38 | });
39 |
40 | group('simplifySketch', () {
41 | const sketch = Sketch(
42 | lines: [
43 | SketchLine(
44 | points: [
45 | Point(0, 0),
46 | Point(1, 1),
47 | Point(2, 0),
48 | Point(3, 3),
49 | Point(4, 0),
50 | ],
51 | color: 0xFF000000,
52 | width: 10,
53 | ),
54 | SketchLine(
55 | points: [
56 | Point(0, 0),
57 | Point(1, 1),
58 | Point(2, 0),
59 | Point(3, 3),
60 | Point(4, 0),
61 | ],
62 | color: 0xFF000000,
63 | width: 10,
64 | ),
65 | ],
66 | );
67 | test('simplifies the given sketch', () async {
68 | final simplified = sut.simplifySketch(sketch, pixelTolerance: 2);
69 |
70 | expect(
71 | simplified,
72 | const Sketch(
73 | lines: [
74 | SketchLine(
75 | points: [
76 | Point(0, 0),
77 | Point(2, 0),
78 | Point(3, 3),
79 | Point(4, 0),
80 | ],
81 | color: 0xFF000000,
82 | width: 10,
83 | ),
84 | SketchLine(
85 | points: [
86 | Point(0, 0),
87 | Point(2, 0),
88 | Point(3, 3),
89 | Point(4, 0),
90 | ],
91 | color: 0xFF000000,
92 | width: 10,
93 | ),
94 | ],
95 | ),
96 | );
97 | });
98 | });
99 | });
100 | }
101 |
--------------------------------------------------------------------------------