Преглед на файлове

Initial import of zefyr package

Anatoly Pulyaevskiy преди 6 години
родител
ревизия
70587315ca
променени са 100 файла, в които са добавени 6322 реда и са изтрити 0 реда
  1. 15
    0
      packages/zefyr/.gitignore
  2. 8
    0
      packages/zefyr/AUTHORS
  3. 3
    0
      packages/zefyr/CHANGELOG.md
  4. 23
    0
      packages/zefyr/LICENSE
  5. 59
    0
      packages/zefyr/README.md
  6. 20
    0
      packages/zefyr/analysis_options.yaml
  7. 9
    0
      packages/zefyr/example/.gitignore
  8. 8
    0
      packages/zefyr/example/.metadata
  9. 8
    0
      packages/zefyr/example/README.md
  10. 10
    0
      packages/zefyr/example/android/.gitignore
  11. 61
    0
      packages/zefyr/example/android/app/build.gradle
  12. 39
    0
      packages/zefyr/example/android/app/src/main/AndroidManifest.xml
  13. 13
    0
      packages/zefyr/example/android/app/src/main/java/com/zefyr/example/MainActivity.java
  14. 12
    0
      packages/zefyr/example/android/app/src/main/res/drawable/launch_background.xml
  15. BIN
      packages/zefyr/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  16. BIN
      packages/zefyr/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  17. BIN
      packages/zefyr/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  18. BIN
      packages/zefyr/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  19. BIN
      packages/zefyr/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  20. 8
    0
      packages/zefyr/example/android/app/src/main/res/values/styles.xml
  21. 29
    0
      packages/zefyr/example/android/build.gradle
  22. 1
    0
      packages/zefyr/example/android/gradle.properties
  23. BIN
      packages/zefyr/example/android/gradle/wrapper/gradle-wrapper.jar
  24. 6
    0
      packages/zefyr/example/android/gradle/wrapper/gradle-wrapper.properties
  25. 160
    0
      packages/zefyr/example/android/gradlew
  26. 90
    0
      packages/zefyr/example/android/gradlew.bat
  27. 15
    0
      packages/zefyr/example/android/settings.gradle
  28. 18
    0
      packages/zefyr/example/example.iml
  29. 27
    0
      packages/zefyr/example/example_android.iml
  30. 45
    0
      packages/zefyr/example/ios/.gitignore
  31. 26
    0
      packages/zefyr/example/ios/Flutter/AppFrameworkInfo.plist
  32. 2
    0
      packages/zefyr/example/ios/Flutter/Debug.xcconfig
  33. 2
    0
      packages/zefyr/example/ios/Flutter/Release.xcconfig
  34. 63
    0
      packages/zefyr/example/ios/Podfile
  35. 22
    0
      packages/zefyr/example/ios/Podfile.lock
  36. 495
    0
      packages/zefyr/example/ios/Runner.xcodeproj/project.pbxproj
  37. 7
    0
      packages/zefyr/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  38. 93
    0
      packages/zefyr/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  39. 10
    0
      packages/zefyr/example/ios/Runner.xcworkspace/contents.xcworkspacedata
  40. 6
    0
      packages/zefyr/example/ios/Runner/AppDelegate.h
  41. 13
    0
      packages/zefyr/example/ios/Runner/AppDelegate.m
  42. 122
    0
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  43. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  44. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  45. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  46. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  47. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  48. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  49. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  50. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  51. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  52. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  53. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  54. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  55. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  56. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  57. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  58. 23
    0
      packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  59. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  60. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  61. BIN
      packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  62. 5
    0
      packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  63. 37
    0
      packages/zefyr/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
  64. 26
    0
      packages/zefyr/example/ios/Runner/Base.lproj/Main.storyboard
  65. 45
    0
      packages/zefyr/example/ios/Runner/Info.plist
  66. 9
    0
      packages/zefyr/example/ios/Runner/main.m
  67. 102
    0
      packages/zefyr/example/lib/main.dart
  68. 67
    0
      packages/zefyr/example/pubspec.yaml
  69. 40
    0
      packages/zefyr/lib/src/fast_diff.dart
  70. 512
    0
      packages/zefyr/lib/src/widgets/buttons.dart
  71. 26
    0
      packages/zefyr/lib/src/widgets/caret.dart
  72. 47
    0
      packages/zefyr/lib/src/widgets/code.dart
  73. 155
    0
      packages/zefyr/lib/src/widgets/common.dart
  74. 164
    0
      packages/zefyr/lib/src/widgets/controller.dart
  75. 18
    0
      packages/zefyr/lib/src/widgets/editable_box.dart
  76. 271
    0
      packages/zefyr/lib/src/widgets/editable_image.dart
  77. 333
    0
      packages/zefyr/lib/src/widgets/editable_paragraph.dart
  78. 322
    0
      packages/zefyr/lib/src/widgets/editable_text.dart
  79. 206
    0
      packages/zefyr/lib/src/widgets/editor.dart
  80. 277
    0
      packages/zefyr/lib/src/widgets/horizontal_rule.dart
  81. 123
    0
      packages/zefyr/lib/src/widgets/input.dart
  82. 86
    0
      packages/zefyr/lib/src/widgets/list.dart
  83. 68
    0
      packages/zefyr/lib/src/widgets/paragraph.dart
  84. 55
    0
      packages/zefyr/lib/src/widgets/quote.dart
  85. 116
    0
      packages/zefyr/lib/src/widgets/render_context.dart
  86. 453
    0
      packages/zefyr/lib/src/widgets/selection.dart
  87. 306
    0
      packages/zefyr/lib/src/widgets/theme.dart
  88. 392
    0
      packages/zefyr/lib/src/widgets/toolbar.dart
  89. 35
    0
      packages/zefyr/lib/util.dart
  90. 18
    0
      packages/zefyr/lib/widgets.dart
  91. 18
    0
      packages/zefyr/lib/zefyr.dart
  92. 19
    0
      packages/zefyr/pubspec.yaml
  93. 37
    0
      packages/zefyr/test/fast_diff_test.dart
  94. 22
    0
      packages/zefyr/test/painting/caret_painter_test.dart
  95. 49
    0
      packages/zefyr/test/rendering/render_editable_paragraph_test.dart
  96. 93
    0
      packages/zefyr/test/testing.dart
  97. 43
    0
      packages/zefyr/test/util_test.dart
  98. 135
    0
      packages/zefyr/test/widgets/buttons_test.dart
  99. 21
    0
      packages/zefyr/test/widgets/code_test.dart
  100. 0
    0
      packages/zefyr/test/widgets/controller_test.dart

+ 15
- 0
packages/zefyr/.gitignore Целия файл

@@ -0,0 +1,15 @@
1
+.DS_Store
2
+.atom/
3
+.idea
4
+.vscode/
5
+*.code-workspace
6
+.packages
7
+.pub/
8
+packages
9
+pubspec.lock
10
+coverage/
11
+ios/Flutter/Generated.xcconfig
12
+.flutter-plugins
13
+example/ios/.symlinks
14
+example/ios/Flutter/Generated.xcconfig
15
+doc/api/

+ 8
- 0
packages/zefyr/AUTHORS Целия файл

@@ -0,0 +1,8 @@
1
+# Below is a list of people and organizations that have contributed
2
+# to the Zefyr project. Names should be added to the list like so:
3
+#
4
+#   Name/Organization <email address>
5
+
6
+Memspace <team@memspace.app>
7
+
8
+Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>

+ 3
- 0
packages/zefyr/CHANGELOG.md Целия файл

@@ -0,0 +1,3 @@
1
+## [0.1.0] - Add release date
2
+
3
+*  Initial release.

+ 23
- 0
packages/zefyr/LICENSE Целия файл

@@ -0,0 +1,23 @@
1
+Copyright 2018, the Zefyr project authors. All rights reserved.
2
+
3
+Redistribution and use in source and binary forms, with or without
4
+modification, are permitted provided that the following conditions are met:
5
+    * Redistributions of source code must retain the above copyright
6
+      notice, this list of conditions and the following disclaimer.
7
+    * Redistributions in binary form must reproduce the above copyright
8
+      notice, this list of conditions and the following disclaimer in the
9
+      documentation and/or other materials provided with the distribution.
10
+    * Neither the name of the <organization> nor the
11
+      names of its contributors may be used to endorse or promote products
12
+      derived from this software without specific prior written permission.
13
+
14
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
18
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 59
- 0
packages/zefyr/README.md Целия файл

@@ -0,0 +1,59 @@
1
+# Zefyr [![Build Status](https://travis-ci.org/memspace/zefyr.svg?branch=master)](https://travis-ci.org/memspace/zefyr)
2
+
3
+*Soft and gentle rich text editing for Flutter applications.*
4
+
5
+Zefyr is currently in **early preview**. If you have a feature
6
+request or found a bug, please file it at the [issue tracker][].
7
+
8
+[issue tracker]: https://github.com/memspace/zefyr/issues
9
+
10
+### Documentation
11
+
12
+* [Quick Start][quick_start]
13
+* [Data Format and Document Model][data_and_document]
14
+* [Style attributes][attributes]
15
+* [Heuristic rules][heuristics]
16
+* [FAQ][faq]
17
+
18
+[quick_start]: /doc/quick_start.md
19
+[data_and_document]: /doc/data_and_document.md
20
+[attributes]: /doc/attributes.md
21
+[heuristics]: /doc/heuristics.md
22
+[faq]: /doc/faq.md
23
+
24
+## Clean and modern look
25
+
26
+Zefyr's rich text editor is built with simplicity and flexibility in
27
+mind. It provides clean interface for distraction-free editing. Think
28
+Medium.com-like experience.
29
+
30
+[screenshot]
31
+
32
+## Markdown-style semantics
33
+
34
+Ever needed to have a heading line inside of a quote block, like in
35
+this Markdown block:
36
+
37
+> ### I'm a Markdown heading
38
+> And I'm a regular paragraph
39
+
40
+Zefyr can deliver exactly that:
41
+
42
+[screenshot]
43
+
44
+> Nested blocks support (e.g. lists inside quote blocks) is planned.
45
+
46
+## Collaborative editing built-in
47
+
48
+Zefyr's document model uses data format compatible with
49
+[Operational Transformation][ot] which makes it possible to use for
50
+collaborative editing use cases. Or whenever there is a need for
51
+conflict-free resolution of changes.
52
+
53
+> Zefyr editor uses Quill.js **Delta** as underlying data format. Read
54
+> more about Zefyr and Deltas in our [documentation][data_and_document].
55
+> Make sure to checkout [official documentation][delta] for Delta format
56
+> as well.
57
+
58
+[delta]: https://quilljs.com/docs/delta/
59
+[ot]: https://en.wikipedia.org/wiki/Operational_transformation

+ 20
- 0
packages/zefyr/analysis_options.yaml Целия файл

@@ -0,0 +1,20 @@
1
+analyzer:
2
+  strong-mode: true
3
+  language:
4
+    enableSuperMixins: true
5
+
6
+# Lint rules and documentation, see http://dart-lang.github.io/linter/lints
7
+linter:
8
+  rules:
9
+    # - avoid_init_to_null
10
+    - cancel_subscriptions
11
+    - close_sinks
12
+    - directives_ordering
13
+    - hash_and_equals
14
+    - iterable_contains_unrelated_type
15
+    - list_remove_unrelated_type
16
+    - prefer_final_fields
17
+    - prefer_is_not_empty
18
+    - test_types_in_equals
19
+    - unrelated_type_equality_checks
20
+    - valid_regexps

+ 9
- 0
packages/zefyr/example/.gitignore Целия файл

@@ -0,0 +1,9 @@
1
+.DS_Store
2
+.dart_tool/
3
+
4
+.packages
5
+.pub/
6
+
7
+build/
8
+
9
+.flutter-plugins

+ 8
- 0
packages/zefyr/example/.metadata Целия файл

@@ -0,0 +1,8 @@
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: 3351423a42b05e3e099a9227b1f52d24c0306d67
8
+  channel: master

+ 8
- 0
packages/zefyr/example/README.md Целия файл

@@ -0,0 +1,8 @@
1
+# example
2
+
3
+A new Flutter project.
4
+
5
+## Getting Started
6
+
7
+For help getting started with Flutter, view our online
8
+[documentation](https://flutter.io/).

+ 10
- 0
packages/zefyr/example/android/.gitignore Целия файл

@@ -0,0 +1,10 @@
1
+*.iml
2
+*.class
3
+.gradle
4
+/local.properties
5
+/.idea/workspace.xml
6
+/.idea/libraries
7
+.DS_Store
8
+/build
9
+/captures
10
+GeneratedPluginRegistrant.java

+ 61
- 0
packages/zefyr/example/android/app/build.gradle Целия файл

@@ -0,0 +1,61 @@
1
+def localProperties = new Properties()
2
+def localPropertiesFile = rootProject.file('local.properties')
3
+if (localPropertiesFile.exists()) {
4
+    localPropertiesFile.withReader('UTF-8') { reader ->
5
+        localProperties.load(reader)
6
+    }
7
+}
8
+
9
+def flutterRoot = localProperties.getProperty('flutter.sdk')
10
+if (flutterRoot == null) {
11
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12
+}
13
+
14
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15
+if (flutterVersionCode == null) {
16
+    throw new GradleException("versionCode not found. Define flutter.versionCode in the local.properties file.")
17
+}
18
+
19
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
20
+if (flutterVersionName == null) {
21
+    throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.")
22
+}
23
+
24
+apply plugin: 'com.android.application'
25
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
26
+
27
+android {
28
+    compileSdkVersion 27
29
+
30
+    lintOptions {
31
+        disable 'InvalidPackage'
32
+    }
33
+
34
+    defaultConfig {
35
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
36
+        applicationId "com.zefyr.example"
37
+        minSdkVersion 16
38
+        targetSdkVersion 27
39
+        versionCode flutterVersionCode.toInteger()
40
+        versionName flutterVersionName
41
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
42
+    }
43
+
44
+    buildTypes {
45
+        release {
46
+            // TODO: Add your own signing config for the release build.
47
+            // Signing with the debug keys for now, so `flutter run --release` works.
48
+            signingConfig signingConfigs.debug
49
+        }
50
+    }
51
+}
52
+
53
+flutter {
54
+    source '../..'
55
+}
56
+
57
+dependencies {
58
+    testImplementation 'junit:junit:4.12'
59
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
60
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
61
+}

+ 39
- 0
packages/zefyr/example/android/app/src/main/AndroidManifest.xml Целия файл

@@ -0,0 +1,39 @@
1
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+    package="com.zefyr.example">
3
+
4
+    <!-- The INTERNET permission is required for development. Specifically,
5
+         flutter needs it to communicate with the running application
6
+         to allow setting breakpoints, to provide hot reload, etc.
7
+    -->
8
+    <uses-permission android:name="android.permission.INTERNET"/>
9
+
10
+    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
11
+         calls FlutterMain.startInitialization(this); in its onCreate method.
12
+         In most cases you can leave this as-is, but you if you want to provide
13
+         additional functionality it is fine to subclass or reimplement
14
+         FlutterApplication and put your custom class here. -->
15
+    <application
16
+        android:name="io.flutter.app.FlutterApplication"
17
+        android:label="example"
18
+        android:icon="@mipmap/ic_launcher">
19
+        <activity
20
+            android:name=".MainActivity"
21
+            android:launchMode="singleTop"
22
+            android:theme="@style/LaunchTheme"
23
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
24
+            android:hardwareAccelerated="true"
25
+            android:windowSoftInputMode="adjustResize">
26
+            <!-- This keeps the window background of the activity showing
27
+                 until Flutter renders its first frame. It can be removed if
28
+                 there is no splash screen (such as the default splash screen
29
+                 defined in @style/LaunchTheme). -->
30
+            <meta-data
31
+                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
32
+                android:value="true" />
33
+            <intent-filter>
34
+                <action android:name="android.intent.action.MAIN"/>
35
+                <category android:name="android.intent.category.LAUNCHER"/>
36
+            </intent-filter>
37
+        </activity>
38
+    </application>
39
+</manifest>

+ 13
- 0
packages/zefyr/example/android/app/src/main/java/com/zefyr/example/MainActivity.java Целия файл

@@ -0,0 +1,13 @@
1
+package com.zefyr.example;
2
+
3
+import android.os.Bundle;
4
+import io.flutter.app.FlutterActivity;
5
+import io.flutter.plugins.GeneratedPluginRegistrant;
6
+
7
+public class MainActivity extends FlutterActivity {
8
+  @Override
9
+  protected void onCreate(Bundle savedInstanceState) {
10
+    super.onCreate(savedInstanceState);
11
+    GeneratedPluginRegistrant.registerWith(this);
12
+  }
13
+}

+ 12
- 0
packages/zefyr/example/android/app/src/main/res/drawable/launch_background.xml Целия файл

@@ -0,0 +1,12 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Modify this file to customize your launch splash screen -->
3
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+    <item android:drawable="@android:color/white" />
5
+
6
+    <!-- You can insert your own image assets here -->
7
+    <!-- <item>
8
+        <bitmap
9
+            android:gravity="center"
10
+            android:src="@mipmap/launch_image" />
11
+    </item> -->
12
+</layer-list>

BIN
packages/zefyr/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png Целия файл


BIN
packages/zefyr/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png Целия файл


BIN
packages/zefyr/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png Целия файл


BIN
packages/zefyr/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Целия файл


BIN
packages/zefyr/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Целия файл


+ 8
- 0
packages/zefyr/example/android/app/src/main/res/values/styles.xml Целия файл

@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
4
+        <!-- Show a splash screen on the activity. Automatically removed when
5
+             Flutter draws its first frame -->
6
+        <item name="android:windowBackground">@drawable/launch_background</item>
7
+    </style>
8
+</resources>

+ 29
- 0
packages/zefyr/example/android/build.gradle Целия файл

@@ -0,0 +1,29 @@
1
+buildscript {
2
+    repositories {
3
+        google()
4
+        jcenter()
5
+    }
6
+
7
+    dependencies {
8
+        classpath 'com.android.tools.build:gradle:3.1.2'
9
+    }
10
+}
11
+
12
+allprojects {
13
+    repositories {
14
+        google()
15
+        jcenter()
16
+    }
17
+}
18
+
19
+rootProject.buildDir = '../build'
20
+subprojects {
21
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
22
+}
23
+subprojects {
24
+    project.evaluationDependsOn(':app')
25
+}
26
+
27
+task clean(type: Delete) {
28
+    delete rootProject.buildDir
29
+}

+ 1
- 0
packages/zefyr/example/android/gradle.properties Целия файл

@@ -0,0 +1 @@
1
+org.gradle.jvmargs=-Xmx1536M

BIN
packages/zefyr/example/android/gradle/wrapper/gradle-wrapper.jar Целия файл


+ 6
- 0
packages/zefyr/example/android/gradle/wrapper/gradle-wrapper.properties Целия файл

@@ -0,0 +1,6 @@
1
+#Fri Jun 23 08:50:38 CEST 2017
2
+distributionBase=GRADLE_USER_HOME
3
+distributionPath=wrapper/dists
4
+zipStoreBase=GRADLE_USER_HOME
5
+zipStorePath=wrapper/dists
6
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

+ 160
- 0
packages/zefyr/example/android/gradlew Целия файл

@@ -0,0 +1,160 @@
1
+#!/usr/bin/env bash
2
+
3
+##############################################################################
4
+##
5
+##  Gradle start up script for UN*X
6
+##
7
+##############################################################################
8
+
9
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10
+DEFAULT_JVM_OPTS=""
11
+
12
+APP_NAME="Gradle"
13
+APP_BASE_NAME=`basename "$0"`
14
+
15
+# Use the maximum available, or set MAX_FD != -1 to use that value.
16
+MAX_FD="maximum"
17
+
18
+warn ( ) {
19
+    echo "$*"
20
+}
21
+
22
+die ( ) {
23
+    echo
24
+    echo "$*"
25
+    echo
26
+    exit 1
27
+}
28
+
29
+# OS specific support (must be 'true' or 'false').
30
+cygwin=false
31
+msys=false
32
+darwin=false
33
+case "`uname`" in
34
+  CYGWIN* )
35
+    cygwin=true
36
+    ;;
37
+  Darwin* )
38
+    darwin=true
39
+    ;;
40
+  MINGW* )
41
+    msys=true
42
+    ;;
43
+esac
44
+
45
+# Attempt to set APP_HOME
46
+# Resolve links: $0 may be a link
47
+PRG="$0"
48
+# Need this for relative symlinks.
49
+while [ -h "$PRG" ] ; do
50
+    ls=`ls -ld "$PRG"`
51
+    link=`expr "$ls" : '.*-> \(.*\)$'`
52
+    if expr "$link" : '/.*' > /dev/null; then
53
+        PRG="$link"
54
+    else
55
+        PRG=`dirname "$PRG"`"/$link"
56
+    fi
57
+done
58
+SAVED="`pwd`"
59
+cd "`dirname \"$PRG\"`/" >/dev/null
60
+APP_HOME="`pwd -P`"
61
+cd "$SAVED" >/dev/null
62
+
63
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64
+
65
+# Determine the Java command to use to start the JVM.
66
+if [ -n "$JAVA_HOME" ] ; then
67
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68
+        # IBM's JDK on AIX uses strange locations for the executables
69
+        JAVACMD="$JAVA_HOME/jre/sh/java"
70
+    else
71
+        JAVACMD="$JAVA_HOME/bin/java"
72
+    fi
73
+    if [ ! -x "$JAVACMD" ] ; then
74
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75
+
76
+Please set the JAVA_HOME variable in your environment to match the
77
+location of your Java installation."
78
+    fi
79
+else
80
+    JAVACMD="java"
81
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82
+
83
+Please set the JAVA_HOME variable in your environment to match the
84
+location of your Java installation."
85
+fi
86
+
87
+# Increase the maximum file descriptors if we can.
88
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89
+    MAX_FD_LIMIT=`ulimit -H -n`
90
+    if [ $? -eq 0 ] ; then
91
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92
+            MAX_FD="$MAX_FD_LIMIT"
93
+        fi
94
+        ulimit -n $MAX_FD
95
+        if [ $? -ne 0 ] ; then
96
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
97
+        fi
98
+    else
99
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100
+    fi
101
+fi
102
+
103
+# For Darwin, add options to specify how the application appears in the dock
104
+if $darwin; then
105
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106
+fi
107
+
108
+# For Cygwin, switch paths to Windows format before running java
109
+if $cygwin ; then
110
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112
+    JAVACMD=`cygpath --unix "$JAVACMD"`
113
+
114
+    # We build the pattern for arguments to be converted via cygpath
115
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116
+    SEP=""
117
+    for dir in $ROOTDIRSRAW ; do
118
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
119
+        SEP="|"
120
+    done
121
+    OURCYGPATTERN="(^($ROOTDIRS))"
122
+    # Add a user-defined pattern to the cygpath arguments
123
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125
+    fi
126
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
127
+    i=0
128
+    for arg in "$@" ; do
129
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
131
+
132
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
133
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134
+        else
135
+            eval `echo args$i`="\"$arg\""
136
+        fi
137
+        i=$((i+1))
138
+    done
139
+    case $i in
140
+        (0) set -- ;;
141
+        (1) set -- "$args0" ;;
142
+        (2) set -- "$args0" "$args1" ;;
143
+        (3) set -- "$args0" "$args1" "$args2" ;;
144
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150
+    esac
151
+fi
152
+
153
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154
+function splitJvmOpts() {
155
+    JVM_OPTS=("$@")
156
+}
157
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159
+
160
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90
- 0
packages/zefyr/example/android/gradlew.bat Целия файл

@@ -0,0 +1,90 @@
1
+@if "%DEBUG%" == "" @echo off
2
+@rem ##########################################################################
3
+@rem
4
+@rem  Gradle startup script for Windows
5
+@rem
6
+@rem ##########################################################################
7
+
8
+@rem Set local scope for the variables with windows NT shell
9
+if "%OS%"=="Windows_NT" setlocal
10
+
11
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12
+set DEFAULT_JVM_OPTS=
13
+
14
+set DIRNAME=%~dp0
15
+if "%DIRNAME%" == "" set DIRNAME=.
16
+set APP_BASE_NAME=%~n0
17
+set APP_HOME=%DIRNAME%
18
+
19
+@rem Find java.exe
20
+if defined JAVA_HOME goto findJavaFromJavaHome
21
+
22
+set JAVA_EXE=java.exe
23
+%JAVA_EXE% -version >NUL 2>&1
24
+if "%ERRORLEVEL%" == "0" goto init
25
+
26
+echo.
27
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28
+echo.
29
+echo Please set the JAVA_HOME variable in your environment to match the
30
+echo location of your Java installation.
31
+
32
+goto fail
33
+
34
+:findJavaFromJavaHome
35
+set JAVA_HOME=%JAVA_HOME:"=%
36
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
+
38
+if exist "%JAVA_EXE%" goto init
39
+
40
+echo.
41
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42
+echo.
43
+echo Please set the JAVA_HOME variable in your environment to match the
44
+echo location of your Java installation.
45
+
46
+goto fail
47
+
48
+:init
49
+@rem Get command-line arguments, handling Windowz variants
50
+
51
+if not "%OS%" == "Windows_NT" goto win9xME_args
52
+if "%@eval[2+2]" == "4" goto 4NT_args
53
+
54
+:win9xME_args
55
+@rem Slurp the command line arguments.
56
+set CMD_LINE_ARGS=
57
+set _SKIP=2
58
+
59
+:win9xME_args_slurp
60
+if "x%~1" == "x" goto execute
61
+
62
+set CMD_LINE_ARGS=%*
63
+goto execute
64
+
65
+:4NT_args
66
+@rem Get arguments from the 4NT Shell from JP Software
67
+set CMD_LINE_ARGS=%$
68
+
69
+:execute
70
+@rem Setup the command line
71
+
72
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73
+
74
+@rem Execute Gradle
75
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76
+
77
+:end
78
+@rem End local scope for the variables with windows NT shell
79
+if "%ERRORLEVEL%"=="0" goto mainEnd
80
+
81
+:fail
82
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83
+rem the _cmd.exe /c_ return code!
84
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85
+exit /b 1
86
+
87
+:mainEnd
88
+if "%OS%"=="Windows_NT" endlocal
89
+
90
+:omega

+ 15
- 0
packages/zefyr/example/android/settings.gradle Целия файл

@@ -0,0 +1,15 @@
1
+include ':app'
2
+
3
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4
+
5
+def plugins = new Properties()
6
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7
+if (pluginsFile.exists()) {
8
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9
+}
10
+
11
+plugins.each { name, path ->
12
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13
+    include ":$name"
14
+    project(":$name").projectDir = pluginDirectory
15
+}

+ 18
- 0
packages/zefyr/example/example.iml Целия файл

@@ -0,0 +1,18 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<module type="JAVA_MODULE" version="4">
3
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+    <exclude-output />
5
+    <content url="file://$MODULE_DIR$">
6
+      <sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
7
+      <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
8
+      <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
9
+      <excludeFolder url="file://$MODULE_DIR$/.idea" />
10
+      <excludeFolder url="file://$MODULE_DIR$/.pub" />
11
+      <excludeFolder url="file://$MODULE_DIR$/build" />
12
+    </content>
13
+    <orderEntry type="sourceFolder" forTests="false" />
14
+    <orderEntry type="library" name="Dart SDK" level="project" />
15
+    <orderEntry type="library" name="Flutter Plugins" level="project" />
16
+    <orderEntry type="library" name="Dart Packages" level="project" />
17
+  </component>
18
+</module>

+ 27
- 0
packages/zefyr/example/example_android.iml Целия файл

@@ -0,0 +1,27 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<module type="JAVA_MODULE" version="4">
3
+  <component name="FacetManager">
4
+    <facet type="android" name="Android">
5
+      <configuration>
6
+        <option name="ALLOW_USER_CONFIGURATION" value="false" />
7
+        <option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/android/gen" />
8
+        <option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/android/gen" />
9
+        <option name="MANIFEST_FILE_RELATIVE_PATH" value="/android/AndroidManifest.xml" />
10
+        <option name="RES_FOLDER_RELATIVE_PATH" value="/android/res" />
11
+        <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/android/assets" />
12
+        <option name="LIBS_FOLDER_RELATIVE_PATH" value="/android/libs" />
13
+        <option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/android/proguard_logs" />
14
+      </configuration>
15
+    </facet>
16
+  </component>
17
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
18
+    <exclude-output />
19
+    <content url="file://$MODULE_DIR$/android">
20
+      <sourceFolder url="file://$MODULE_DIR$/android/app/src/main/java" isTestSource="false" />
21
+      <sourceFolder url="file://$MODULE_DIR$/android/gen" isTestSource="false" generated="true" />
22
+    </content>
23
+    <orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
24
+    <orderEntry type="sourceFolder" forTests="false" />
25
+    <orderEntry type="library" name="Flutter for Android" level="project" />
26
+  </component>
27
+</module>

+ 45
- 0
packages/zefyr/example/ios/.gitignore Целия файл

@@ -0,0 +1,45 @@
1
+.idea/
2
+.vagrant/
3
+.sconsign.dblite
4
+.svn/
5
+
6
+.DS_Store
7
+*.swp
8
+profile
9
+
10
+DerivedData/
11
+build/
12
+GeneratedPluginRegistrant.h
13
+GeneratedPluginRegistrant.m
14
+
15
+.generated/
16
+
17
+*.pbxuser
18
+*.mode1v3
19
+*.mode2v3
20
+*.perspectivev3
21
+
22
+!default.pbxuser
23
+!default.mode1v3
24
+!default.mode2v3
25
+!default.perspectivev3
26
+
27
+xcuserdata
28
+
29
+*.moved-aside
30
+
31
+*.pyc
32
+*sync/
33
+Icon?
34
+.tags*
35
+
36
+/Flutter/app.flx
37
+/Flutter/app.zip
38
+/Flutter/flutter_assets/
39
+/Flutter/App.framework
40
+/Flutter/Flutter.framework
41
+/Flutter/Generated.xcconfig
42
+/ServiceDefinitions.json
43
+
44
+Pods/
45
+.symlinks/

+ 26
- 0
packages/zefyr/example/ios/Flutter/AppFrameworkInfo.plist Целия файл

@@ -0,0 +1,26 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+  <key>CFBundleDevelopmentRegion</key>
6
+  <string>en</string>
7
+  <key>CFBundleExecutable</key>
8
+  <string>App</string>
9
+  <key>CFBundleIdentifier</key>
10
+  <string>io.flutter.flutter.app</string>
11
+  <key>CFBundleInfoDictionaryVersion</key>
12
+  <string>6.0</string>
13
+  <key>CFBundleName</key>
14
+  <string>App</string>
15
+  <key>CFBundlePackageType</key>
16
+  <string>FMWK</string>
17
+  <key>CFBundleShortVersionString</key>
18
+  <string>1.0</string>
19
+  <key>CFBundleSignature</key>
20
+  <string>????</string>
21
+  <key>CFBundleVersion</key>
22
+  <string>1.0</string>
23
+  <key>MinimumOSVersion</key>
24
+  <string>8.0</string>
25
+</dict>
26
+</plist>

+ 2
- 0
packages/zefyr/example/ios/Flutter/Debug.xcconfig Целия файл

@@ -0,0 +1,2 @@
1
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2
+#include "Generated.xcconfig"

+ 2
- 0
packages/zefyr/example/ios/Flutter/Release.xcconfig Целия файл

@@ -0,0 +1,2 @@
1
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2
+#include "Generated.xcconfig"

+ 63
- 0
packages/zefyr/example/ios/Podfile Целия файл

@@ -0,0 +1,63 @@
1
+# Uncomment this line to define a global platform for your project
2
+# platform :ios, '9.0'
3
+
4
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6
+
7
+def parse_KV_file(file, separator='=')
8
+  file_abs_path = File.expand_path(file)
9
+  if !File.exists? file_abs_path
10
+    return [];
11
+  end
12
+  pods_ary = []
13
+  skip_line_start_symbols = ["#", "/"]
14
+  File.foreach(file_abs_path) { |line|
15
+      next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
16
+      plugin = line.split(pattern=separator)
17
+      if plugin.length == 2
18
+        podname = plugin[0].strip()
19
+        path = plugin[1].strip()
20
+        podpath = File.expand_path("#{path}", file_abs_path)
21
+        pods_ary.push({:name => podname, :path => podpath});
22
+      else
23
+        puts "Invalid plugin specification: #{line}"
24
+      end
25
+  }
26
+  return pods_ary
27
+end
28
+
29
+target 'Runner' do
30
+  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
31
+  # referring to absolute paths on developers' machines.
32
+  system('rm -rf .symlinks')
33
+  system('mkdir -p .symlinks/plugins')
34
+
35
+  # Flutter Pods
36
+  generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
37
+  if generated_xcode_build_settings.empty?
38
+    puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
39
+  end
40
+  generated_xcode_build_settings.map { |p|
41
+    if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
42
+      symlink = File.join('.symlinks', 'flutter')
43
+      File.symlink(File.dirname(p[:path]), symlink)
44
+      pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
45
+    end
46
+  }
47
+
48
+  # Plugin Pods
49
+  plugin_pods = parse_KV_file('../.flutter-plugins')
50
+  plugin_pods.map { |p|
51
+    symlink = File.join('.symlinks', 'plugins', p[:name])
52
+    File.symlink(p[:path], symlink)
53
+    pod p[:name], :path => File.join(symlink, 'ios')
54
+  }
55
+end
56
+
57
+post_install do |installer|
58
+  installer.pods_project.targets.each do |target|
59
+    target.build_configurations.each do |config|
60
+      config.build_settings['ENABLE_BITCODE'] = 'NO'
61
+    end
62
+  end
63
+end

+ 22
- 0
packages/zefyr/example/ios/Podfile.lock Целия файл

@@ -0,0 +1,22 @@
1
+PODS:
2
+  - Flutter (1.0.0)
3
+  - url_launcher (0.0.1):
4
+    - Flutter
5
+
6
+DEPENDENCIES:
7
+  - Flutter (from `.symlinks/flutter/ios`)
8
+  - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
9
+
10
+EXTERNAL SOURCES:
11
+  Flutter:
12
+    :path: ".symlinks/flutter/ios"
13
+  url_launcher:
14
+    :path: ".symlinks/plugins/url_launcher/ios"
15
+
16
+SPEC CHECKSUMS:
17
+  Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296
18
+  url_launcher: 92b89c1029a0373879933c21642958c874539095
19
+
20
+PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2
21
+
22
+COCOAPODS: 1.5.2

+ 495
- 0
packages/zefyr/example/ios/Runner.xcodeproj/project.pbxproj Целия файл

@@ -0,0 +1,495 @@
1
+// !$*UTF8*$!
2
+{
3
+	archiveVersion = 1;
4
+	classes = {
5
+	};
6
+	objectVersion = 46;
7
+	objects = {
8
+
9
+/* Begin PBXBuildFile section */
10
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11
+		2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
12
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
13
+		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
14
+		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
15
+		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
16
+		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
17
+		9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
18
+		9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };
19
+		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
20
+		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
21
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
22
+		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
23
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
24
+		A287E1B0D09CE333E91C72F8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A1CCE149440E9C6B5CCC0FA7 /* libPods-Runner.a */; };
25
+/* End PBXBuildFile section */
26
+
27
+/* Begin PBXCopyFilesBuildPhase section */
28
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
29
+			isa = PBXCopyFilesBuildPhase;
30
+			buildActionMask = 2147483647;
31
+			dstPath = "";
32
+			dstSubfolderSpec = 10;
33
+			files = (
34
+				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
35
+				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
36
+			);
37
+			name = "Embed Frameworks";
38
+			runOnlyForDeploymentPostprocessing = 0;
39
+		};
40
+/* End PBXCopyFilesBuildPhase section */
41
+
42
+/* Begin PBXFileReference section */
43
+		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
44
+		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
45
+		2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
46
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
47
+		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
48
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
49
+		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
50
+		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
51
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
52
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
53
+		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
54
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
55
+		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
56
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
57
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
58
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
59
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
60
+		A1CCE149440E9C6B5CCC0FA7 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
61
+/* End PBXFileReference section */
62
+
63
+/* Begin PBXFrameworksBuildPhase section */
64
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
65
+			isa = PBXFrameworksBuildPhase;
66
+			buildActionMask = 2147483647;
67
+			files = (
68
+				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
69
+				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
70
+				A287E1B0D09CE333E91C72F8 /* libPods-Runner.a in Frameworks */,
71
+			);
72
+			runOnlyForDeploymentPostprocessing = 0;
73
+		};
74
+/* End PBXFrameworksBuildPhase section */
75
+
76
+/* Begin PBXGroup section */
77
+		8488E8CB1FA32CE8522EADB7 /* Frameworks */ = {
78
+			isa = PBXGroup;
79
+			children = (
80
+				A1CCE149440E9C6B5CCC0FA7 /* libPods-Runner.a */,
81
+			);
82
+			name = Frameworks;
83
+			sourceTree = "<group>";
84
+		};
85
+		9740EEB11CF90186004384FC /* Flutter */ = {
86
+			isa = PBXGroup;
87
+			children = (
88
+				2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
89
+				3B80C3931E831B6300D905FE /* App.framework */,
90
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
91
+				9740EEBA1CF902C7004384FC /* Flutter.framework */,
92
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
93
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
94
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
95
+			);
96
+			name = Flutter;
97
+			sourceTree = "<group>";
98
+		};
99
+		97C146E51CF9000F007C117D = {
100
+			isa = PBXGroup;
101
+			children = (
102
+				9740EEB11CF90186004384FC /* Flutter */,
103
+				97C146F01CF9000F007C117D /* Runner */,
104
+				97C146EF1CF9000F007C117D /* Products */,
105
+				E477B343036FAA13A2565E3A /* Pods */,
106
+				8488E8CB1FA32CE8522EADB7 /* Frameworks */,
107
+			);
108
+			sourceTree = "<group>";
109
+		};
110
+		97C146EF1CF9000F007C117D /* Products */ = {
111
+			isa = PBXGroup;
112
+			children = (
113
+				97C146EE1CF9000F007C117D /* Runner.app */,
114
+			);
115
+			name = Products;
116
+			sourceTree = "<group>";
117
+		};
118
+		97C146F01CF9000F007C117D /* Runner */ = {
119
+			isa = PBXGroup;
120
+			children = (
121
+				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
122
+				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
123
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
124
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
125
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
126
+				97C147021CF9000F007C117D /* Info.plist */,
127
+				97C146F11CF9000F007C117D /* Supporting Files */,
128
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
129
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
130
+			);
131
+			path = Runner;
132
+			sourceTree = "<group>";
133
+		};
134
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
135
+			isa = PBXGroup;
136
+			children = (
137
+				97C146F21CF9000F007C117D /* main.m */,
138
+			);
139
+			name = "Supporting Files";
140
+			sourceTree = "<group>";
141
+		};
142
+		E477B343036FAA13A2565E3A /* Pods */ = {
143
+			isa = PBXGroup;
144
+			children = (
145
+			);
146
+			name = Pods;
147
+			sourceTree = "<group>";
148
+		};
149
+/* End PBXGroup section */
150
+
151
+/* Begin PBXNativeTarget section */
152
+		97C146ED1CF9000F007C117D /* Runner */ = {
153
+			isa = PBXNativeTarget;
154
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
155
+			buildPhases = (
156
+				497F746AAAAAA284327A4563 /* [CP] Check Pods Manifest.lock */,
157
+				9740EEB61CF901F6004384FC /* Run Script */,
158
+				97C146EA1CF9000F007C117D /* Sources */,
159
+				97C146EB1CF9000F007C117D /* Frameworks */,
160
+				97C146EC1CF9000F007C117D /* Resources */,
161
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
162
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
163
+				130991D2A439F8766FDFE1D7 /* [CP] Embed Pods Frameworks */,
164
+			);
165
+			buildRules = (
166
+			);
167
+			dependencies = (
168
+			);
169
+			name = Runner;
170
+			productName = Runner;
171
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
172
+			productType = "com.apple.product-type.application";
173
+		};
174
+/* End PBXNativeTarget section */
175
+
176
+/* Begin PBXProject section */
177
+		97C146E61CF9000F007C117D /* Project object */ = {
178
+			isa = PBXProject;
179
+			attributes = {
180
+				LastUpgradeCheck = 0910;
181
+				ORGANIZATIONNAME = "The Chromium Authors";
182
+				TargetAttributes = {
183
+					97C146ED1CF9000F007C117D = {
184
+						CreatedOnToolsVersion = 7.3.1;
185
+					};
186
+				};
187
+			};
188
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
189
+			compatibilityVersion = "Xcode 3.2";
190
+			developmentRegion = English;
191
+			hasScannedForEncodings = 0;
192
+			knownRegions = (
193
+				en,
194
+				Base,
195
+			);
196
+			mainGroup = 97C146E51CF9000F007C117D;
197
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
198
+			projectDirPath = "";
199
+			projectRoot = "";
200
+			targets = (
201
+				97C146ED1CF9000F007C117D /* Runner */,
202
+			);
203
+		};
204
+/* End PBXProject section */
205
+
206
+/* Begin PBXResourcesBuildPhase section */
207
+		97C146EC1CF9000F007C117D /* Resources */ = {
208
+			isa = PBXResourcesBuildPhase;
209
+			buildActionMask = 2147483647;
210
+			files = (
211
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
212
+				9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,
213
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
214
+				9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
215
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
216
+				2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
217
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
218
+			);
219
+			runOnlyForDeploymentPostprocessing = 0;
220
+		};
221
+/* End PBXResourcesBuildPhase section */
222
+
223
+/* Begin PBXShellScriptBuildPhase section */
224
+		130991D2A439F8766FDFE1D7 /* [CP] Embed Pods Frameworks */ = {
225
+			isa = PBXShellScriptBuildPhase;
226
+			buildActionMask = 2147483647;
227
+			files = (
228
+			);
229
+			inputPaths = (
230
+				"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
231
+				"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
232
+			);
233
+			name = "[CP] Embed Pods Frameworks";
234
+			outputPaths = (
235
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
236
+			);
237
+			runOnlyForDeploymentPostprocessing = 0;
238
+			shellPath = /bin/sh;
239
+			shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
240
+			showEnvVarsInLog = 0;
241
+		};
242
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
243
+			isa = PBXShellScriptBuildPhase;
244
+			buildActionMask = 2147483647;
245
+			files = (
246
+			);
247
+			inputPaths = (
248
+			);
249
+			name = "Thin Binary";
250
+			outputPaths = (
251
+			);
252
+			runOnlyForDeploymentPostprocessing = 0;
253
+			shellPath = /bin/sh;
254
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
255
+		};
256
+		497F746AAAAAA284327A4563 /* [CP] Check Pods Manifest.lock */ = {
257
+			isa = PBXShellScriptBuildPhase;
258
+			buildActionMask = 2147483647;
259
+			files = (
260
+			);
261
+			inputPaths = (
262
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
263
+				"${PODS_ROOT}/Manifest.lock",
264
+			);
265
+			name = "[CP] Check Pods Manifest.lock";
266
+			outputPaths = (
267
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
268
+			);
269
+			runOnlyForDeploymentPostprocessing = 0;
270
+			shellPath = /bin/sh;
271
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
272
+			showEnvVarsInLog = 0;
273
+		};
274
+		9740EEB61CF901F6004384FC /* Run Script */ = {
275
+			isa = PBXShellScriptBuildPhase;
276
+			buildActionMask = 2147483647;
277
+			files = (
278
+			);
279
+			inputPaths = (
280
+			);
281
+			name = "Run Script";
282
+			outputPaths = (
283
+			);
284
+			runOnlyForDeploymentPostprocessing = 0;
285
+			shellPath = /bin/sh;
286
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
287
+		};
288
+/* End PBXShellScriptBuildPhase section */
289
+
290
+/* Begin PBXSourcesBuildPhase section */
291
+		97C146EA1CF9000F007C117D /* Sources */ = {
292
+			isa = PBXSourcesBuildPhase;
293
+			buildActionMask = 2147483647;
294
+			files = (
295
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
296
+				97C146F31CF9000F007C117D /* main.m in Sources */,
297
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
298
+			);
299
+			runOnlyForDeploymentPostprocessing = 0;
300
+		};
301
+/* End PBXSourcesBuildPhase section */
302
+
303
+/* Begin PBXVariantGroup section */
304
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
305
+			isa = PBXVariantGroup;
306
+			children = (
307
+				97C146FB1CF9000F007C117D /* Base */,
308
+			);
309
+			name = Main.storyboard;
310
+			sourceTree = "<group>";
311
+		};
312
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
313
+			isa = PBXVariantGroup;
314
+			children = (
315
+				97C147001CF9000F007C117D /* Base */,
316
+			);
317
+			name = LaunchScreen.storyboard;
318
+			sourceTree = "<group>";
319
+		};
320
+/* End PBXVariantGroup section */
321
+
322
+/* Begin XCBuildConfiguration section */
323
+		97C147031CF9000F007C117D /* Debug */ = {
324
+			isa = XCBuildConfiguration;
325
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
326
+			buildSettings = {
327
+				ALWAYS_SEARCH_USER_PATHS = NO;
328
+				CLANG_ANALYZER_NONNULL = YES;
329
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
330
+				CLANG_CXX_LIBRARY = "libc++";
331
+				CLANG_ENABLE_MODULES = YES;
332
+				CLANG_ENABLE_OBJC_ARC = YES;
333
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
334
+				CLANG_WARN_BOOL_CONVERSION = YES;
335
+				CLANG_WARN_COMMA = YES;
336
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
337
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
338
+				CLANG_WARN_EMPTY_BODY = YES;
339
+				CLANG_WARN_ENUM_CONVERSION = YES;
340
+				CLANG_WARN_INFINITE_RECURSION = YES;
341
+				CLANG_WARN_INT_CONVERSION = YES;
342
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
343
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
344
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
345
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
346
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
347
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
348
+				CLANG_WARN_UNREACHABLE_CODE = YES;
349
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
350
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
351
+				COPY_PHASE_STRIP = NO;
352
+				DEBUG_INFORMATION_FORMAT = dwarf;
353
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
354
+				ENABLE_TESTABILITY = YES;
355
+				GCC_C_LANGUAGE_STANDARD = gnu99;
356
+				GCC_DYNAMIC_NO_PIC = NO;
357
+				GCC_NO_COMMON_BLOCKS = YES;
358
+				GCC_OPTIMIZATION_LEVEL = 0;
359
+				GCC_PREPROCESSOR_DEFINITIONS = (
360
+					"DEBUG=1",
361
+					"$(inherited)",
362
+				);
363
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
364
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
365
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
366
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
367
+				GCC_WARN_UNUSED_FUNCTION = YES;
368
+				GCC_WARN_UNUSED_VARIABLE = YES;
369
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
370
+				MTL_ENABLE_DEBUG_INFO = YES;
371
+				ONLY_ACTIVE_ARCH = YES;
372
+				SDKROOT = iphoneos;
373
+				TARGETED_DEVICE_FAMILY = "1,2";
374
+			};
375
+			name = Debug;
376
+		};
377
+		97C147041CF9000F007C117D /* Release */ = {
378
+			isa = XCBuildConfiguration;
379
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
380
+			buildSettings = {
381
+				ALWAYS_SEARCH_USER_PATHS = NO;
382
+				CLANG_ANALYZER_NONNULL = YES;
383
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
384
+				CLANG_CXX_LIBRARY = "libc++";
385
+				CLANG_ENABLE_MODULES = YES;
386
+				CLANG_ENABLE_OBJC_ARC = YES;
387
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
388
+				CLANG_WARN_BOOL_CONVERSION = YES;
389
+				CLANG_WARN_COMMA = YES;
390
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
391
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
392
+				CLANG_WARN_EMPTY_BODY = YES;
393
+				CLANG_WARN_ENUM_CONVERSION = YES;
394
+				CLANG_WARN_INFINITE_RECURSION = YES;
395
+				CLANG_WARN_INT_CONVERSION = YES;
396
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
397
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
398
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
399
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
400
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
401
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
402
+				CLANG_WARN_UNREACHABLE_CODE = YES;
403
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
404
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
405
+				COPY_PHASE_STRIP = NO;
406
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
407
+				ENABLE_NS_ASSERTIONS = NO;
408
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
409
+				GCC_C_LANGUAGE_STANDARD = gnu99;
410
+				GCC_NO_COMMON_BLOCKS = YES;
411
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
412
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
413
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
414
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
415
+				GCC_WARN_UNUSED_FUNCTION = YES;
416
+				GCC_WARN_UNUSED_VARIABLE = YES;
417
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
418
+				MTL_ENABLE_DEBUG_INFO = NO;
419
+				SDKROOT = iphoneos;
420
+				TARGETED_DEVICE_FAMILY = "1,2";
421
+				VALIDATE_PRODUCT = YES;
422
+			};
423
+			name = Release;
424
+		};
425
+		97C147061CF9000F007C117D /* Debug */ = {
426
+			isa = XCBuildConfiguration;
427
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
428
+			buildSettings = {
429
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
430
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
431
+				ENABLE_BITCODE = NO;
432
+				FRAMEWORK_SEARCH_PATHS = (
433
+					"$(inherited)",
434
+					"$(PROJECT_DIR)/Flutter",
435
+				);
436
+				INFOPLIST_FILE = Runner/Info.plist;
437
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
438
+				LIBRARY_SEARCH_PATHS = (
439
+					"$(inherited)",
440
+					"$(PROJECT_DIR)/Flutter",
441
+				);
442
+				PRODUCT_BUNDLE_IDENTIFIER = com.zefyr.example;
443
+				PRODUCT_NAME = "$(TARGET_NAME)";
444
+				VERSIONING_SYSTEM = "apple-generic";
445
+			};
446
+			name = Debug;
447
+		};
448
+		97C147071CF9000F007C117D /* Release */ = {
449
+			isa = XCBuildConfiguration;
450
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
451
+			buildSettings = {
452
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
453
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
454
+				ENABLE_BITCODE = NO;
455
+				FRAMEWORK_SEARCH_PATHS = (
456
+					"$(inherited)",
457
+					"$(PROJECT_DIR)/Flutter",
458
+				);
459
+				INFOPLIST_FILE = Runner/Info.plist;
460
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
461
+				LIBRARY_SEARCH_PATHS = (
462
+					"$(inherited)",
463
+					"$(PROJECT_DIR)/Flutter",
464
+				);
465
+				PRODUCT_BUNDLE_IDENTIFIER = com.zefyr.example;
466
+				PRODUCT_NAME = "$(TARGET_NAME)";
467
+				VERSIONING_SYSTEM = "apple-generic";
468
+			};
469
+			name = Release;
470
+		};
471
+/* End XCBuildConfiguration section */
472
+
473
+/* Begin XCConfigurationList section */
474
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
475
+			isa = XCConfigurationList;
476
+			buildConfigurations = (
477
+				97C147031CF9000F007C117D /* Debug */,
478
+				97C147041CF9000F007C117D /* Release */,
479
+			);
480
+			defaultConfigurationIsVisible = 0;
481
+			defaultConfigurationName = Release;
482
+		};
483
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
484
+			isa = XCConfigurationList;
485
+			buildConfigurations = (
486
+				97C147061CF9000F007C117D /* Debug */,
487
+				97C147071CF9000F007C117D /* Release */,
488
+			);
489
+			defaultConfigurationIsVisible = 0;
490
+			defaultConfigurationName = Release;
491
+		};
492
+/* End XCConfigurationList section */
493
+	};
494
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
495
+}

+ 7
- 0
packages/zefyr/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata Целия файл

@@ -0,0 +1,7 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Workspace
3
+   version = "1.0">
4
+   <FileRef
5
+      location = "group:Runner.xcodeproj">
6
+   </FileRef>
7
+</Workspace>

+ 93
- 0
packages/zefyr/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme Целия файл

@@ -0,0 +1,93 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Scheme
3
+   LastUpgradeVersion = "0910"
4
+   version = "1.3">
5
+   <BuildAction
6
+      parallelizeBuildables = "YES"
7
+      buildImplicitDependencies = "YES">
8
+      <BuildActionEntries>
9
+         <BuildActionEntry
10
+            buildForTesting = "YES"
11
+            buildForRunning = "YES"
12
+            buildForProfiling = "YES"
13
+            buildForArchiving = "YES"
14
+            buildForAnalyzing = "YES">
15
+            <BuildableReference
16
+               BuildableIdentifier = "primary"
17
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
18
+               BuildableName = "Runner.app"
19
+               BlueprintName = "Runner"
20
+               ReferencedContainer = "container:Runner.xcodeproj">
21
+            </BuildableReference>
22
+         </BuildActionEntry>
23
+      </BuildActionEntries>
24
+   </BuildAction>
25
+   <TestAction
26
+      buildConfiguration = "Debug"
27
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29
+      language = ""
30
+      shouldUseLaunchSchemeArgsEnv = "YES">
31
+      <Testables>
32
+      </Testables>
33
+      <MacroExpansion>
34
+         <BuildableReference
35
+            BuildableIdentifier = "primary"
36
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
37
+            BuildableName = "Runner.app"
38
+            BlueprintName = "Runner"
39
+            ReferencedContainer = "container:Runner.xcodeproj">
40
+         </BuildableReference>
41
+      </MacroExpansion>
42
+      <AdditionalOptions>
43
+      </AdditionalOptions>
44
+   </TestAction>
45
+   <LaunchAction
46
+      buildConfiguration = "Debug"
47
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
48
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
49
+      language = ""
50
+      launchStyle = "0"
51
+      useCustomWorkingDirectory = "NO"
52
+      ignoresPersistentStateOnLaunch = "NO"
53
+      debugDocumentVersioning = "YES"
54
+      debugServiceExtension = "internal"
55
+      allowLocationSimulation = "YES">
56
+      <BuildableProductRunnable
57
+         runnableDebuggingMode = "0">
58
+         <BuildableReference
59
+            BuildableIdentifier = "primary"
60
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
61
+            BuildableName = "Runner.app"
62
+            BlueprintName = "Runner"
63
+            ReferencedContainer = "container:Runner.xcodeproj">
64
+         </BuildableReference>
65
+      </BuildableProductRunnable>
66
+      <AdditionalOptions>
67
+      </AdditionalOptions>
68
+   </LaunchAction>
69
+   <ProfileAction
70
+      buildConfiguration = "Release"
71
+      shouldUseLaunchSchemeArgsEnv = "YES"
72
+      savedToolIdentifier = ""
73
+      useCustomWorkingDirectory = "NO"
74
+      debugDocumentVersioning = "YES">
75
+      <BuildableProductRunnable
76
+         runnableDebuggingMode = "0">
77
+         <BuildableReference
78
+            BuildableIdentifier = "primary"
79
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
80
+            BuildableName = "Runner.app"
81
+            BlueprintName = "Runner"
82
+            ReferencedContainer = "container:Runner.xcodeproj">
83
+         </BuildableReference>
84
+      </BuildableProductRunnable>
85
+   </ProfileAction>
86
+   <AnalyzeAction
87
+      buildConfiguration = "Debug">
88
+   </AnalyzeAction>
89
+   <ArchiveAction
90
+      buildConfiguration = "Release"
91
+      revealArchiveInOrganizer = "YES">
92
+   </ArchiveAction>
93
+</Scheme>

+ 10
- 0
packages/zefyr/example/ios/Runner.xcworkspace/contents.xcworkspacedata Целия файл

@@ -0,0 +1,10 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Workspace
3
+   version = "1.0">
4
+   <FileRef
5
+      location = "group:Runner.xcodeproj">
6
+   </FileRef>
7
+   <FileRef
8
+      location = "group:Pods/Pods.xcodeproj">
9
+   </FileRef>
10
+</Workspace>

+ 6
- 0
packages/zefyr/example/ios/Runner/AppDelegate.h Целия файл

@@ -0,0 +1,6 @@
1
+#import <Flutter/Flutter.h>
2
+#import <UIKit/UIKit.h>
3
+
4
+@interface AppDelegate : FlutterAppDelegate
5
+
6
+@end

+ 13
- 0
packages/zefyr/example/ios/Runner/AppDelegate.m Целия файл

@@ -0,0 +1,13 @@
1
+#include "AppDelegate.h"
2
+#include "GeneratedPluginRegistrant.h"
3
+
4
+@implementation AppDelegate
5
+
6
+- (BOOL)application:(UIApplication *)application
7
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
8
+  [GeneratedPluginRegistrant registerWithRegistry:self];
9
+  // Override point for customization after application launch.
10
+  return [super application:application didFinishLaunchingWithOptions:launchOptions];
11
+}
12
+
13
+@end

+ 122
- 0
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json Целия файл

@@ -0,0 +1,122 @@
1
+{
2
+  "images" : [
3
+    {
4
+      "size" : "20x20",
5
+      "idiom" : "iphone",
6
+      "filename" : "Icon-App-20x20@2x.png",
7
+      "scale" : "2x"
8
+    },
9
+    {
10
+      "size" : "20x20",
11
+      "idiom" : "iphone",
12
+      "filename" : "Icon-App-20x20@3x.png",
13
+      "scale" : "3x"
14
+    },
15
+    {
16
+      "size" : "29x29",
17
+      "idiom" : "iphone",
18
+      "filename" : "Icon-App-29x29@1x.png",
19
+      "scale" : "1x"
20
+    },
21
+    {
22
+      "size" : "29x29",
23
+      "idiom" : "iphone",
24
+      "filename" : "Icon-App-29x29@2x.png",
25
+      "scale" : "2x"
26
+    },
27
+    {
28
+      "size" : "29x29",
29
+      "idiom" : "iphone",
30
+      "filename" : "Icon-App-29x29@3x.png",
31
+      "scale" : "3x"
32
+    },
33
+    {
34
+      "size" : "40x40",
35
+      "idiom" : "iphone",
36
+      "filename" : "Icon-App-40x40@2x.png",
37
+      "scale" : "2x"
38
+    },
39
+    {
40
+      "size" : "40x40",
41
+      "idiom" : "iphone",
42
+      "filename" : "Icon-App-40x40@3x.png",
43
+      "scale" : "3x"
44
+    },
45
+    {
46
+      "size" : "60x60",
47
+      "idiom" : "iphone",
48
+      "filename" : "Icon-App-60x60@2x.png",
49
+      "scale" : "2x"
50
+    },
51
+    {
52
+      "size" : "60x60",
53
+      "idiom" : "iphone",
54
+      "filename" : "Icon-App-60x60@3x.png",
55
+      "scale" : "3x"
56
+    },
57
+    {
58
+      "size" : "20x20",
59
+      "idiom" : "ipad",
60
+      "filename" : "Icon-App-20x20@1x.png",
61
+      "scale" : "1x"
62
+    },
63
+    {
64
+      "size" : "20x20",
65
+      "idiom" : "ipad",
66
+      "filename" : "Icon-App-20x20@2x.png",
67
+      "scale" : "2x"
68
+    },
69
+    {
70
+      "size" : "29x29",
71
+      "idiom" : "ipad",
72
+      "filename" : "Icon-App-29x29@1x.png",
73
+      "scale" : "1x"
74
+    },
75
+    {
76
+      "size" : "29x29",
77
+      "idiom" : "ipad",
78
+      "filename" : "Icon-App-29x29@2x.png",
79
+      "scale" : "2x"
80
+    },
81
+    {
82
+      "size" : "40x40",
83
+      "idiom" : "ipad",
84
+      "filename" : "Icon-App-40x40@1x.png",
85
+      "scale" : "1x"
86
+    },
87
+    {
88
+      "size" : "40x40",
89
+      "idiom" : "ipad",
90
+      "filename" : "Icon-App-40x40@2x.png",
91
+      "scale" : "2x"
92
+    },
93
+    {
94
+      "size" : "76x76",
95
+      "idiom" : "ipad",
96
+      "filename" : "Icon-App-76x76@1x.png",
97
+      "scale" : "1x"
98
+    },
99
+    {
100
+      "size" : "76x76",
101
+      "idiom" : "ipad",
102
+      "filename" : "Icon-App-76x76@2x.png",
103
+      "scale" : "2x"
104
+    },
105
+    {
106
+      "size" : "83.5x83.5",
107
+      "idiom" : "ipad",
108
+      "filename" : "Icon-App-83.5x83.5@2x.png",
109
+      "scale" : "2x"
110
+    },
111
+    {
112
+      "size" : "1024x1024",
113
+      "idiom" : "ios-marketing",
114
+      "filename" : "Icon-App-1024x1024@1x.png",
115
+      "scale" : "1x"
116
+    }
117
+  ],
118
+  "info" : {
119
+    "version" : 1,
120
+    "author" : "xcode"
121
+  }
122
+}

BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png Целия файл


+ 23
- 0
packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json Целия файл

@@ -0,0 +1,23 @@
1
+{
2
+  "images" : [
3
+    {
4
+      "idiom" : "universal",
5
+      "filename" : "LaunchImage.png",
6
+      "scale" : "1x"
7
+    },
8
+    {
9
+      "idiom" : "universal",
10
+      "filename" : "LaunchImage@2x.png",
11
+      "scale" : "2x"
12
+    },
13
+    {
14
+      "idiom" : "universal",
15
+      "filename" : "LaunchImage@3x.png",
16
+      "scale" : "3x"
17
+    }
18
+  ],
19
+  "info" : {
20
+    "version" : 1,
21
+    "author" : "xcode"
22
+  }
23
+}

BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png Целия файл


BIN
packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png Целия файл


+ 5
- 0
packages/zefyr/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md Целия файл

@@ -0,0 +1,5 @@
1
+# Launch Screen Assets
2
+
3
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4
+
5
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

+ 37
- 0
packages/zefyr/example/ios/Runner/Base.lproj/LaunchScreen.storyboard Целия файл

@@ -0,0 +1,37 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
3
+    <dependencies>
4
+        <deployment identifier="iOS"/>
5
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
6
+    </dependencies>
7
+    <scenes>
8
+        <!--View Controller-->
9
+        <scene sceneID="EHf-IW-A2E">
10
+            <objects>
11
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
12
+                    <layoutGuides>
13
+                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
14
+                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
15
+                    </layoutGuides>
16
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
17
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
18
+                        <subviews>
19
+                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
20
+                            </imageView>
21
+                        </subviews>
22
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
23
+                        <constraints>
24
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
25
+                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
26
+                        </constraints>
27
+                    </view>
28
+                </viewController>
29
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
30
+            </objects>
31
+            <point key="canvasLocation" x="53" y="375"/>
32
+        </scene>
33
+    </scenes>
34
+    <resources>
35
+        <image name="LaunchImage" width="168" height="185"/>
36
+    </resources>
37
+</document>

+ 26
- 0
packages/zefyr/example/ios/Runner/Base.lproj/Main.storyboard Целия файл

@@ -0,0 +1,26 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
3
+    <dependencies>
4
+        <deployment identifier="iOS"/>
5
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
6
+    </dependencies>
7
+    <scenes>
8
+        <!--Flutter View Controller-->
9
+        <scene sceneID="tne-QT-ifu">
10
+            <objects>
11
+                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
12
+                    <layoutGuides>
13
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
14
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
15
+                    </layoutGuides>
16
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
17
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
18
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
19
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
20
+                    </view>
21
+                </viewController>
22
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
23
+            </objects>
24
+        </scene>
25
+    </scenes>
26
+</document>

+ 45
- 0
packages/zefyr/example/ios/Runner/Info.plist Целия файл

@@ -0,0 +1,45 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+	<key>CFBundleDevelopmentRegion</key>
6
+	<string>en</string>
7
+	<key>CFBundleExecutable</key>
8
+	<string>$(EXECUTABLE_NAME)</string>
9
+	<key>CFBundleIdentifier</key>
10
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+	<key>CFBundleInfoDictionaryVersion</key>
12
+	<string>6.0</string>
13
+	<key>CFBundleName</key>
14
+	<string>example</string>
15
+	<key>CFBundlePackageType</key>
16
+	<string>APPL</string>
17
+	<key>CFBundleShortVersionString</key>
18
+	<string>$(FLUTTER_BUILD_NAME)</string>
19
+	<key>CFBundleSignature</key>
20
+	<string>????</string>
21
+	<key>CFBundleVersion</key>
22
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
23
+	<key>LSRequiresIPhoneOS</key>
24
+	<true/>
25
+	<key>UILaunchStoryboardName</key>
26
+	<string>LaunchScreen</string>
27
+	<key>UIMainStoryboardFile</key>
28
+	<string>Main</string>
29
+	<key>UISupportedInterfaceOrientations</key>
30
+	<array>
31
+		<string>UIInterfaceOrientationPortrait</string>
32
+		<string>UIInterfaceOrientationLandscapeLeft</string>
33
+		<string>UIInterfaceOrientationLandscapeRight</string>
34
+	</array>
35
+	<key>UISupportedInterfaceOrientations~ipad</key>
36
+	<array>
37
+		<string>UIInterfaceOrientationPortrait</string>
38
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
39
+		<string>UIInterfaceOrientationLandscapeLeft</string>
40
+		<string>UIInterfaceOrientationLandscapeRight</string>
41
+	</array>
42
+	<key>UIViewControllerBasedStatusBarAppearance</key>
43
+	<false/>
44
+</dict>
45
+</plist>

+ 9
- 0
packages/zefyr/example/ios/Runner/main.m Целия файл

@@ -0,0 +1,9 @@
1
+#import <Flutter/Flutter.h>
2
+#import <UIKit/UIKit.h>
3
+#import "AppDelegate.h"
4
+
5
+int main(int argc, char* argv[]) {
6
+  @autoreleasepool {
7
+    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8
+  }
9
+}

+ 102
- 0
packages/zefyr/example/lib/main.dart Целия файл

@@ -0,0 +1,102 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:convert';
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:quill_delta/quill_delta.dart';
8
+import 'package:zefyr/zefyr.dart';
9
+
10
+void main() {
11
+  runApp(new ZefyrApp());
12
+}
13
+
14
+class ZefyrLogo extends StatelessWidget {
15
+  @override
16
+  Widget build(BuildContext context) {
17
+    return Row(
18
+      mainAxisAlignment: MainAxisAlignment.center,
19
+      children: <Widget>[
20
+        Text('Ze'),
21
+        FlutterLogo(size: 24.0),
22
+        Text('yr'),
23
+      ],
24
+    );
25
+  }
26
+}
27
+
28
+class ZefyrApp extends StatelessWidget {
29
+  @override
30
+  Widget build(BuildContext context) {
31
+    return new MaterialApp(
32
+      title: 'Zefyr Editor',
33
+      theme: new ThemeData(primarySwatch: Colors.cyan),
34
+      home: new MyHomePage(),
35
+    );
36
+  }
37
+}
38
+
39
+class MyHomePage extends StatefulWidget {
40
+  @override
41
+  _MyHomePageState createState() => new _MyHomePageState();
42
+}
43
+
44
+final doc =
45
+    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"​","attributes":{"embed":{"type":"hr"}}},{"insert":"\n"},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\nZefyr is an "},{"insert":"early preview","attributes":{"b":true}},{"insert":" open source library.\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data format and Document Model"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style attributes"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic rules"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and flexibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" print(“Hello world!”);"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
46
+
47
+Delta getDelta() {
48
+  return Delta.fromJson(json.decode(doc));
49
+}
50
+
51
+class _MyHomePageState extends State<MyHomePage> {
52
+  final ZefyrController _controller =
53
+      ZefyrController(NotusDocument.fromDelta(getDelta()));
54
+  final FocusNode _focusNode = new FocusNode();
55
+  bool _editing = false;
56
+
57
+  @override
58
+  Widget build(BuildContext context) {
59
+    final theme = new ZefyrThemeData(
60
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
61
+            color: Colors.grey.shade800,
62
+            toggleColor: Colors.grey.shade900,
63
+            iconColor: Colors.white,
64
+            disabledIconColor: Colors.grey.shade500,
65
+          ),
66
+    );
67
+
68
+    final done = _editing
69
+        ? [new FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
70
+        : [new FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
71
+    return Scaffold(
72
+      resizeToAvoidBottomPadding: true,
73
+      appBar: AppBar(
74
+        elevation: 1.0,
75
+        backgroundColor: Colors.grey.shade200,
76
+        brightness: Brightness.light,
77
+        title: ZefyrLogo(),
78
+        actions: done,
79
+      ),
80
+      body: ZefyrTheme(
81
+        data: theme,
82
+        child: ZefyrEditor(
83
+          controller: _controller,
84
+          focusNode: _focusNode,
85
+          enabled: _editing,
86
+        ),
87
+      ),
88
+    );
89
+  }
90
+
91
+  void _startEditing() {
92
+    setState(() {
93
+      _editing = true;
94
+    });
95
+  }
96
+
97
+  void _stopEditing() {
98
+    setState(() {
99
+      _editing = false;
100
+    });
101
+  }
102
+}

+ 67
- 0
packages/zefyr/example/pubspec.yaml Целия файл

@@ -0,0 +1,67 @@
1
+name: example
2
+description: A new Flutter project.
3
+
4
+# The following defines the version and build number for your application.
5
+# A version number is three numbers separated by dots, like 1.2.43
6
+# followed by an optional build number separated by a +.
7
+# Both the version and the builder number may be overridden in flutter
8
+# build by specifying --build-name and --build-number, respectively.
9
+# Read more about versioning at semver.org.
10
+version: 1.0.0+1
11
+
12
+dependencies:
13
+  flutter:
14
+    sdk: flutter
15
+
16
+  # The following adds the Cupertino Icons font to your application.
17
+  # Use with the CupertinoIcons class for iOS style icons.
18
+  cupertino_icons: ^0.1.2
19
+  zefyr:
20
+    path: ../
21
+
22
+dev_dependencies:
23
+  flutter_test:
24
+    sdk: flutter
25
+
26
+
27
+# For information on the generic Dart part of this file, see the
28
+# following page: https://www.dartlang.org/tools/pub/pubspec
29
+
30
+# The following section is specific to Flutter.
31
+flutter:
32
+
33
+  # The following line ensures that the Material Icons font is
34
+  # included with your application, so that you can use the icons in
35
+  # the material Icons class.
36
+  uses-material-design: true
37
+
38
+  # To add assets to your application, add an assets section, like this:
39
+  # assets:
40
+  #  - images/a_dot_burr.jpeg
41
+  #  - images/a_dot_ham.jpeg
42
+
43
+  # An image asset can refer to one or more resolution-specific "variants", see
44
+  # https://flutter.io/assets-and-images/#resolution-aware.
45
+
46
+  # For details regarding adding assets from package dependencies, see
47
+  # https://flutter.io/assets-and-images/#from-packages
48
+
49
+  # To add custom fonts to your application, add a fonts section here,
50
+  # in this "flutter" section. Each entry in this list should have a
51
+  # "family" key with the font family name, and a "fonts" key with a
52
+  # list giving the asset and other descriptors for the font. For
53
+  # example:
54
+  # fonts:
55
+  #   - family: Schyler
56
+  #     fonts:
57
+  #       - asset: fonts/Schyler-Regular.ttf
58
+  #       - asset: fonts/Schyler-Italic.ttf
59
+  #         style: italic
60
+  #   - family: Trajan Pro
61
+  #     fonts:
62
+  #       - asset: fonts/TrajanPro.ttf
63
+  #       - asset: fonts/TrajanPro_Bold.ttf
64
+  #         weight: 700
65
+  #
66
+  # For details regarding fonts from package dependencies,
67
+  # see https://flutter.io/custom-fonts/#from-packages

+ 40
- 0
packages/zefyr/lib/src/fast_diff.dart Целия файл

@@ -0,0 +1,40 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:math' as math;
5
+
6
+/// Performs a fast diff operation on two input strings based on provided
7
+/// [cursorPosition].
8
+DiffResult fastDiff(String oldText, String newText, int cursorPosition) {
9
+  var delta = newText.length - oldText.length;
10
+  var limit = math.max(0, cursorPosition - delta);
11
+  var end = oldText.length;
12
+  while (end > limit && oldText[end - 1] == newText[end + delta - 1]) {
13
+    end -= 1;
14
+  }
15
+  var start = 0;
16
+  var startLimit = cursorPosition - math.max(0, delta);
17
+  while (start < startLimit && oldText[start] == newText[start]) {
18
+    start += 1;
19
+  }
20
+  final String deleted = (start < end) ? oldText.substring(start, end) : '';
21
+  final inserted = newText.substring(start, end + delta);
22
+  return new DiffResult(start, deleted, inserted);
23
+}
24
+
25
+/// A diff between two strings of text.
26
+class DiffResult {
27
+  /// Start index in old text at which changes begin.
28
+  final int start;
29
+
30
+  /// Deleted text in old text.
31
+  final String deleted;
32
+
33
+  /// Inserted text.
34
+  final String inserted;
35
+
36
+  DiffResult(this.start, this.deleted, this.inserted);
37
+
38
+  @override
39
+  String toString() => 'DiffResult[$start, "$deleted", "$inserted"]';
40
+}

+ 512
- 0
packages/zefyr/lib/src/widgets/buttons.dart Целия файл

@@ -0,0 +1,512 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter/services.dart';
6
+import 'package:url_launcher/url_launcher.dart';
7
+import 'package:notus/notus.dart';
8
+
9
+import 'editor.dart';
10
+import 'theme.dart';
11
+import 'toolbar.dart';
12
+
13
+/// A button used in [ZefyrToolbar].
14
+///
15
+/// Create an instance of this widget with [ZefyrButton.icon] or
16
+/// [ZefyrButton.text] constructors.
17
+///
18
+/// Toolbar buttons are normally created by a [ZefyrToolbarDelegate].
19
+class ZefyrButton extends StatelessWidget {
20
+  /// Creates a toolbar button with an icon.
21
+  ZefyrButton.icon({
22
+    @required this.action,
23
+    @required IconData icon,
24
+    double iconSize,
25
+    this.onPressed,
26
+  })  : assert(action != null),
27
+        assert(icon != null),
28
+        _icon = icon,
29
+        _iconSize = iconSize,
30
+        _text = null,
31
+        _textStyle = null,
32
+        super();
33
+
34
+  /// Creates a toolbar button containing text.
35
+  ///
36
+  /// Note that [ZefyrButton] has fixed width and does not expand to accommodate
37
+  /// long texts.
38
+  ZefyrButton.text({
39
+    @required this.action,
40
+    @required String text,
41
+    TextStyle style,
42
+    this.onPressed,
43
+  })  : assert(action != null),
44
+        assert(text != null),
45
+        _icon = null,
46
+        _iconSize = null,
47
+        _text = text,
48
+        _textStyle = style,
49
+        super();
50
+
51
+  /// Toolbar action associated with this button.
52
+  final ZefyrToolbarAction action;
53
+  final IconData _icon;
54
+  final double _iconSize;
55
+  final String _text;
56
+  final TextStyle _textStyle;
57
+
58
+  /// Callback to trigger when this button is tapped.
59
+  final VoidCallback onPressed;
60
+
61
+  bool get isAttributeAction {
62
+    return kZefyrToolbarAttributeActions.keys.contains(action);
63
+  }
64
+
65
+  @override
66
+  Widget build(BuildContext context) {
67
+    final editor = ZefyrEditor.of(context);
68
+    final toolbar = ZefyrToolbar.of(context);
69
+    final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
70
+    final pressedHandler = _getPressedHandler(editor, toolbar);
71
+    final iconColor = (pressedHandler == null)
72
+        ? toolbarTheme.disabledIconColor
73
+        : toolbarTheme.iconColor;
74
+    if (_icon != null) {
75
+      return RawZefyrButton.icon(
76
+        action: action,
77
+        icon: _icon,
78
+        size: _iconSize,
79
+        iconColor: iconColor,
80
+        color: _getColor(editor, toolbarTheme),
81
+        onPressed: _getPressedHandler(editor, toolbar),
82
+      );
83
+    } else {
84
+      assert(_text != null);
85
+      var style = _textStyle ?? new TextStyle();
86
+      style = style.copyWith(color: iconColor);
87
+      return RawZefyrButton(
88
+        action: action,
89
+        child: new Text(_text, style: style),
90
+        color: _getColor(editor, toolbarTheme),
91
+        onPressed: _getPressedHandler(editor, toolbar),
92
+      );
93
+    }
94
+  }
95
+
96
+  Color _getColor(ZefyrEditorScope editor, ZefyrToolbarTheme theme) {
97
+    if (isAttributeAction) {
98
+      final attribute = kZefyrToolbarAttributeActions[action];
99
+      final isToggled = (attribute is NotusAttribute)
100
+          ? editor.selectionStyle.containsSame(attribute)
101
+          : editor.selectionStyle.contains(attribute);
102
+      return isToggled ? theme.toggleColor : null;
103
+    }
104
+    return null;
105
+  }
106
+
107
+  VoidCallback _getPressedHandler(
108
+      ZefyrEditorScope editor, ZefyrToolbarState toolbar) {
109
+    if (onPressed != null) {
110
+      return onPressed;
111
+    } else if (isAttributeAction) {
112
+      final attribute = kZefyrToolbarAttributeActions[action];
113
+      if (attribute is NotusAttribute) {
114
+        return () => _toggleAttribute(attribute, editor);
115
+      }
116
+    } else if (action == ZefyrToolbarAction.close) {
117
+      return () => toolbar.closeOverlay();
118
+    } else if (action == ZefyrToolbarAction.hideKeyboard) {
119
+      return () => editor.hideKeyboard();
120
+    }
121
+
122
+    return null;
123
+  }
124
+
125
+  void _toggleAttribute(NotusAttribute attribute, ZefyrEditorScope editor) {
126
+    final isToggled = editor.selectionStyle.containsSame(attribute);
127
+    if (isToggled) {
128
+      editor.formatSelection(attribute.unset);
129
+    } else {
130
+      editor.formatSelection(attribute);
131
+    }
132
+  }
133
+}
134
+
135
+/// Raw button widget used by [ZefyrToolbar].
136
+///
137
+/// See also:
138
+///
139
+///   * [ZefyrButton], which wraps this widget and implements most of the
140
+///     action-specific logic.
141
+class RawZefyrButton extends StatelessWidget {
142
+  const RawZefyrButton({
143
+    Key key,
144
+    @required this.action,
145
+    @required this.child,
146
+    @required this.color,
147
+    @required this.onPressed,
148
+  }) : super(key: key);
149
+
150
+  /// Creates a [RawZefyrButton] containing an icon.
151
+  RawZefyrButton.icon({
152
+    @required this.action,
153
+    @required IconData icon,
154
+    double size,
155
+    Color iconColor,
156
+    @required this.color,
157
+    @required this.onPressed,
158
+  })  : child = new Icon(icon, size: size, color: iconColor),
159
+        super();
160
+
161
+  /// Toolbar action associated with this button.
162
+  final ZefyrToolbarAction action;
163
+
164
+  /// Child widget to show inside this button. Usually an icon.
165
+  final Widget child;
166
+
167
+  /// Background color of this button.
168
+  final Color color;
169
+
170
+  /// Callback to trigger when this button is pressed.
171
+  final VoidCallback onPressed;
172
+
173
+  /// Returns `true` if this button is currently toggled on.
174
+  bool get isToggled => color != null;
175
+
176
+  @override
177
+  Widget build(BuildContext context) {
178
+    final theme = Theme.of(context);
179
+    final width = theme.buttonTheme.constraints.minHeight + 4.0;
180
+    final constraints = theme.buttonTheme.constraints.copyWith(
181
+        minWidth: width, maxHeight: theme.buttonTheme.constraints.minHeight);
182
+    final radius = BorderRadius.all(Radius.circular(3.0));
183
+    return Padding(
184
+      padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0),
185
+      child: RawMaterialButton(
186
+        shape: RoundedRectangleBorder(borderRadius: radius),
187
+        elevation: 0.0,
188
+        fillColor: color,
189
+        constraints: constraints,
190
+        onPressed: onPressed,
191
+        child: child,
192
+      ),
193
+    );
194
+  }
195
+}
196
+
197
+/// Controls heading styles.
198
+///
199
+/// When pressed, this button displays overlay toolbar with three
200
+/// buttons for each heading level.
201
+class HeadingButton extends StatefulWidget {
202
+  const HeadingButton({Key key}) : super(key: key);
203
+
204
+  @override
205
+  _HeadingButtonState createState() => _HeadingButtonState();
206
+}
207
+
208
+class _HeadingButtonState extends State<HeadingButton> {
209
+  @override
210
+  Widget build(BuildContext context) {
211
+    final toolbar = ZefyrToolbar.of(context);
212
+    return toolbar.buildButton(
213
+      context,
214
+      ZefyrToolbarAction.heading,
215
+      onPressed: showOverlay,
216
+    );
217
+  }
218
+
219
+  void showOverlay() {
220
+    final toolbar = ZefyrToolbar.of(context);
221
+    toolbar.showOverlay(buildOverlay);
222
+  }
223
+
224
+  Widget buildOverlay(BuildContext context) {
225
+    final toolbar = ZefyrToolbar.of(context);
226
+    final buttons = Row(
227
+      children: <Widget>[
228
+        SizedBox(width: 8.0),
229
+        toolbar.buildButton(context, ZefyrToolbarAction.headingLevel1),
230
+        toolbar.buildButton(context, ZefyrToolbarAction.headingLevel2),
231
+        toolbar.buildButton(context, ZefyrToolbarAction.headingLevel3),
232
+      ],
233
+    );
234
+    return ZefyrToolbarScaffold(body: buttons);
235
+  }
236
+}
237
+
238
+class LinkButton extends StatefulWidget {
239
+  const LinkButton({Key key}) : super(key: key);
240
+
241
+  @override
242
+  _LinkButtonState createState() => _LinkButtonState();
243
+}
244
+
245
+class _LinkButtonState extends State<LinkButton> {
246
+  final TextEditingController _inputController = new TextEditingController();
247
+  Key _inputKey;
248
+  bool _formatError = false;
249
+  bool get isEditing => _inputKey != null;
250
+
251
+  @override
252
+  Widget build(BuildContext context) {
253
+    final editor = ZefyrEditor.of(context);
254
+    final toolbar = ZefyrToolbar.of(context);
255
+    final enabled =
256
+        hasLink(editor.selectionStyle) || !editor.selection.isCollapsed;
257
+
258
+    return toolbar.buildButton(
259
+      context,
260
+      ZefyrToolbarAction.link,
261
+      onPressed: enabled ? showOverlay : null,
262
+    );
263
+  }
264
+
265
+  bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link);
266
+
267
+  String getLink([String defaultValue]) {
268
+    final editor = ZefyrEditor.of(context);
269
+    final attrs = editor.selectionStyle;
270
+    if (hasLink(attrs)) {
271
+      return attrs.value(NotusAttribute.link);
272
+    }
273
+    return defaultValue;
274
+  }
275
+
276
+  void showOverlay() {
277
+    final toolbar = ZefyrToolbar.of(context);
278
+    toolbar.showOverlay(buildOverlay).whenComplete(cancelEdit);
279
+  }
280
+
281
+  void closeOverlay() {
282
+    final toolbar = ZefyrToolbar.of(context);
283
+    toolbar.closeOverlay();
284
+  }
285
+
286
+  void edit() {
287
+    final toolbar = ZefyrToolbar.of(context);
288
+    setState(() {
289
+      _inputKey = new UniqueKey();
290
+      _inputController.text = getLink('https://');
291
+      _inputController.addListener(_handleInputChange);
292
+      toolbar.markNeedsRebuild();
293
+    });
294
+  }
295
+
296
+  void doneEdit() {
297
+    final editor = ZefyrEditor.of(context);
298
+    final toolbar = ZefyrToolbar.of(context);
299
+    setState(() {
300
+      var error = false;
301
+      if (_inputController.text.isNotEmpty) {
302
+        try {
303
+          var uri = Uri.parse(_inputController.text);
304
+          if ((uri.isScheme('https') || uri.isScheme('http')) &&
305
+              uri.host.isNotEmpty) {
306
+            editor.formatSelection(
307
+                NotusAttribute.link.fromString(_inputController.text));
308
+          } else {
309
+            error = true;
310
+          }
311
+        } on FormatException {
312
+          error = true;
313
+        }
314
+      }
315
+      if (error) {
316
+        _formatError = error;
317
+        toolbar.markNeedsRebuild();
318
+      } else {
319
+        _inputKey = null;
320
+        _inputController.text = '';
321
+        _inputController.removeListener(_handleInputChange);
322
+        toolbar.markNeedsRebuild();
323
+        editor.focus(context);
324
+      }
325
+    });
326
+  }
327
+
328
+  void cancelEdit() {
329
+    if (mounted) {
330
+      final editor = ZefyrEditor.of(context);
331
+      setState(() {
332
+        _inputKey = null;
333
+        _inputController.text = '';
334
+        _inputController.removeListener(_handleInputChange);
335
+        editor.focus(context);
336
+      });
337
+    }
338
+  }
339
+
340
+  void unlink() {
341
+    final editor = ZefyrEditor.of(context);
342
+    editor.formatSelection(NotusAttribute.link.unset);
343
+    closeOverlay();
344
+  }
345
+
346
+  void copyToClipboard() {
347
+    var link = getLink();
348
+    assert(link != null);
349
+    Clipboard.setData(new ClipboardData(text: link));
350
+  }
351
+
352
+  void openInBrowser() async {
353
+    final editor = ZefyrEditor.of(context);
354
+    var link = getLink();
355
+    assert(link != null);
356
+    if (await canLaunch(link)) {
357
+      editor.hideKeyboard();
358
+      await launch(link, forceWebView: true);
359
+    }
360
+  }
361
+
362
+  void _handleInputChange() {
363
+    final toolbar = ZefyrToolbar.of(context);
364
+    setState(() {
365
+      _formatError = false;
366
+      toolbar.markNeedsRebuild();
367
+    });
368
+  }
369
+
370
+  Widget buildOverlay(BuildContext context) {
371
+    final editor = ZefyrEditor.of(context);
372
+    final toolbar = ZefyrToolbar.of(context);
373
+    final style = editor.selectionStyle;
374
+
375
+    String value = 'Tap to edit link';
376
+    if (style.contains(NotusAttribute.link)) {
377
+      value = style.value(NotusAttribute.link);
378
+    }
379
+    final clipboardEnabled = value != 'Tap to edit link';
380
+    final body = !isEditing
381
+        ? _LinkView(value: value, onTap: edit)
382
+        : _LinkInput(
383
+            key: _inputKey,
384
+            controller: _inputController,
385
+            focusNode: editor.toolbarFocusNode,
386
+            formatError: _formatError,
387
+          );
388
+    final items = <Widget>[Expanded(child: body)];
389
+    if (!isEditing) {
390
+      final unlinkHandler = hasLink(style) ? unlink : null;
391
+      final copyHandler = clipboardEnabled ? copyToClipboard : null;
392
+      final openHandler = hasLink(style) ? openInBrowser : null;
393
+      final buttons = <Widget>[
394
+        toolbar.buildButton(context, ZefyrToolbarAction.unlink,
395
+            onPressed: unlinkHandler),
396
+        toolbar.buildButton(context, ZefyrToolbarAction.clipboardCopy,
397
+            onPressed: copyHandler),
398
+        toolbar.buildButton(
399
+          context,
400
+          ZefyrToolbarAction.openInBrowser,
401
+          onPressed: openHandler,
402
+        ),
403
+      ];
404
+      items.addAll(buttons);
405
+    }
406
+    final trailingPressed = isEditing ? doneEdit : closeOverlay;
407
+    final trailingAction =
408
+        isEditing ? ZefyrToolbarAction.confirm : ZefyrToolbarAction.close;
409
+
410
+    return ZefyrToolbarScaffold(
411
+      body: Row(children: items),
412
+      trailing: toolbar.buildButton(
413
+        context,
414
+        trailingAction,
415
+        onPressed: trailingPressed,
416
+      ),
417
+    );
418
+  }
419
+}
420
+
421
+class _LinkInput extends StatefulWidget {
422
+  final FocusNode focusNode;
423
+  final TextEditingController controller;
424
+  final bool formatError;
425
+
426
+  const _LinkInput({
427
+    Key key,
428
+    @required this.focusNode,
429
+    @required this.controller,
430
+    this.formatError: false,
431
+  }) : super(key: key);
432
+  @override
433
+  _LinkInputState createState() {
434
+    return new _LinkInputState();
435
+  }
436
+}
437
+
438
+class _LinkInputState extends State<_LinkInput> {
439
+  bool _didAutoFocus = false;
440
+
441
+  @override
442
+  void didChangeDependencies() {
443
+    super.didChangeDependencies();
444
+    if (!_didAutoFocus) {
445
+      FocusScope.of(context).requestFocus(widget.focusNode);
446
+      _didAutoFocus = true;
447
+    }
448
+  }
449
+
450
+  @override
451
+  Widget build(BuildContext context) {
452
+    FocusScope.of(context).reparentIfNeeded(widget.focusNode);
453
+
454
+    final theme = Theme.of(context);
455
+    final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
456
+    final color =
457
+        widget.formatError ? Colors.redAccent : toolbarTheme.iconColor;
458
+    final style = theme.textTheme.subhead.copyWith(color: color);
459
+    return TextField(
460
+      style: style,
461
+      keyboardType: TextInputType.url,
462
+      focusNode: widget.focusNode,
463
+      controller: widget.controller,
464
+      autofocus: true,
465
+      decoration: new InputDecoration(
466
+          hintText: 'https://',
467
+          filled: true,
468
+          fillColor: toolbarTheme.color,
469
+          border: InputBorder.none,
470
+          contentPadding: const EdgeInsets.all(10.0)),
471
+    );
472
+  }
473
+}
474
+
475
+class _LinkView extends StatelessWidget {
476
+  const _LinkView({Key key, @required this.value, this.onTap})
477
+      : super(key: key);
478
+  final String value;
479
+  final VoidCallback onTap;
480
+
481
+  @override
482
+  Widget build(BuildContext context) {
483
+    final theme = Theme.of(context);
484
+    final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
485
+    Widget widget = new ClipRect(
486
+      child: ListView(
487
+        scrollDirection: Axis.horizontal,
488
+        children: <Widget>[
489
+          Container(
490
+            alignment: AlignmentDirectional.centerStart,
491
+            constraints: BoxConstraints(minHeight: ZefyrToolbar.kToolbarHeight),
492
+            padding: const EdgeInsets.all(10.0),
493
+            child: Text(
494
+              value,
495
+              maxLines: 1,
496
+              overflow: TextOverflow.ellipsis,
497
+              style: theme.textTheme.subhead
498
+                  .copyWith(color: toolbarTheme.disabledIconColor),
499
+            ),
500
+          )
501
+        ],
502
+      ),
503
+    );
504
+    if (onTap != null) {
505
+      widget = GestureDetector(
506
+        child: widget,
507
+        onTap: onTap,
508
+      );
509
+    }
510
+    return widget;
511
+  }
512
+}

+ 26
- 0
packages/zefyr/lib/src/widgets/caret.dart Целия файл

@@ -0,0 +1,26 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui';
5
+
6
+import 'package:flutter/material.dart';
7
+
8
+class CaretPainter {
9
+  static const double _kCaretHeightOffset = 2.0; // pixels
10
+  static const double _kCaretWidth = 1.0; // pixels
11
+
12
+  Rect _prototype;
13
+
14
+  Rect get prototype => _prototype;
15
+
16
+  void layout(double lineHeight) {
17
+    _prototype = new Rect.fromLTWH(
18
+        0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
19
+  }
20
+
21
+  void paint(Canvas canvas, Offset offset) {
22
+    final Paint paint = new Paint()..color = Colors.black;
23
+    final Rect caretRect = _prototype.shift(offset);
24
+    canvas.drawRect(caretRect, paint);
25
+  }
26
+}

+ 47
- 0
packages/zefyr/lib/src/widgets/code.dart Целия файл

@@ -0,0 +1,47 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:notus/notus.dart';
6
+
7
+import 'common.dart';
8
+import 'theme.dart';
9
+
10
+/// Represents a code snippet in Zefyr editor.
11
+class ZefyrCode extends StatelessWidget {
12
+  const ZefyrCode({Key key, @required this.node}) : super(key: key);
13
+
14
+  /// Document node represented by this widget.
15
+  final BlockNode node;
16
+
17
+  @override
18
+  Widget build(BuildContext context) {
19
+    final theme = ZefyrTheme.of(context);
20
+
21
+    List<Widget> items = [];
22
+    for (var line in node.children) {
23
+      items.add(_buildLine(line, theme.blockTheme.code.textStyle));
24
+    }
25
+
26
+    return new Padding(
27
+      padding: theme.blockTheme.code.padding,
28
+      child: new Container(
29
+        // TODO: make decorations configurable
30
+        decoration: BoxDecoration(
31
+          color: Colors.blueGrey.shade50,
32
+          borderRadius: BorderRadius.circular(3.0),
33
+        ),
34
+        padding: const EdgeInsets.all(16.0),
35
+        child: new Column(
36
+          crossAxisAlignment: CrossAxisAlignment.stretch,
37
+          children: items,
38
+        ),
39
+      ),
40
+    );
41
+  }
42
+
43
+  Widget _buildLine(Node node, TextStyle style) {
44
+    LineNode line = node;
45
+    return new RawZefyrLine(node: line, style: style);
46
+  }
47
+}

+ 155
- 0
packages/zefyr/lib/src/widgets/common.dart Целия файл

@@ -0,0 +1,155 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/rendering.dart';
5
+import 'package:flutter/widgets.dart';
6
+import 'package:notus/notus.dart';
7
+
8
+import 'editable_paragraph.dart';
9
+import 'editable_text.dart';
10
+import 'horizontal_rule.dart';
11
+import 'theme.dart';
12
+
13
+/// Raw widget representing a single line of Notus document in Zefyr editor.
14
+///
15
+/// See [ZefyrParagraph] and [ZefyrHeading] which wrap this widget and
16
+/// integrate it with current [ZefyrTheme].
17
+class RawZefyrLine extends StatefulWidget {
18
+  const RawZefyrLine({
19
+    Key key,
20
+    @required this.node,
21
+    this.style,
22
+    this.padding,
23
+  }) : super(key: key);
24
+
25
+  /// Line in the document represented by this widget.
26
+  final LineNode node;
27
+
28
+  /// Style to apply to this line. Required for lines with text contents,
29
+  /// ignored for lines containing embeds.
30
+  final TextStyle style;
31
+
32
+  /// Padding to add around this paragraph.
33
+  final EdgeInsets padding;
34
+
35
+  @override
36
+  _RawZefyrLineState createState() => new _RawZefyrLineState();
37
+}
38
+
39
+class _RawZefyrLineState extends State<RawZefyrLine> {
40
+  final LayerLink _link = new LayerLink();
41
+
42
+  @override
43
+  Widget build(BuildContext context) {
44
+    ensureVisible(context);
45
+    final theme = ZefyrTheme.of(context);
46
+    final editable = ZefyrEditableText.of(context);
47
+
48
+    Widget content;
49
+    if (widget.node.hasEmbed) {
50
+      content = buildEmbed(context);
51
+    } else {
52
+      assert(widget.style != null);
53
+      content = new EditableParagraph(
54
+        node: widget.node,
55
+        text: buildText(context),
56
+        layerLink: _link,
57
+        renderContext: editable.renderContext,
58
+        showCursor: editable.showCursor,
59
+        selection: editable.selection,
60
+        selectionColor: theme.selectionColor,
61
+      );
62
+    }
63
+
64
+    final result = new CompositedTransformTarget(link: _link, child: content);
65
+    if (widget.padding != null) {
66
+      return new Padding(padding: widget.padding, child: result);
67
+    }
68
+    return result;
69
+  }
70
+
71
+  void ensureVisible(BuildContext context) {
72
+    final editable = ZefyrEditableText.of(context);
73
+    if (editable.selection.isCollapsed &&
74
+        widget.node.containsOffset(editable.selection.extentOffset)) {
75
+      WidgetsBinding.instance.addPostFrameCallback((_) {
76
+        bringIntoView(context);
77
+      });
78
+    }
79
+  }
80
+
81
+  void bringIntoView(BuildContext context) {
82
+    ScrollableState scrollable = Scrollable.of(context);
83
+    final object = context.findRenderObject();
84
+    assert(object.attached);
85
+    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
86
+    assert(viewport != null);
87
+
88
+    final double offset = scrollable.position.pixels;
89
+    double target = viewport.getOffsetToReveal(object, 0.0).offset;
90
+    if (target - offset < 0.0) {
91
+      scrollable.position.jumpTo(target);
92
+      return;
93
+    }
94
+    target = viewport.getOffsetToReveal(object, 1.0).offset;
95
+    if (target - offset > 0.0) {
96
+      scrollable.position.jumpTo(target);
97
+    }
98
+  }
99
+
100
+  TextSpan buildText(BuildContext context) {
101
+    final theme = ZefyrTheme.of(context);
102
+    final List<TextSpan> children = widget.node.children
103
+        .map((node) => _segmentToTextSpan(node, theme))
104
+        .toList(growable: false);
105
+    return new TextSpan(style: widget.style, children: children);
106
+  }
107
+
108
+  TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) {
109
+    final TextNode segment = node;
110
+    final attrs = segment.style;
111
+
112
+    return new TextSpan(
113
+      text: segment.value,
114
+      style: _getTextStyle(attrs, theme),
115
+    );
116
+  }
117
+
118
+  TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
119
+    TextStyle result = new TextStyle();
120
+    if (style.containsSame(NotusAttribute.bold)) {
121
+      result = result.merge(theme.boldStyle);
122
+    }
123
+    if (style.containsSame(NotusAttribute.italic)) {
124
+      result = result.merge(theme.italicStyle);
125
+    }
126
+    if (style.contains(NotusAttribute.link)) {
127
+      result = result.merge(theme.linkStyle);
128
+    }
129
+    return result;
130
+  }
131
+
132
+  Widget buildEmbed(BuildContext context) {
133
+    final theme = ZefyrTheme.of(context);
134
+    final editable = ZefyrEditableText.of(context);
135
+
136
+    EmbedNode node = widget.node.children.single;
137
+    EmbedAttribute embed = node.style.get(NotusAttribute.embed);
138
+
139
+    if (embed.type == EmbedType.horizontalRule) {
140
+      return new HorizontalRule(
141
+        node: widget.node,
142
+        layerLink: _link,
143
+        renderContext: editable.renderContext,
144
+        showCursor: editable.showCursor,
145
+        selection: editable.selection,
146
+        selectionColor: theme.selectionColor,
147
+      );
148
+    } else if (embed.type == EmbedType.image) {
149
+      // TODO: implement image embeds.
150
+      return null;
151
+    } else {
152
+      throw new UnimplementedError('Unimplemented embed type ${embed.type}');
153
+    }
154
+  }
155
+}

+ 164
- 0
packages/zefyr/lib/src/widgets/controller.dart Целия файл

@@ -0,0 +1,164 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:math' as math;
5
+
6
+import 'package:flutter/widgets.dart';
7
+import 'package:notus/notus.dart';
8
+import 'package:quill_delta/quill_delta.dart';
9
+import 'package:zefyr/util.dart';
10
+
11
+const TextSelection _kZeroSelection = const TextSelection.collapsed(
12
+  offset: 0,
13
+  affinity: TextAffinity.upstream,
14
+);
15
+
16
+/// Owner of focus.
17
+enum FocusOwner {
18
+  /// Current owner is the editor.
19
+  editor,
20
+
21
+  /// Current owner is the toolbar.
22
+  toolbar,
23
+
24
+  /// No focus owner.
25
+  none,
26
+}
27
+
28
+/// Controls instance of [ZefyrEditor].
29
+class ZefyrController extends ChangeNotifier {
30
+  ZefyrController(NotusDocument document)
31
+      : assert(document != null),
32
+        _document = document;
33
+
34
+  /// Zefyr document managed by this controller.
35
+  NotusDocument get document => _document;
36
+  NotusDocument _document;
37
+
38
+  /// Currently selected text within the [document].
39
+  TextSelection get selection => _selection;
40
+  TextSelection _selection = _kZeroSelection;
41
+
42
+  ChangeSource _lastChangeSource;
43
+
44
+  /// Source of the last text or selection change.
45
+  ChangeSource get lastChangeSource => _lastChangeSource;
46
+
47
+  /// Updates selection with specified [value].
48
+  ///
49
+  /// [value] and [source] cannot be `null`.
50
+  void updateSelection(TextSelection value,
51
+      {ChangeSource source: ChangeSource.remote}) {
52
+    _updateSelectionSilent(value, source: source);
53
+    notifyListeners();
54
+  }
55
+
56
+  // Updates selection without triggering notifications to listeners.
57
+  void _updateSelectionSilent(TextSelection value,
58
+      {ChangeSource source: ChangeSource.remote}) {
59
+    assert(value != null && source != null);
60
+    _selection = value;
61
+    _lastChangeSource = source;
62
+    _ensureSelectionBeforeLastBreak();
63
+  }
64
+
65
+  @override
66
+  void dispose() {
67
+    _document.close();
68
+    super.dispose();
69
+  }
70
+
71
+  /// Composes [change] into document managed by this controller.
72
+  ///
73
+  /// This method does not apply any adjustments or heuristic rules to
74
+  /// provided [change] and it is caller's responsibility to ensure this change
75
+  /// can be composed without errors.
76
+  ///
77
+  /// If composing this change fails then this method throws [ComposeError].
78
+  void compose(Delta change,
79
+      {TextSelection selection, ChangeSource source: ChangeSource.remote}) {
80
+    if (change.isNotEmpty) {
81
+      _document.compose(change, source);
82
+    }
83
+    if (selection != null) {
84
+      _updateSelectionSilent(selection, source: source);
85
+    } else {
86
+      // Transform selection against the composed change and give priority to
87
+      // current position (force: false).
88
+      final base =
89
+          change.transformPosition(_selection.baseOffset, force: false);
90
+      final extent =
91
+          change.transformPosition(_selection.extentOffset, force: false);
92
+      selection = _selection.copyWith(baseOffset: base, extentOffset: extent);
93
+      if (_selection != selection) {
94
+        _updateSelectionSilent(selection, source: source);
95
+      }
96
+    }
97
+    _lastChangeSource = source;
98
+    notifyListeners();
99
+  }
100
+
101
+  void replaceText(int index, int length, String text,
102
+      {TextSelection selection}) {
103
+    Delta delta;
104
+    if (length > 0 || text.isNotEmpty) {
105
+      delta = document.replace(index, length, text);
106
+    }
107
+    if (selection != null) {
108
+      if (delta == null) {
109
+        _updateSelectionSilent(selection, source: ChangeSource.local);
110
+      } else {
111
+        // need to transform selection position in case actual delta
112
+        // is different from user's version (in deletes and inserts).
113
+        Delta user = new Delta()
114
+          ..retain(index)
115
+          ..insert(text)
116
+          ..delete(length);
117
+        int positionDelta = getPositionDelta(user, delta);
118
+        _updateSelectionSilent(
119
+          selection.copyWith(
120
+            baseOffset: selection.baseOffset + positionDelta,
121
+            extentOffset: selection.extentOffset + positionDelta,
122
+          ),
123
+          source: ChangeSource.local,
124
+        );
125
+      }
126
+    }
127
+    _lastChangeSource = ChangeSource.local;
128
+    notifyListeners();
129
+  }
130
+
131
+  void formatText(int index, int length, NotusAttribute attribute) {
132
+    document.format(index, length, attribute);
133
+    _lastChangeSource = ChangeSource.local;
134
+    notifyListeners();
135
+  }
136
+
137
+  /// Formats current selection with [attribute].
138
+  void formatSelection(NotusAttribute attribute) {
139
+    int index = _selection.start;
140
+    int length = _selection.end - index;
141
+    formatText(index, length, attribute);
142
+  }
143
+
144
+  NotusStyle getSelectionStyle() {
145
+    int start = _selection.start;
146
+    int length = _selection.end - start;
147
+    return _document.collectStyle(start, length);
148
+  }
149
+
150
+  TextEditingValue get plainTextEditingValue {
151
+    return new TextEditingValue(
152
+      text: document.toPlainText(),
153
+      selection: selection,
154
+      composing: new TextRange.collapsed(0),
155
+    );
156
+  }
157
+
158
+  void _ensureSelectionBeforeLastBreak() {
159
+    final end = _document.length - 1;
160
+    final base = math.min(_selection.baseOffset, end);
161
+    final extent = math.min(_selection.extentOffset, end);
162
+    _selection = _selection.copyWith(baseOffset: base, extentOffset: extent);
163
+  }
164
+}

+ 18
- 0
packages/zefyr/lib/src/widgets/editable_box.dart Целия файл

@@ -0,0 +1,18 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui' as ui;
5
+
6
+import 'package:flutter/rendering.dart';
7
+import 'package:notus/notus.dart';
8
+
9
+abstract class RenderEditableBox implements RenderBox {
10
+  ContainerNode get node;
11
+  double get preferredLineHeight;
12
+  LayerLink get layerLink;
13
+  TextPosition getPositionForOffset(Offset offset);
14
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
15
+      {bool isLocal: false});
16
+  TextSelection getLocalSelection(TextSelection selection);
17
+  TextRange getWordBoundary(TextPosition position);
18
+}

+ 271
- 0
packages/zefyr/lib/src/widgets/editable_image.dart Целия файл

@@ -0,0 +1,271 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:math' as math;
5
+import 'dart:ui' as ui;
6
+
7
+import 'package:flutter/rendering.dart';
8
+import 'package:flutter/widgets.dart';
9
+import 'package:notus/notus.dart';
10
+
11
+import 'editable_box.dart';
12
+import 'render_context.dart';
13
+
14
+class EditableImage extends LeafRenderObjectWidget {
15
+  EditableImage({
16
+    @required this.node,
17
+    @required this.layerLink,
18
+    @required this.renderContext,
19
+    @required this.showCursor,
20
+    @required this.selection,
21
+    @required this.selectionColor,
22
+  }) : assert(renderContext != null);
23
+
24
+  final ContainerNode node;
25
+  final LayerLink layerLink;
26
+  final ZefyrRenderContext renderContext;
27
+  final ValueNotifier<bool> showCursor;
28
+  final TextSelection selection;
29
+  final Color selectionColor;
30
+
31
+  @override
32
+  RenderEditableImage createRenderObject(BuildContext context) {
33
+    return new RenderEditableImage(
34
+      node: node,
35
+      layerLink: layerLink,
36
+      renderContext: renderContext,
37
+      showCursor: showCursor,
38
+      selection: selection,
39
+      selectionColor: selectionColor,
40
+    );
41
+  }
42
+
43
+  @override
44
+  void updateRenderObject(
45
+      BuildContext context, RenderEditableImage renderObject) {
46
+    renderObject
47
+      ..node = node
48
+      ..layerLink = layerLink
49
+      ..renderContext = renderContext
50
+      ..showCursor = showCursor
51
+      ..selection = selection
52
+      ..selectionColor = selectionColor;
53
+  }
54
+}
55
+
56
+class RenderEditableImage extends RenderImage implements RenderEditableBox {
57
+  RenderEditableImage({
58
+    ui.Image image,
59
+    @required ContainerNode node,
60
+    @required LayerLink layerLink,
61
+    @required ZefyrRenderContext renderContext,
62
+    @required ValueNotifier<bool> showCursor,
63
+    @required TextSelection selection,
64
+    @required Color selectionColor,
65
+  })  : _node = node,
66
+        _layerLink = layerLink,
67
+        _renderContext = renderContext,
68
+        _showCursor = showCursor,
69
+        _selection = selection,
70
+        _selectionColor = selectionColor,
71
+        super(
72
+          image: image,
73
+        );
74
+  //
75
+  // Public members
76
+  //
77
+
78
+  ContainerNode get node => _node;
79
+  ContainerNode _node;
80
+  void set node(ContainerNode value) {
81
+    _node = value;
82
+  }
83
+
84
+  LayerLink get layerLink => _layerLink;
85
+  LayerLink _layerLink;
86
+  void set layerLink(LayerLink value) {
87
+    if (_layerLink == value) return;
88
+    _layerLink = value;
89
+  }
90
+
91
+  ZefyrRenderContext _renderContext;
92
+  void set renderContext(ZefyrRenderContext value) {
93
+    if (_renderContext == value) return;
94
+    if (attached) _renderContext.removeBox(this);
95
+    _renderContext = value;
96
+    if (attached) _renderContext.addBox(this);
97
+  }
98
+
99
+  ValueNotifier<bool> _showCursor;
100
+  set showCursor(ValueNotifier<bool> value) {
101
+    assert(value != null);
102
+    if (_showCursor == value) return;
103
+    if (attached) _showCursor.removeListener(markNeedsPaint);
104
+    _showCursor = value;
105
+    if (attached) _showCursor.addListener(markNeedsPaint);
106
+    markNeedsPaint();
107
+  }
108
+
109
+  TextSelection _selection;
110
+  set selection(TextSelection value) {
111
+    if (_selection == value) return;
112
+    // TODO: check if selection affects this block (also check previous value)
113
+    _selection = value;
114
+    markNeedsPaint();
115
+  }
116
+
117
+  Color _selectionColor;
118
+  set selectionColor(Color value) {
119
+    if (_selectionColor == value) return;
120
+    _selectionColor = value;
121
+    markNeedsPaint();
122
+  }
123
+
124
+  double get preferredLineHeight => size.height;
125
+
126
+  /// Returns part of document [selection] local to this paragraph. May return
127
+  /// `null`.
128
+  ///
129
+  /// [selection] must not be collapsed.
130
+  TextSelection getLocalSelection(TextSelection selection) {
131
+    if (!_intersectsWithSelection(selection)) return null;
132
+
133
+    int nodeBase = node.documentOffset;
134
+    int nodeExtent = nodeBase + node.length;
135
+    int base = math.max(0, selection.baseOffset - nodeBase);
136
+    int extent = math.min(selection.extentOffset, nodeExtent) - nodeBase;
137
+    return _selection.copyWith(baseOffset: base, extentOffset: extent);
138
+  }
139
+
140
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
141
+      {bool isLocal: false}) {
142
+    TextSelection local = isLocal ? selection : getLocalSelection(selection);
143
+    if (local.isCollapsed) {
144
+      return [
145
+        new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
146
+      ];
147
+    }
148
+
149
+    return [
150
+      new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
151
+      new ui.TextBox.fromLTRBD(
152
+          size.width, 0.0, size.width, size.height, TextDirection.ltr),
153
+    ];
154
+  }
155
+
156
+  //
157
+  // Overridden members
158
+  //
159
+
160
+  @override
161
+  void attach(PipelineOwner owner) {
162
+    super.attach(owner);
163
+    _showCursor.addListener(markNeedsPaint);
164
+    _renderContext.addBox(this);
165
+  }
166
+
167
+  @override
168
+  void detach() {
169
+    _showCursor.removeListener(markNeedsPaint);
170
+    _renderContext.removeBox(this);
171
+    super.detach();
172
+  }
173
+
174
+  @override
175
+  bool hitTestSelf(Offset position) => true;
176
+
177
+  @override
178
+  bool hitTest(HitTestResult result, {Offset position}) {
179
+    if (size.contains(position)) {
180
+      result.add(new BoxHitTestEntry(this, position));
181
+      return true;
182
+    }
183
+    return false;
184
+  }
185
+
186
+  @override
187
+  void performLayout() {
188
+    super.performLayout();
189
+//    _prototypePainter.layout(
190
+//        minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
191
+//    _caretPainter.layout(_prototypePainter.height);
192
+    // Indicate to render context that this object can be used by other
193
+    // layers (selection overlay, for instance).
194
+    _renderContext.markDirty(this, false);
195
+  }
196
+
197
+  @override
198
+  void paint(PaintingContext context, Offset offset) {
199
+//    if (_isSelectionVisible) _paintSelection(context, offset);
200
+    super.paint(context, offset);
201
+//    if (_isCaretVisible) _paintCaret(context, offset);
202
+  }
203
+
204
+  @override
205
+  void markNeedsLayout() {
206
+    // Temporarily remove this object from the render context.
207
+    _renderContext.markDirty(this, true);
208
+    super.markNeedsLayout();
209
+  }
210
+
211
+  @override
212
+  ui.TextPosition getPositionForOffset(ui.Offset offset) {
213
+    return new ui.TextPosition(offset: node.documentOffset);
214
+  }
215
+
216
+  //
217
+  // Private members
218
+  //
219
+
220
+//  final CaretPainter _caretPainter = new CaretPainter();
221
+//  List<ui.TextBox> _selectionRects;
222
+
223
+  /// Returns `true` if current selection is collapsed, located within
224
+  /// this paragraph and is visible according to tick timer.
225
+//  bool get _isCaretVisible {
226
+//    if (!_selection.isCollapsed) return false;
227
+//    if (!_showCursor.value) return false;
228
+//
229
+//    final int start = node.documentOffset;
230
+//    final int end = start + node.length;
231
+//    final int caretOffset = _selection.extentOffset;
232
+//    return caretOffset >= start && caretOffset < end;
233
+//  }
234
+
235
+  /// Returns `true` if selection is not collapsed and intersects with this
236
+  /// paragraph.
237
+//  bool get _isSelectionVisible {
238
+//    if (_selection.isCollapsed) return false;
239
+//    return _intersectsWithSelection(_selection);
240
+//  }
241
+
242
+  /// Returns `true` if this paragraph intersects with document [selection].
243
+  bool _intersectsWithSelection(TextSelection selection) {
244
+    final int base = node.documentOffset;
245
+    final int extent = base + node.length;
246
+    return base <= selection.extentOffset && selection.baseOffset <= extent;
247
+  }
248
+
249
+  @override
250
+  TextRange getWordBoundary(ui.TextPosition position) {
251
+    return new TextRange(start: position.offset, end: position.offset + 1);
252
+  }
253
+
254
+//
255
+//  void _paintCaret(PaintingContext context, Offset offset) {
256
+//    final TextPosition caret = new TextPosition(
257
+//      offset: _selection.extentOffset - node.documentOffset,
258
+//    );
259
+//    Offset caretOffset = getOffsetForCaret(caret, _caretPainter.prototype);
260
+//    _caretPainter.paint(context.canvas, caretOffset + offset);
261
+//  }
262
+//
263
+//  void _paintSelection(PaintingContext context, Offset offset) {
264
+//    assert(_isSelectionVisible);
265
+//    // TODO: this could be improved by painting additional box for line-break characters.
266
+//    _selectionRects ??= getBoxesForSelection(getLocalSelection(_selection));
267
+//    final Paint paint = new Paint()..color = _selectionColor;
268
+//    for (ui.TextBox box in _selectionRects)
269
+//      context.canvas.drawRect(box.toRect().shift(offset), paint);
270
+//  }
271
+}

+ 333
- 0
packages/zefyr/lib/src/widgets/editable_paragraph.dart Целия файл

@@ -0,0 +1,333 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:math' as math;
5
+import 'dart:ui' as ui;
6
+
7
+import 'package:flutter/rendering.dart';
8
+import 'package:flutter/widgets.dart';
9
+import 'package:notus/notus.dart';
10
+
11
+import 'caret.dart';
12
+import 'editable_box.dart';
13
+import 'render_context.dart';
14
+
15
+class EditableParagraph extends LeafRenderObjectWidget {
16
+  EditableParagraph({
17
+    @required this.node,
18
+    @required this.text,
19
+    @required this.layerLink,
20
+    @required this.renderContext,
21
+    @required this.showCursor,
22
+    @required this.selection,
23
+    @required this.selectionColor,
24
+  }) : assert(renderContext != null);
25
+
26
+  final ContainerNode node;
27
+  final TextSpan text;
28
+  final LayerLink layerLink;
29
+  final ZefyrRenderContext renderContext;
30
+  final ValueNotifier<bool> showCursor;
31
+  final TextSelection selection;
32
+  final Color selectionColor;
33
+
34
+  @override
35
+  RenderObject createRenderObject(BuildContext context) {
36
+    return new RenderEditableParagraph(
37
+      text,
38
+      node: node,
39
+      layerLink: layerLink,
40
+      renderContext: renderContext,
41
+      showCursor: showCursor,
42
+      selection: selection,
43
+      selectionColor: selectionColor,
44
+      textDirection: Directionality.of(context),
45
+    );
46
+  }
47
+
48
+  @override
49
+  void updateRenderObject(
50
+      BuildContext context, RenderEditableParagraph renderObject) {
51
+    renderObject
52
+      ..text = text
53
+      ..node = node
54
+      ..layerLink = layerLink
55
+      ..renderContext = renderContext
56
+      ..showCursor = showCursor
57
+      ..selection = selection
58
+      ..selectionColor = selectionColor;
59
+  }
60
+}
61
+
62
+class RenderEditableParagraph extends RenderParagraph
63
+    implements RenderEditableBox {
64
+  RenderEditableParagraph(
65
+    TextSpan text, {
66
+    @required ContainerNode node,
67
+    @required LayerLink layerLink,
68
+    @required ZefyrRenderContext renderContext,
69
+    @required ValueNotifier<bool> showCursor,
70
+    @required TextSelection selection,
71
+    @required Color selectionColor,
72
+    TextAlign textAlign: TextAlign.start,
73
+    @required TextDirection textDirection,
74
+    bool softWrap: true,
75
+    TextOverflow overflow: TextOverflow.clip,
76
+    double textScaleFactor: 1.0,
77
+    int maxLines,
78
+  })  : _node = node,
79
+        _layerLink = layerLink,
80
+        _renderContext = renderContext,
81
+        _showCursor = showCursor,
82
+        _selection = selection,
83
+        _selectionColor = selectionColor,
84
+        _prototypePainter = new TextPainter(
85
+          text: new TextSpan(text: '.', style: text.style),
86
+          textAlign: textAlign,
87
+          textDirection: textDirection,
88
+          textScaleFactor: textScaleFactor,
89
+        ),
90
+        super(
91
+          text,
92
+          textAlign: textAlign,
93
+          textDirection: textDirection,
94
+          softWrap: softWrap,
95
+          overflow: overflow,
96
+          textScaleFactor: textScaleFactor,
97
+          maxLines: maxLines,
98
+        );
99
+
100
+  //
101
+  // Public members
102
+  //
103
+
104
+  ContainerNode get node => _node;
105
+  ContainerNode _node;
106
+  void set node(ContainerNode value) {
107
+    _node = value;
108
+  }
109
+
110
+  LayerLink get layerLink => _layerLink;
111
+  LayerLink _layerLink;
112
+  void set layerLink(LayerLink value) {
113
+    if (_layerLink == value) return;
114
+    _layerLink = value;
115
+  }
116
+
117
+  ZefyrRenderContext _renderContext;
118
+  void set renderContext(ZefyrRenderContext value) {
119
+    if (_renderContext == value) return;
120
+    if (attached) _renderContext.removeBox(this);
121
+    _renderContext = value;
122
+    if (attached) _renderContext.addBox(this);
123
+  }
124
+
125
+  ValueNotifier<bool> _showCursor;
126
+  set showCursor(ValueNotifier<bool> value) {
127
+    assert(value != null);
128
+    if (_showCursor == value) return;
129
+    if (attached) _showCursor.removeListener(markNeedsPaint);
130
+    _showCursor = value;
131
+    if (attached) _showCursor.addListener(markNeedsPaint);
132
+    markNeedsPaint();
133
+  }
134
+
135
+  TextSelection _selection;
136
+  set selection(TextSelection value) {
137
+    if (_selection == value) return;
138
+    // TODO: check if selection affects this block (also check previous value)
139
+    _selection = value;
140
+    _selectionRects = null;
141
+    markNeedsPaint();
142
+  }
143
+
144
+  Color _selectionColor;
145
+  set selectionColor(Color value) {
146
+    if (_selectionColor == value) return;
147
+    _selectionColor = value;
148
+    markNeedsPaint();
149
+  }
150
+
151
+  double get preferredLineHeight => _prototypePainter.height;
152
+
153
+  /// Returns part of document [selection] local to this paragraph. May return
154
+  /// `null`.
155
+  ///
156
+  /// [selection] must not be collapsed.
157
+  TextSelection getLocalSelection(TextSelection selection) {
158
+    if (!_intersectsWithSelection(selection)) return null;
159
+
160
+    int nodeBase = node.documentOffset;
161
+    int nodeExtent = nodeBase + node.length;
162
+    int base = math.max(0, selection.baseOffset - nodeBase);
163
+    int extent = math.min(selection.extentOffset, nodeExtent) - nodeBase;
164
+    return _selection.copyWith(baseOffset: base, extentOffset: extent);
165
+  }
166
+
167
+  // This method works around some issues in getBoxesForSelection and handleы
168
+  // edge-case with our TextSpan objects not having last line-break character.
169
+  // Wait for https://github.com/flutter/flutter/issues/16418 to be resolved.
170
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
171
+      {bool isLocal: false}) {
172
+    TextSelection local = isLocal ? selection : getLocalSelection(selection);
173
+    if (local.isCollapsed) {
174
+      final offset = getOffsetForCaret(local.extent, _caretPainter.prototype);
175
+      return [
176
+        new ui.TextBox.fromLTRBD(
177
+          offset.dx,
178
+          offset.dy,
179
+          offset.dx,
180
+          offset.dy + _caretPainter.prototype.height,
181
+          TextDirection.ltr,
182
+        )
183
+      ];
184
+    }
185
+
186
+    int isBaseShifted = 0;
187
+    bool isExtentShifted = false;
188
+    if (local.baseOffset == node.length - 1 && local.baseOffset > 0) {
189
+      // Since we exclude last line-break from rendered TextSpan we have to
190
+      // handle end-of-line selection explicitly.
191
+      local = local.copyWith(baseOffset: local.baseOffset - 1);
192
+      isBaseShifted = -1;
193
+    } else if (local.baseOffset == 0 && local.isCollapsed) {
194
+      // This takes care of beginning of line position.
195
+      local = local.copyWith(baseOffset: local.baseOffset + 1);
196
+      isBaseShifted = 1;
197
+    }
198
+    if (text.codeUnitAt(local.extentOffset - 1) == 0xA) {
199
+      // This takes care of the rest end-of-line scenarios, where there are
200
+      // actually line-breaks in the TextSpan (e.g. in code blocks).
201
+      local = local.copyWith(extentOffset: local.extentOffset + 1);
202
+      isExtentShifted = true;
203
+    }
204
+    final result = getBoxesForSelection(local).toList();
205
+    if (isBaseShifted != 0) {
206
+      final box = result.first;
207
+      final dx = isBaseShifted == -1 ? box.right : box.left;
208
+      result.removeAt(0);
209
+      result.insert(0,
210
+          new ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction));
211
+    }
212
+    if (isExtentShifted) {
213
+      final box = result.last;
214
+      result.removeLast;
215
+      result.add(new ui.TextBox.fromLTRBD(
216
+          box.left, box.top, box.left, box.bottom, box.direction));
217
+    }
218
+    return result;
219
+  }
220
+
221
+  //
222
+  // Overridden members
223
+  //
224
+
225
+  @override
226
+  void set text(TextSpan value) {
227
+    _prototypePainter.text = new TextSpan(text: '.', style: value.style);
228
+    _selectionRects = null;
229
+    super.text = value;
230
+  }
231
+
232
+  @override
233
+  void attach(PipelineOwner owner) {
234
+    super.attach(owner);
235
+    _showCursor.addListener(markNeedsPaint);
236
+    _renderContext.addBox(this);
237
+  }
238
+
239
+  @override
240
+  void detach() {
241
+    _showCursor.removeListener(markNeedsPaint);
242
+    _renderContext.removeBox(this);
243
+    super.detach();
244
+  }
245
+
246
+  @override
247
+  bool hitTestSelf(Offset position) => true;
248
+
249
+  @override
250
+  bool hitTest(HitTestResult result, {Offset position}) {
251
+    if (size.contains(position)) {
252
+      result.add(new BoxHitTestEntry(this, position));
253
+      return true;
254
+    }
255
+    return false;
256
+  }
257
+
258
+  @override
259
+  void performLayout() {
260
+    super.performLayout();
261
+    _prototypePainter.layout(
262
+        minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
263
+    _caretPainter.layout(_prototypePainter.height);
264
+    // Indicate to render context that this object can be used by other
265
+    // layers (selection overlay, for instance).
266
+    _renderContext.markDirty(this, false);
267
+  }
268
+
269
+  @override
270
+  void paint(PaintingContext context, Offset offset) {
271
+    if (_isSelectionVisible) _paintSelection(context, offset);
272
+    super.paint(context, offset);
273
+    if (_isCaretVisible) _paintCaret(context, offset);
274
+  }
275
+
276
+  @override
277
+  void markNeedsLayout() {
278
+    // Temporarily remove this object from the render context.
279
+    _renderContext.markDirty(this, true);
280
+    super.markNeedsLayout();
281
+  }
282
+
283
+  //
284
+  // Private members
285
+  //
286
+
287
+  final TextPainter _prototypePainter;
288
+  final CaretPainter _caretPainter = new CaretPainter();
289
+  List<ui.TextBox> _selectionRects;
290
+
291
+  /// Returns `true` if current selection is collapsed, located within
292
+  /// this paragraph and is visible according to tick timer.
293
+  bool get _isCaretVisible {
294
+    if (!_selection.isCollapsed) return false;
295
+    if (!_showCursor.value) return false;
296
+
297
+    final int start = node.documentOffset;
298
+    final int end = start + node.length;
299
+    final int caretOffset = _selection.extentOffset;
300
+    return caretOffset >= start && caretOffset < end;
301
+  }
302
+
303
+  /// Returns `true` if selection is not collapsed and intersects with this
304
+  /// paragraph.
305
+  bool get _isSelectionVisible {
306
+    if (_selection.isCollapsed) return false;
307
+    return _intersectsWithSelection(_selection);
308
+  }
309
+
310
+  /// Returns `true` if this paragraph intersects with document [selection].
311
+  bool _intersectsWithSelection(TextSelection selection) {
312
+    final int base = node.documentOffset;
313
+    final int extent = base + node.length;
314
+    return base <= selection.extentOffset && selection.baseOffset <= extent;
315
+  }
316
+
317
+  void _paintCaret(PaintingContext context, Offset offset) {
318
+    final TextPosition caret = new TextPosition(
319
+      offset: _selection.extentOffset - node.documentOffset,
320
+    );
321
+    Offset caretOffset = getOffsetForCaret(caret, _caretPainter.prototype);
322
+    _caretPainter.paint(context.canvas, caretOffset + offset);
323
+  }
324
+
325
+  void _paintSelection(PaintingContext context, Offset offset) {
326
+    assert(_isSelectionVisible);
327
+    // TODO: this could be improved by painting additional box for line-break characters.
328
+    _selectionRects ??= getBoxesForSelection(getLocalSelection(_selection));
329
+    final Paint paint = new Paint()..color = _selectionColor;
330
+    for (ui.TextBox box in _selectionRects)
331
+      context.canvas.drawRect(box.toRect().shift(offset), paint);
332
+  }
333
+}

+ 322
- 0
packages/zefyr/lib/src/widgets/editable_text.dart Целия файл

@@ -0,0 +1,322 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:async';
5
+
6
+import 'package:collection/collection.dart';
7
+import 'package:flutter/cupertino.dart';
8
+import 'package:flutter/widgets.dart';
9
+import 'package:notus/notus.dart';
10
+
11
+import 'code.dart';
12
+import 'common.dart';
13
+import 'controller.dart';
14
+import 'editable_box.dart';
15
+import 'editor.dart';
16
+import 'input.dart';
17
+import 'list.dart';
18
+import 'paragraph.dart';
19
+import 'quote.dart';
20
+import 'render_context.dart';
21
+import 'selection.dart';
22
+
23
+/// Core widget responsible for editing Zefyr documents.
24
+///
25
+/// Depends on presence of [ZefyrTheme] somewhere up the widget tree.
26
+///
27
+/// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
28
+/// edit style attributes.
29
+class ZefyrEditableText extends StatefulWidget {
30
+  const ZefyrEditableText({
31
+    Key key,
32
+    @required this.controller,
33
+    @required this.focusNode,
34
+    this.autofocus: true,
35
+    this.enabled: true,
36
+  }) : super(key: key);
37
+
38
+  final ZefyrController controller;
39
+  final FocusNode focusNode;
40
+  final bool autofocus;
41
+  final bool enabled;
42
+
43
+  static ZefyrEditableTextScope of(BuildContext context) {
44
+    final ZefyrEditableTextScope result =
45
+        context.inheritFromWidgetOfExactType(ZefyrEditableTextScope);
46
+    return result;
47
+  }
48
+
49
+  @override
50
+  _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
51
+}
52
+
53
+/// Provides access to shared state of [ZefyrEditableText].
54
+class ZefyrEditableTextScope extends InheritedWidget {
55
+  static const _kEquality = const SetEquality<RenderEditableBox>();
56
+
57
+  ZefyrEditableTextScope({
58
+    Key key,
59
+    @required Widget child,
60
+    @required this.selection,
61
+    @required this.showCursor,
62
+    @required this.renderContext,
63
+  })  : _activeParagraphs = new Set.from(renderContext.active),
64
+        super(key: key, child: child);
65
+
66
+  final TextSelection selection;
67
+  final ValueNotifier<bool> showCursor;
68
+  final ZefyrRenderContext renderContext;
69
+  final Set<RenderEditableBox> _activeParagraphs;
70
+
71
+  @override
72
+  bool updateShouldNotify(ZefyrEditableTextScope oldWidget) {
73
+    return selection != oldWidget.selection ||
74
+        showCursor != oldWidget.showCursor ||
75
+        !_kEquality.equals(_activeParagraphs, oldWidget._activeParagraphs);
76
+  }
77
+}
78
+
79
+class _ZefyrEditableTextState extends State<ZefyrEditableText>
80
+    with AutomaticKeepAliveClientMixin {
81
+  //
82
+  // New public members
83
+  //
84
+
85
+  /// Focus node of this widget.
86
+  FocusNode get focusNode => widget.focusNode;
87
+
88
+  /// Document controlled by this widget.
89
+  NotusDocument get document => widget.controller.document;
90
+
91
+  /// Current text selection.
92
+  TextSelection get selection => widget.controller.selection;
93
+  ZefyrRenderContext get renderContext => _renderContext;
94
+  ValueNotifier<bool> get showCursor => _cursorTimer.value;
95
+
96
+  /// Express interest in interacting with the keyboard.
97
+  ///
98
+  /// If this control is already attached to the keyboard, this function will
99
+  /// request that the keyboard become visible. Otherwise, this function will
100
+  /// ask the focus system that it become focused. If successful in acquiring
101
+  /// focus, the control will then attach to the keyboard and request that the
102
+  /// keyboard become visible.
103
+  void requestKeyboard() {
104
+    if (focusNode.hasFocus)
105
+      _input.openConnection(widget.controller.plainTextEditingValue);
106
+    else
107
+      FocusScope.of(context).requestFocus(focusNode);
108
+  }
109
+
110
+  //
111
+  // Overridden members of State
112
+  //
113
+
114
+  @override
115
+  Widget build(BuildContext context) {
116
+    FocusScope.of(context).reparentIfNeeded(focusNode);
117
+    super.build(context); // See AutomaticKeepAliveState.
118
+    ZefyrEditor.of(context);
119
+
120
+    final scrollable = SingleChildScrollView(
121
+      padding: EdgeInsets.only(top: 16.0),
122
+      physics: AlwaysScrollableScrollPhysics(),
123
+      controller: _scrollController,
124
+      child: ListBody(children: _buildChildren(context)),
125
+    );
126
+
127
+    final overlay = Overlay.of(context, debugRequiredFor: widget);
128
+    final layers = <Widget>[scrollable];
129
+    if (widget.enabled) {
130
+      layers.add(ZefyrSelectionOverlay(
131
+        controller: widget.controller,
132
+        controls: cupertinoTextSelectionControls,
133
+        overlay: overlay,
134
+      ));
135
+    }
136
+
137
+    return new ZefyrEditableTextScope(
138
+      selection: selection,
139
+      showCursor: showCursor,
140
+      renderContext: renderContext,
141
+      child: Stack(fit: StackFit.expand, children: layers),
142
+    );
143
+  }
144
+
145
+  @override
146
+  void initState() {
147
+    super.initState();
148
+    _input = new InputConnectionController(_handleRemoteValueChange);
149
+    _updateSubscriptions();
150
+  }
151
+
152
+  @override
153
+  void didUpdateWidget(ZefyrEditableText oldWidget) {
154
+    super.didUpdateWidget(oldWidget);
155
+    _updateSubscriptions(oldWidget);
156
+    if (!_didAutoFocus && widget.autofocus && widget.enabled) {
157
+      FocusScope.of(context).autofocus(focusNode);
158
+      _didAutoFocus = true;
159
+    }
160
+    if (!widget.enabled && focusNode.hasFocus) {
161
+      _didAutoFocus = false;
162
+      focusNode.unfocus();
163
+    }
164
+  }
165
+
166
+  @override
167
+  void dispose() {
168
+    _cancelSubscriptions();
169
+    super.dispose();
170
+  }
171
+
172
+  //
173
+  // Overridden members of AutomaticKeepAliveClientMixin
174
+  //
175
+
176
+  @override
177
+  bool get wantKeepAlive => focusNode.hasFocus;
178
+
179
+  //
180
+  // Private members
181
+  //
182
+
183
+  final ScrollController _scrollController = new ScrollController();
184
+  final ZefyrRenderContext _renderContext = new ZefyrRenderContext();
185
+  final _CursorTimer _cursorTimer = new _CursorTimer();
186
+  InputConnectionController _input;
187
+  bool _didAutoFocus = false;
188
+
189
+  List<Widget> _buildChildren(BuildContext context) {
190
+    final result = <Widget>[];
191
+    for (var node in document.root.children) {
192
+      result.add(_defaultChildBuilder(context, node));
193
+    }
194
+    return result;
195
+  }
196
+
197
+  Widget _defaultChildBuilder(BuildContext context, Node node) {
198
+    if (node is LineNode) {
199
+      if (node.hasEmbed) {
200
+        return new RawZefyrLine(node: node);
201
+      } else if (node.style.contains(NotusAttribute.heading)) {
202
+        return new ZefyrHeading(node: node);
203
+      }
204
+      return new ZefyrParagraph(node: node);
205
+    }
206
+
207
+    final BlockNode block = node;
208
+    final blockStyle = block.style.get(NotusAttribute.block);
209
+    if (blockStyle == NotusAttribute.block.code) {
210
+      return new ZefyrCode(node: node);
211
+    } else if (blockStyle == NotusAttribute.block.bulletList) {
212
+      return new ZefyrList(node: node);
213
+    } else if (blockStyle == NotusAttribute.block.numberList) {
214
+      return new ZefyrList(node: node);
215
+    } else if (blockStyle == NotusAttribute.block.quote) {
216
+      return new ZefyrQuote(node: node);
217
+    }
218
+
219
+    throw new UnimplementedError('Block format $blockStyle.');
220
+  }
221
+
222
+  void _updateSubscriptions([ZefyrEditableText oldWidget]) {
223
+    if (oldWidget == null) {
224
+      _renderContext.addListener(_handleRenderContextChange);
225
+      widget.controller.addListener(_handleLocalValueChange);
226
+      focusNode.addListener(_handleFocusChange);
227
+      return;
228
+    }
229
+
230
+    if (widget.controller != oldWidget.controller) {
231
+      oldWidget.controller.removeListener(_handleLocalValueChange);
232
+      widget.controller.addListener(_handleLocalValueChange);
233
+      _input.updateRemoteValue(widget.controller.plainTextEditingValue);
234
+    }
235
+    if (widget.focusNode != oldWidget.focusNode) {
236
+      oldWidget.focusNode.removeListener(_handleFocusChange);
237
+      widget.focusNode.addListener(_handleFocusChange);
238
+      updateKeepAlive();
239
+    }
240
+  }
241
+
242
+  void _cancelSubscriptions() {
243
+    _renderContext.removeListener(_handleRenderContextChange);
244
+    _renderContext.dispose();
245
+    widget.controller.removeListener(_handleLocalValueChange);
246
+    focusNode.removeListener(_handleFocusChange);
247
+    _input.closeConnection();
248
+    _cursorTimer.stop();
249
+  }
250
+
251
+  // Triggered for both text and selection changes.
252
+  void _handleLocalValueChange() {
253
+    if (widget.enabled &&
254
+        widget.controller.lastChangeSource == ChangeSource.local) {
255
+      // Only request keyboard for user actions.
256
+      requestKeyboard();
257
+    }
258
+    _input.updateRemoteValue(widget.controller.plainTextEditingValue);
259
+    _cursorTimer.startOrStop(focusNode, selection);
260
+    setState(() {
261
+      // nothing to update internally.
262
+    });
263
+  }
264
+
265
+  void _handleFocusChange() {
266
+    _input.openOrCloseConnection(
267
+        focusNode, widget.controller.plainTextEditingValue);
268
+    _cursorTimer.startOrStop(focusNode, selection);
269
+    updateKeepAlive();
270
+  }
271
+
272
+  void _handleRemoteValueChange(
273
+      int start, String deleted, String inserted, TextSelection selection) {
274
+    widget.controller
275
+        .replaceText(start, deleted.length, inserted, selection: selection);
276
+  }
277
+
278
+  void _handleRenderContextChange() {
279
+    setState(() {
280
+      // nothing to update internally.
281
+    });
282
+  }
283
+}
284
+
285
+/// Helper class that keeps state relevant to the cursor.
286
+class _CursorTimer {
287
+  static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
288
+
289
+  Timer _timer;
290
+  final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
291
+
292
+  ValueNotifier<bool> get value => _showCursor;
293
+
294
+  void _cursorTick(Timer timer) {
295
+    _showCursor.value = !_showCursor.value;
296
+  }
297
+
298
+  /// Starts cursor timer.
299
+  void start() {
300
+    _showCursor.value = true;
301
+    _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
302
+  }
303
+
304
+  /// Stops cursor timer.
305
+  void stop() {
306
+    _timer?.cancel();
307
+    _timer = null;
308
+    _showCursor.value = false;
309
+  }
310
+
311
+  /// Starts or stops cursor timer based on current state of [focusNode]
312
+  /// and [selection].
313
+  void startOrStop(FocusNode focusNode, TextSelection selection) {
314
+    final hasFocus = focusNode.hasFocus;
315
+    final selectionCollapsed = selection.isCollapsed;
316
+    if (_timer == null && hasFocus && selectionCollapsed) {
317
+      start();
318
+    } else if (_timer != null && (!hasFocus || !selectionCollapsed)) {
319
+      stop();
320
+    }
321
+  }
322
+}

+ 206
- 0
packages/zefyr/lib/src/widgets/editor.dart Целия файл

@@ -0,0 +1,206 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/widgets.dart';
5
+import 'package:notus/notus.dart';
6
+
7
+import 'controller.dart';
8
+import 'editable_text.dart';
9
+import 'theme.dart';
10
+import 'toolbar.dart';
11
+
12
+/// Widget for editing Zefyr documents.
13
+class ZefyrEditor extends StatefulWidget {
14
+  const ZefyrEditor({
15
+    Key key,
16
+    @required this.controller,
17
+    @required this.focusNode,
18
+    this.autofocus: true,
19
+    this.enabled: true,
20
+    this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
21
+    this.toolbarDelegate,
22
+  }) : super(key: key);
23
+
24
+  final ZefyrController controller;
25
+  final FocusNode focusNode;
26
+  final bool autofocus;
27
+  final bool enabled;
28
+  final ZefyrToolbarDelegate toolbarDelegate;
29
+
30
+  /// Padding around editable area.
31
+  final EdgeInsets padding;
32
+
33
+  static ZefyrEditorScope of(BuildContext context) {
34
+    ZefyrEditorScope scope =
35
+        context.inheritFromWidgetOfExactType(ZefyrEditorScope);
36
+    return scope;
37
+  }
38
+
39
+  @override
40
+  _ZefyrEditorState createState() => new _ZefyrEditorState();
41
+}
42
+
43
+/// Inherited widget which provides access to shared state of a Zefyr editor.
44
+class ZefyrEditorScope extends InheritedWidget {
45
+  /// Current selection style
46
+  final NotusStyle selectionStyle;
47
+  final TextSelection selection;
48
+  final FocusOwner focusOwner;
49
+  final FocusNode toolbarFocusNode;
50
+  final ZefyrController _controller;
51
+  final FocusNode _focusNode;
52
+
53
+  ZefyrEditorScope({
54
+    Key key,
55
+    @required Widget child,
56
+    @required this.selectionStyle,
57
+    @required this.selection,
58
+    @required this.focusOwner,
59
+    @required this.toolbarFocusNode,
60
+    @required ZefyrController controller,
61
+    @required FocusNode focusNode,
62
+  })  : _controller = controller,
63
+        _focusNode = focusNode,
64
+        super(key: key, child: child);
65
+
66
+  void updateSelection(TextSelection value,
67
+      {ChangeSource source: ChangeSource.remote}) {
68
+    _controller.updateSelection(value, source: source);
69
+  }
70
+
71
+  void formatSelection(NotusAttribute value) {
72
+    _controller.formatSelection(value);
73
+  }
74
+
75
+  void focus(BuildContext context) {
76
+    FocusScope.of(context).requestFocus(_focusNode);
77
+  }
78
+
79
+  void hideKeyboard() {
80
+    _focusNode.unfocus();
81
+  }
82
+
83
+  @override
84
+  bool updateShouldNotify(ZefyrEditorScope oldWidget) {
85
+    return (selectionStyle != oldWidget.selectionStyle ||
86
+        selection != oldWidget.selection ||
87
+        focusOwner != oldWidget.focusOwner);
88
+  }
89
+}
90
+
91
+class _ZefyrEditorState extends State<ZefyrEditor> {
92
+  final FocusNode _toolbarFocusNode = new FocusNode();
93
+
94
+  NotusStyle _selectionStyle;
95
+  TextSelection _selection;
96
+  FocusOwner _focusOwner;
97
+
98
+  FocusOwner getFocusOwner() {
99
+    if (widget.focusNode.hasFocus) {
100
+      return FocusOwner.editor;
101
+    } else if (_toolbarFocusNode.hasFocus) {
102
+      return FocusOwner.toolbar;
103
+    } else {
104
+      return FocusOwner.none;
105
+    }
106
+  }
107
+
108
+  @override
109
+  void initState() {
110
+    super.initState();
111
+    _selectionStyle = widget.controller.getSelectionStyle();
112
+    _selection = widget.controller.selection;
113
+    _focusOwner = getFocusOwner();
114
+    widget.controller.addListener(_handleControllerChange);
115
+    _toolbarFocusNode.addListener(_handleFocusChange);
116
+    widget.focusNode.addListener(_handleFocusChange);
117
+  }
118
+
119
+  @override
120
+  void didUpdateWidget(ZefyrEditor oldWidget) {
121
+    super.didUpdateWidget(oldWidget);
122
+    if (widget.focusNode != oldWidget.focusNode) {
123
+      oldWidget.focusNode.removeListener(_handleFocusChange);
124
+      widget.focusNode.addListener(_handleFocusChange);
125
+    }
126
+    if (widget.controller != oldWidget.controller) {
127
+      oldWidget.controller.removeListener(_handleControllerChange);
128
+      widget.controller.addListener(_handleControllerChange);
129
+      _selectionStyle = widget.controller.getSelectionStyle();
130
+      _selection = widget.controller.selection;
131
+    }
132
+  }
133
+
134
+  @override
135
+  void dispose() {
136
+    widget.controller.removeListener(_handleControllerChange);
137
+    widget.focusNode.removeListener(_handleFocusChange);
138
+    _toolbarFocusNode.removeListener(_handleFocusChange);
139
+    _toolbarFocusNode.dispose();
140
+    super.dispose();
141
+  }
142
+
143
+  @override
144
+  Widget build(BuildContext context) {
145
+    Widget editable = new ZefyrEditableText(
146
+      controller: widget.controller,
147
+      focusNode: widget.focusNode,
148
+      autofocus: widget.autofocus,
149
+      enabled: widget.enabled,
150
+    );
151
+    if (widget.padding != null) {
152
+      editable = new Padding(padding: widget.padding, child: editable);
153
+    }
154
+    final children = <Widget>[];
155
+    children.add(Expanded(child: editable));
156
+    final toolbar = ZefyrToolbar(
157
+      focusNode: _toolbarFocusNode,
158
+      controller: widget.controller,
159
+      delegate: widget.toolbarDelegate,
160
+    );
161
+    children.add(toolbar);
162
+
163
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
164
+    final fallbackTheme = ZefyrThemeData.fallback(context);
165
+    final actualTheme = (parentTheme != null)
166
+        ? fallbackTheme.merge(parentTheme)
167
+        : fallbackTheme;
168
+
169
+    return ZefyrTheme(
170
+      data: actualTheme,
171
+      child: ZefyrEditorScope(
172
+        selection: _selection,
173
+        selectionStyle: _selectionStyle,
174
+        focusOwner: _focusOwner,
175
+        toolbarFocusNode: _toolbarFocusNode,
176
+        controller: widget.controller,
177
+        focusNode: widget.focusNode,
178
+        child: Column(children: children),
179
+      ),
180
+    );
181
+  }
182
+
183
+  void _handleControllerChange() {
184
+    final attrs = widget.controller.getSelectionStyle();
185
+    final selection = widget.controller.selection;
186
+    if (_selectionStyle != attrs || _selection != selection) {
187
+      setState(() {
188
+        _selectionStyle = attrs;
189
+        _selection = widget.controller.selection;
190
+      });
191
+    }
192
+  }
193
+
194
+  void _handleFocusChange() {
195
+    setState(() {
196
+      _focusOwner = getFocusOwner();
197
+      if (_focusOwner == FocusOwner.none && !_selection.isCollapsed) {
198
+        // Collapse selection if there is nothing focused.
199
+        widget.controller.updateSelection(_selection.copyWith(
200
+          baseOffset: _selection.extentOffset,
201
+          extentOffset: _selection.extentOffset,
202
+        ));
203
+      }
204
+    });
205
+  }
206
+}

+ 277
- 0
packages/zefyr/lib/src/widgets/horizontal_rule.dart Целия файл

@@ -0,0 +1,277 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:math' as math;
5
+import 'dart:ui' as ui;
6
+
7
+import 'package:flutter/material.dart';
8
+import 'package:flutter/rendering.dart';
9
+import 'package:flutter/widgets.dart';
10
+import 'package:notus/notus.dart';
11
+
12
+import 'caret.dart';
13
+import 'editable_box.dart';
14
+import 'render_context.dart';
15
+
16
+class HorizontalRule extends LeafRenderObjectWidget {
17
+  HorizontalRule({
18
+    @required this.node,
19
+    @required this.layerLink,
20
+    @required this.renderContext,
21
+    @required this.showCursor,
22
+    @required this.selection,
23
+    @required this.selectionColor,
24
+  }) : assert(renderContext != null);
25
+
26
+  final ContainerNode node;
27
+  final LayerLink layerLink;
28
+  final ZefyrRenderContext renderContext;
29
+  final ValueNotifier<bool> showCursor;
30
+  final TextSelection selection;
31
+  final Color selectionColor;
32
+
33
+  @override
34
+  RenderHorizontalRule createRenderObject(BuildContext context) {
35
+    return new RenderHorizontalRule(
36
+      node: node,
37
+      layerLink: layerLink,
38
+      renderContext: renderContext,
39
+      showCursor: showCursor,
40
+      selection: selection,
41
+      selectionColor: selectionColor,
42
+    );
43
+  }
44
+
45
+  @override
46
+  void updateRenderObject(
47
+      BuildContext context, RenderHorizontalRule renderObject) {
48
+    renderObject
49
+      ..node = node
50
+      ..layerLink = layerLink
51
+      ..renderContext = renderContext
52
+      ..showCursor = showCursor
53
+      ..selection = selection
54
+      ..selectionColor = selectionColor;
55
+  }
56
+}
57
+
58
+class RenderHorizontalRule extends RenderBox implements RenderEditableBox {
59
+  static const kPaddingBottom = 16.0;
60
+  static const kWidth = 3.0;
61
+
62
+  RenderHorizontalRule({
63
+    @required ContainerNode node,
64
+    @required LayerLink layerLink,
65
+    @required ZefyrRenderContext renderContext,
66
+    @required ValueNotifier<bool> showCursor,
67
+    @required TextSelection selection,
68
+    @required Color selectionColor,
69
+  })  : _node = node,
70
+        _layerLink = layerLink,
71
+        _renderContext = renderContext,
72
+        _showCursor = showCursor,
73
+        _selection = selection,
74
+        _selectionColor = selectionColor,
75
+        super();
76
+
77
+  //
78
+  // Public members
79
+  //
80
+
81
+  ContainerNode get node => _node;
82
+  ContainerNode _node;
83
+  void set node(ContainerNode value) {
84
+    _node = value;
85
+  }
86
+
87
+  LayerLink get layerLink => _layerLink;
88
+  LayerLink _layerLink;
89
+  void set layerLink(LayerLink value) {
90
+    if (_layerLink == value) return;
91
+    _layerLink = value;
92
+  }
93
+
94
+  ZefyrRenderContext _renderContext;
95
+  void set renderContext(ZefyrRenderContext value) {
96
+    if (_renderContext == value) return;
97
+    if (attached) _renderContext.removeBox(this);
98
+    _renderContext = value;
99
+    if (attached) _renderContext.addBox(this);
100
+  }
101
+
102
+  ValueNotifier<bool> _showCursor;
103
+  set showCursor(ValueNotifier<bool> value) {
104
+    assert(value != null);
105
+    if (_showCursor == value) return;
106
+    if (attached) _showCursor.removeListener(markNeedsPaint);
107
+    _showCursor = value;
108
+    if (attached) _showCursor.addListener(markNeedsPaint);
109
+    markNeedsPaint();
110
+  }
111
+
112
+  TextSelection _selection;
113
+  set selection(TextSelection value) {
114
+    if (_selection == value) return;
115
+    // TODO: check if selection affects this block (also check previous value)
116
+    _selection = value;
117
+    markNeedsPaint();
118
+  }
119
+
120
+  Color _selectionColor;
121
+  set selectionColor(Color value) {
122
+    if (_selectionColor == value) return;
123
+    _selectionColor = value;
124
+    markNeedsPaint();
125
+  }
126
+
127
+  double get preferredLineHeight => size.height;
128
+
129
+  /// Returns part of document [selection] local to this paragraph. May return
130
+  /// `null`.
131
+  ///
132
+  /// [selection] must not be collapsed.
133
+  TextSelection getLocalSelection(TextSelection selection) {
134
+    if (!_intersectsWithSelection(selection)) return null;
135
+
136
+    int nodeBase = node.documentOffset;
137
+    int nodeExtent = nodeBase + node.length;
138
+    int base = math.max(0, selection.baseOffset - nodeBase);
139
+    int extent = math.min(selection.extentOffset, nodeExtent) - nodeBase;
140
+    return _selection.copyWith(baseOffset: base, extentOffset: extent);
141
+  }
142
+
143
+  List<ui.TextBox> getEndpointsForSelection(TextSelection selection,
144
+      {bool isLocal: false}) {
145
+    TextSelection local = isLocal ? selection : getLocalSelection(selection);
146
+    if (local.isCollapsed) {
147
+      return [
148
+        new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
149
+      ];
150
+    }
151
+
152
+    return [
153
+      new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr),
154
+      new ui.TextBox.fromLTRBD(
155
+          size.width, 0.0, size.width, size.height, TextDirection.ltr),
156
+    ];
157
+  }
158
+
159
+  //
160
+  // Overridden members
161
+  //
162
+
163
+  @override
164
+  void attach(PipelineOwner owner) {
165
+    super.attach(owner);
166
+    _showCursor.addListener(markNeedsPaint);
167
+    _renderContext.addBox(this);
168
+  }
169
+
170
+  @override
171
+  void detach() {
172
+    _showCursor.removeListener(markNeedsPaint);
173
+    _renderContext.removeBox(this);
174
+    super.detach();
175
+  }
176
+
177
+  @override
178
+  bool hitTestSelf(Offset position) => true;
179
+
180
+  @override
181
+  bool hitTest(HitTestResult result, {Offset position}) {
182
+    if (size.contains(position)) {
183
+      result.add(new BoxHitTestEntry(this, position));
184
+      return true;
185
+    }
186
+    return false;
187
+  }
188
+
189
+  @override
190
+  void performLayout() {
191
+    assert(constraints.hasBoundedWidth);
192
+    final height = kWidth + kPaddingBottom;
193
+    size = new Size(constraints.maxWidth, height);
194
+    _caretPainter.layout(height);
195
+    // Indicate to render context that this object can be used by other
196
+    // layers (selection overlay, for instance).
197
+    _renderContext.markDirty(this, false);
198
+  }
199
+
200
+  @override
201
+  void paint(PaintingContext context, Offset offset) {
202
+//    if (_isSelectionVisible) _paintSelection(context, offset);
203
+    final rect = new Rect.fromLTWH(0.0, 0.0, size.width, kWidth);
204
+    final paint = new ui.Paint()..color = Colors.grey.shade200;
205
+    context.canvas.drawRect(rect.shift(offset), paint);
206
+    if (_isCaretVisible) _paintCaret(context, offset);
207
+  }
208
+
209
+  @override
210
+  void markNeedsLayout() {
211
+    // Temporarily remove this object from the render context.
212
+    _renderContext.markDirty(this, true);
213
+    super.markNeedsLayout();
214
+  }
215
+
216
+  @override
217
+  ui.TextPosition getPositionForOffset(ui.Offset offset) {
218
+    return new ui.TextPosition(offset: node.documentOffset);
219
+  }
220
+
221
+  @override
222
+  TextRange getWordBoundary(ui.TextPosition position) {
223
+    return new TextRange(start: position.offset, end: position.offset + 1);
224
+  }
225
+
226
+  //
227
+  // Private members
228
+  //
229
+
230
+  final CaretPainter _caretPainter = new CaretPainter();
231
+//  List<ui.TextBox> _selectionRects;
232
+
233
+  /// Returns `true` if current selection is collapsed, located within
234
+  /// this paragraph and is visible according to tick timer.
235
+  bool get _isCaretVisible {
236
+    if (!_selection.isCollapsed) return false;
237
+    if (!_showCursor.value) return false;
238
+
239
+    final int start = node.documentOffset;
240
+    final int end = start + node.length;
241
+    final int caretOffset = _selection.extentOffset;
242
+    return caretOffset >= start && caretOffset < end;
243
+  }
244
+
245
+  /// Returns `true` if selection is not collapsed and intersects with this
246
+  /// paragraph.
247
+//  bool get _isSelectionVisible {
248
+//    if (_selection.isCollapsed) return false;
249
+//    return _intersectsWithSelection(_selection);
250
+//  }
251
+
252
+  /// Returns `true` if this paragraph intersects with document [selection].
253
+  bool _intersectsWithSelection(TextSelection selection) {
254
+    final int base = node.documentOffset;
255
+    final int extent = base + node.length;
256
+    return base <= selection.extentOffset && selection.baseOffset <= extent;
257
+  }
258
+
259
+  void _paintCaret(PaintingContext context, Offset offset) {
260
+    final pos = _selection.extentOffset - node.documentOffset;
261
+    Offset caretOffset = Offset.zero;
262
+    if (pos == 1) {
263
+      caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0);
264
+    }
265
+    _caretPainter.paint(context.canvas, caretOffset + offset);
266
+  }
267
+
268
+//
269
+//  void _paintSelection(PaintingContext context, Offset offset) {
270
+//    assert(_isSelectionVisible);
271
+//    // TODO: this could be improved by painting additional box for line-break characters.
272
+//    _selectionRects ??= getBoxesForSelection(getLocalSelection(_selection));
273
+//    final Paint paint = new Paint()..color = _selectionColor;
274
+//    for (ui.TextBox box in _selectionRects)
275
+//      context.canvas.drawRect(box.toRect().shift(offset), paint);
276
+//  }
277
+}

+ 123
- 0
packages/zefyr/lib/src/widgets/input.dart Целия файл

@@ -0,0 +1,123 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/services.dart';
5
+import 'package:flutter/widgets.dart';
6
+import 'package:zefyr/util.dart';
7
+
8
+typedef RemoteValueChanged = Function(
9
+    int start, String deleted, String inserted, TextSelection selection);
10
+
11
+class InputConnectionController implements TextInputClient {
12
+  InputConnectionController(this.onValueChanged)
13
+      : assert(onValueChanged != null);
14
+
15
+  //
16
+  // New public members
17
+  //
18
+
19
+  final RemoteValueChanged onValueChanged;
20
+
21
+  /// Returns `true` if there is open input connection.
22
+  bool get hasConnection =>
23
+      _textInputConnection != null && _textInputConnection.attached;
24
+
25
+  /// Opens or closes input connection based on the current state of
26
+  /// [focusNode] and [value].
27
+  void openOrCloseConnection(FocusNode focusNode, TextEditingValue value) {
28
+    if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) {
29
+      openConnection(value);
30
+    } else if (!focusNode.hasFocus) {
31
+      closeConnection();
32
+    }
33
+  }
34
+
35
+  void openConnection(TextEditingValue value) {
36
+    if (!hasConnection) {
37
+      _lastKnownRemoteTextEditingValue = value;
38
+      _textInputConnection = TextInput.attach(
39
+        this,
40
+        new TextInputConfiguration(
41
+          inputType: TextInputType.multiline,
42
+          obscureText: false,
43
+          autocorrect: true,
44
+          inputAction: TextInputAction.newline,
45
+        ),
46
+      )..setEditingState(value);
47
+      _sentRemoteValues.add(value);
48
+    }
49
+    _textInputConnection.show();
50
+  }
51
+
52
+  /// Closes input connection if it's currently open. Otherwise does nothing.
53
+  void closeConnection() {
54
+    if (hasConnection) {
55
+      _textInputConnection.close();
56
+      _textInputConnection = null;
57
+      _lastKnownRemoteTextEditingValue = null;
58
+      _sentRemoteValues.clear();
59
+    }
60
+  }
61
+
62
+  /// Updates remote value based on current state of [document] and
63
+  /// [selection].
64
+  ///
65
+  /// This method may not actually send an update to native side if it thinks
66
+  /// remote value is up to date or identical.
67
+  void updateRemoteValue(TextEditingValue value) {
68
+    if (!hasConnection) return;
69
+
70
+    if (value == _lastKnownRemoteTextEditingValue) return;
71
+    bool shouldRemember = value.text != _lastKnownRemoteTextEditingValue.text;
72
+    _lastKnownRemoteTextEditingValue = value;
73
+    _textInputConnection.setEditingState(value);
74
+    // Only keep track if text changed (selection changes are not relevant)
75
+    if (shouldRemember) {
76
+      _sentRemoteValues.add(value);
77
+    }
78
+  }
79
+
80
+  //
81
+  // Overridden members
82
+  //
83
+
84
+  @override
85
+  void performAction(TextInputAction action) {
86
+    // no-op
87
+  }
88
+
89
+  @override
90
+  void updateEditingValue(TextEditingValue value) {
91
+    if (_sentRemoteValues.contains(value)) {
92
+      /// There is a race condition in Flutter text input plugin where sending
93
+      /// updates to native side too often results in broken behavior.
94
+      /// TextInputConnection.setEditingValue is an async call to native side.
95
+      /// For each such call native side _always_ sends update which triggers
96
+      /// this method (updateEditingValue) with the same value we've sent it.
97
+      /// If multiple calls to setEditingValue happen too fast and we only
98
+      /// track the last sent value then there is no way for us to filter out
99
+      /// automatic callbacks from native side.
100
+      /// Therefore we have to keep track of all values we send to the native
101
+      /// side and when we see this same value appear here we skip it.
102
+      /// This is fragile but it's probably the only available option.
103
+      _sentRemoteValues.remove(value);
104
+      return;
105
+    }
106
+
107
+    final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue;
108
+    _lastKnownRemoteTextEditingValue = value;
109
+    final oldText = effectiveLastKnownValue.text;
110
+    final text = value.text;
111
+    final cursorPosition = value.selection.extentOffset;
112
+    final diff = fastDiff(oldText, text, cursorPosition);
113
+    onValueChanged(diff.start, diff.deleted, diff.inserted, value.selection);
114
+  }
115
+
116
+  //
117
+  // Private members
118
+  //
119
+
120
+  final List<TextEditingValue> _sentRemoteValues = [];
121
+  TextInputConnection _textInputConnection;
122
+  TextEditingValue _lastKnownRemoteTextEditingValue;
123
+}

+ 86
- 0
packages/zefyr/lib/src/widgets/list.dart Целия файл

@@ -0,0 +1,86 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:notus/notus.dart';
6
+
7
+import 'common.dart';
8
+import 'paragraph.dart';
9
+import 'theme.dart';
10
+
11
+/// Represents number lists and bullet lists in a Zefyr editor.
12
+class ZefyrList extends StatelessWidget {
13
+  const ZefyrList({Key key, @required this.node}) : super(key: key);
14
+
15
+  final BlockNode node;
16
+
17
+  @override
18
+  Widget build(BuildContext context) {
19
+    final theme = ZefyrTheme.of(context);
20
+    List<Widget> items = [];
21
+    int index = 1;
22
+    for (var line in node.children) {
23
+      items.add(_buildItem(line, index));
24
+      index++;
25
+    }
26
+
27
+    final isNumberList =
28
+        node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList;
29
+    EdgeInsets padding = isNumberList
30
+        ? theme.blockTheme.numberList.padding
31
+        : theme.blockTheme.bulletList.padding;
32
+    padding = padding.copyWith(left: theme.indentSize);
33
+
34
+    return new Padding(
35
+      padding: padding,
36
+      child: new Column(children: items),
37
+    );
38
+  }
39
+
40
+  Widget _buildItem(Node node, int index) {
41
+    LineNode line = node;
42
+    return new ZefyrListItem(index: index, node: line);
43
+  }
44
+}
45
+
46
+/// An item in a [ZefyrList].
47
+class ZefyrListItem extends StatelessWidget {
48
+  ZefyrListItem({Key key, this.index, this.node}) : super(key: key);
49
+
50
+  final int index;
51
+  final LineNode node;
52
+
53
+  @override
54
+  Widget build(BuildContext context) {
55
+    final BlockNode block = node.parent;
56
+    final style = block.style.get(NotusAttribute.block);
57
+    final theme = ZefyrTheme.of(context);
58
+    final bulletText =
59
+        (style == NotusAttribute.block.bulletList) ? '•' : '$index.';
60
+
61
+    TextStyle textStyle;
62
+    Widget content;
63
+    EdgeInsets padding;
64
+
65
+    if (node.style.contains(NotusAttribute.heading)) {
66
+      final headingTheme = ZefyrHeading.themeOf(node, context);
67
+      textStyle = headingTheme.textStyle;
68
+      padding = headingTheme.padding;
69
+      content = new ZefyrHeading(node: node);
70
+    } else {
71
+      textStyle = theme.paragraphTheme.textStyle;
72
+      content = new RawZefyrLine(node: node, style: textStyle);
73
+    }
74
+
75
+    Widget bullet =
76
+        SizedBox(width: 24.0, child: Text(bulletText, style: textStyle));
77
+    if (padding != null) {
78
+      bullet = Padding(padding: padding, child: bullet);
79
+    }
80
+
81
+    return Row(
82
+      crossAxisAlignment: CrossAxisAlignment.start,
83
+      children: <Widget>[bullet, Expanded(child: content)],
84
+    );
85
+  }
86
+}

+ 68
- 0
packages/zefyr/lib/src/widgets/paragraph.dart Целия файл

@@ -0,0 +1,68 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:notus/notus.dart';
6
+
7
+import 'common.dart';
8
+import 'theme.dart';
9
+
10
+/// Represents regular paragraph line in a Zefyr editor.
11
+class ZefyrParagraph extends StatelessWidget {
12
+  ZefyrParagraph({Key key, @required this.node, this.blockStyle})
13
+      : super(key: key);
14
+
15
+  final LineNode node;
16
+  final TextStyle blockStyle;
17
+
18
+  @override
19
+  Widget build(BuildContext context) {
20
+    final theme = ZefyrTheme.of(context);
21
+    TextStyle style = theme.paragraphTheme.textStyle;
22
+    if (blockStyle != null) {
23
+      style = style.merge(blockStyle);
24
+    }
25
+    return new RawZefyrLine(
26
+      node: node,
27
+      style: style,
28
+      padding: theme.paragraphTheme.padding,
29
+    );
30
+  }
31
+}
32
+
33
+/// Represents heading-styled line in [ZefyrEditor].
34
+class ZefyrHeading extends StatelessWidget {
35
+  ZefyrHeading({Key key, @required this.node, this.blockStyle})
36
+      : assert(node.style.contains(NotusAttribute.heading)),
37
+        super(key: key);
38
+
39
+  final LineNode node;
40
+  final TextStyle blockStyle;
41
+
42
+  @override
43
+  Widget build(BuildContext context) {
44
+    final theme = themeOf(node, context);
45
+    TextStyle style = theme.textStyle;
46
+    if (blockStyle != null) {
47
+      style = style.merge(blockStyle);
48
+    }
49
+    return new RawZefyrLine(
50
+      node: node,
51
+      style: style,
52
+      padding: theme.padding,
53
+    );
54
+  }
55
+
56
+  static StyleTheme themeOf(LineNode node, BuildContext context) {
57
+    final theme = ZefyrTheme.of(context);
58
+    final style = node.style.get(NotusAttribute.heading);
59
+    if (style == NotusAttribute.heading.level1) {
60
+      return theme.headingTheme.level1;
61
+    } else if (style == NotusAttribute.heading.level2) {
62
+      return theme.headingTheme.level2;
63
+    } else if (style == NotusAttribute.heading.level3) {
64
+      return theme.headingTheme.level3;
65
+    }
66
+    throw new UnimplementedError('Unsupported heading style $style');
67
+  }
68
+}

+ 55
- 0
packages/zefyr/lib/src/widgets/quote.dart Целия файл

@@ -0,0 +1,55 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:notus/notus.dart';
6
+
7
+import 'paragraph.dart';
8
+import 'theme.dart';
9
+
10
+/// Represents a quote block in a Zefyr editor.
11
+class ZefyrQuote extends StatelessWidget {
12
+  const ZefyrQuote({Key key, @required this.node}) : super(key: key);
13
+
14
+  final BlockNode node;
15
+
16
+  @override
17
+  Widget build(BuildContext context) {
18
+    final theme = ZefyrTheme.of(context);
19
+    final style = theme.blockTheme.quote.textStyle;
20
+    List<Widget> items = [];
21
+    for (var line in node.children) {
22
+      items.add(_buildLine(line, style, theme.indentSize));
23
+    }
24
+
25
+    return Padding(
26
+      padding: theme.blockTheme.quote.padding,
27
+      child: Column(
28
+        crossAxisAlignment: CrossAxisAlignment.stretch,
29
+        children: items,
30
+      ),
31
+    );
32
+  }
33
+
34
+  Widget _buildLine(Node node, TextStyle blockStyle, double indentSize) {
35
+    LineNode line = node;
36
+
37
+    Widget content;
38
+    if (line.style.contains(NotusAttribute.heading)) {
39
+      content = new ZefyrHeading(node: node, blockStyle: blockStyle);
40
+    } else {
41
+      content = new ZefyrParagraph(node: node, blockStyle: blockStyle);
42
+    }
43
+
44
+    final row = Row(children: <Widget>[Expanded(child: content)]);
45
+    return Container(
46
+      decoration: BoxDecoration(
47
+        border: Border(
48
+          left: BorderSide(width: 4.0, color: Colors.grey.shade300),
49
+        ),
50
+      ),
51
+      padding: EdgeInsets.only(left: indentSize),
52
+      child: row,
53
+    );
54
+  }
55
+}

+ 116
- 0
packages/zefyr/lib/src/widgets/render_context.dart Целия файл

@@ -0,0 +1,116 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/foundation.dart';
5
+import 'package:flutter/widgets.dart';
6
+
7
+import 'editable_box.dart';
8
+
9
+/// Registry of all [RenderEditableBox]es inside a [ZefyrEditableText].
10
+///
11
+/// Provides access to all currently active [RenderEditableBox]
12
+/// instances of a [ZefyrEditableText].
13
+///
14
+/// Use [boxForTextOffset] or [boxForGlobalPoint] to retrieve a
15
+/// specific box.
16
+///
17
+/// The [addBox], [removeBox] and [markDirty] are intended to be
18
+/// only used by [RenderEditableBox] objects to register with a rendering
19
+/// context.
20
+///
21
+/// ### Life cycle details
22
+///
23
+/// When a box object is attached to rendering pipeline it registers
24
+/// itself with a render scope by calling [addBox]. At this point the context
25
+/// treats this object as "dirty" and query methods like [boxForTextOffset]
26
+/// still exclude this object from returned results.
27
+///
28
+/// When this box considers itself initialized it calls [markDirty] with
29
+/// `isDirty` set to `false` which activates it. At this point query methods
30
+/// include this object in results.
31
+///
32
+/// When a box is rebuilt it may deactivate itself by calling [markDirty]
33
+/// again.
34
+///
35
+/// When a box is detached from rendering pipeline it unregisters
36
+/// itself by calling [removeBox].
37
+class ZefyrRenderContext extends ChangeNotifier {
38
+  final Set<RenderEditableBox> _dirtyBoxes = new Set();
39
+  final Set<RenderEditableBox> _activeBoxes = new Set();
40
+
41
+  Set<RenderEditableBox> get dirty => _dirtyBoxes;
42
+  Set<RenderEditableBox> get active => _activeBoxes;
43
+
44
+  bool _disposed = false;
45
+
46
+  /// Adds [box] to this context. The box is considered "dirty" at
47
+  /// this point and is not included in query results of `boxFor*`
48
+  /// methods.
49
+  void addBox(RenderEditableBox box) {
50
+    assert(!_disposed);
51
+    _dirtyBoxes.add(box);
52
+  }
53
+
54
+  /// Removes [box] from this render context.
55
+  void removeBox(RenderEditableBox box) {
56
+    assert(!_disposed);
57
+    _dirtyBoxes.remove(box);
58
+    _activeBoxes.remove(box);
59
+    notifyListeners();
60
+  }
61
+
62
+  void markDirty(RenderEditableBox box, bool isDirty) {
63
+    assert(!_disposed);
64
+
65
+    var collection = isDirty ? _dirtyBoxes : _activeBoxes;
66
+    if (collection.contains(box)) return;
67
+
68
+    if (isDirty) {
69
+      _activeBoxes.remove(box);
70
+      _dirtyBoxes.add(box);
71
+    } else {
72
+      _dirtyBoxes.remove(box);
73
+      _activeBoxes.add(box);
74
+    }
75
+    notifyListeners();
76
+  }
77
+
78
+  /// Returns box containing character at specified document [offset].
79
+  RenderEditableBox boxForTextOffset(int offset) {
80
+    assert(!_disposed);
81
+    return _activeBoxes.firstWhere(
82
+      (p) => p.node.containsOffset(offset),
83
+      orElse: _null,
84
+    );
85
+  }
86
+
87
+  /// Returns box located at specified global [point] on the screen or
88
+  /// `null`.
89
+  RenderEditableBox boxForGlobalPoint(Offset point) {
90
+    assert(!_disposed);
91
+    return _activeBoxes.firstWhere((p) {
92
+      final localPoint = p.globalToLocal(point);
93
+      return p.size.contains(localPoint);
94
+    }, orElse: _null);
95
+  }
96
+
97
+  static Null _null() => null;
98
+
99
+  @override
100
+  void dispose() {
101
+    _disposed = true;
102
+    _activeBoxes.clear();
103
+    _dirtyBoxes.clear();
104
+    super.dispose();
105
+  }
106
+
107
+  @override
108
+  void notifyListeners() {
109
+    /// Ensures listeners are not notified during rendering phase where they
110
+    /// cannot react by updating their state or rebuilding.
111
+    WidgetsBinding.instance.addPostFrameCallback((_) {
112
+      if (_disposed) return;
113
+      super.notifyListeners();
114
+    });
115
+  }
116
+}

+ 453
- 0
packages/zefyr/lib/src/widgets/selection.dart Целия файл

@@ -0,0 +1,453 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui' as ui;
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter/rendering.dart';
8
+import 'package:notus/notus.dart';
9
+import 'package:zefyr/util.dart';
10
+
11
+import 'controller.dart';
12
+import 'editable_box.dart';
13
+import 'editable_text.dart';
14
+import 'editor.dart';
15
+
16
+RenderEditableBox _getRenderParagraph(HitTestResult result) {
17
+  for (var entry in result.path) {
18
+    if (entry.target is RenderEditableBox) {
19
+      return entry.target;
20
+    }
21
+  }
22
+  return null;
23
+}
24
+
25
+/// Selection overlay controls selection handles and other gestures.
26
+class ZefyrSelectionOverlay extends StatefulWidget {
27
+  const ZefyrSelectionOverlay({
28
+    Key key,
29
+    @required this.controller,
30
+    @required this.controls,
31
+    @required this.overlay,
32
+  }) : super(key: key);
33
+
34
+  final ZefyrController controller;
35
+  final TextSelectionControls controls;
36
+  final OverlayState overlay;
37
+
38
+  @override
39
+  _ZefyrSelectionOverlayState createState() =>
40
+      new _ZefyrSelectionOverlayState();
41
+}
42
+
43
+class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
44
+    implements TextSelectionDelegate {
45
+  @override
46
+  TextEditingValue get textEditingValue =>
47
+      widget.controller.plainTextEditingValue;
48
+
49
+  set textEditingValue(TextEditingValue value) {
50
+    final cursorPosition = value.selection.extentOffset;
51
+    final oldText = widget.controller.document.toPlainText();
52
+    final newText = value.text;
53
+    final diff = fastDiff(oldText, newText, cursorPosition);
54
+    widget.controller.replaceText(
55
+        diff.start, diff.deleted.length, diff.inserted,
56
+        selection: value.selection);
57
+  }
58
+
59
+  @override
60
+  void bringIntoView(ui.TextPosition position) {
61
+    // TODO: implement bringIntoView
62
+  }
63
+
64
+  bool get isToolbarVisible => _toolbar != null;
65
+  bool get isToolbarHidden => _toolbar == null;
66
+
67
+  @override
68
+  void hideToolbar() {
69
+    _toolbar?.remove();
70
+    _toolbar = null;
71
+    _toolbarController.stop();
72
+  }
73
+
74
+  void showToolbar() {
75
+    final editable = ZefyrEditableText.of(context);
76
+    assert(editable != null);
77
+    final toolbarOpacity = _toolbarController.view;
78
+    _toolbar = new OverlayEntry(
79
+      builder: (context) => new FadeTransition(
80
+            opacity: toolbarOpacity,
81
+            child: new _SelectionToolbar(
82
+              editable: editable,
83
+              controls: widget.controls,
84
+              delegate: this,
85
+              visible: true,
86
+            ),
87
+          ),
88
+    );
89
+    widget.overlay.insert(_toolbar);
90
+    _toolbarController.forward(from: 0.0);
91
+  }
92
+
93
+  //
94
+  // Overridden members of State
95
+  //
96
+
97
+  @override
98
+  void initState() {
99
+    super.initState();
100
+    _toolbarController = new AnimationController(
101
+        duration: _kFadeDuration, vsync: widget.overlay);
102
+  }
103
+
104
+  static const Duration _kFadeDuration = const Duration(milliseconds: 150);
105
+
106
+  @override
107
+  void didUpdateWidget(ZefyrSelectionOverlay oldWidget) {
108
+    super.didUpdateWidget(oldWidget);
109
+    if (oldWidget.overlay != widget.overlay) {
110
+      hideToolbar();
111
+      _toolbarController.dispose();
112
+      _toolbarController = new AnimationController(
113
+          duration: _kFadeDuration, vsync: widget.overlay);
114
+    }
115
+  }
116
+
117
+  @override
118
+  void didChangeDependencies() {
119
+    super.didChangeDependencies();
120
+    WidgetsBinding.instance.addPostFrameCallback((_) {
121
+      _updateToolbar();
122
+    });
123
+  }
124
+
125
+  @override
126
+  void dispose() {
127
+    hideToolbar();
128
+    _toolbarController.dispose();
129
+    _toolbarController = null;
130
+    super.dispose();
131
+  }
132
+
133
+  @override
134
+  Widget build(BuildContext context) {
135
+    final overlay = new GestureDetector(
136
+      behavior: HitTestBehavior.translucent,
137
+      onTapDown: _handleTapDown,
138
+      onTap: _handleTap,
139
+      onTapCancel: _handleTapCancel,
140
+      onLongPress: _handleLongPress,
141
+      child: new Stack(
142
+        fit: StackFit.expand,
143
+        children: <Widget>[
144
+          new SelectionHandleDriver(
145
+            position: _SelectionHandlePosition.base,
146
+            controls: widget.controls,
147
+          ),
148
+          new SelectionHandleDriver(
149
+            position: _SelectionHandlePosition.extent,
150
+            controls: widget.controls,
151
+          ),
152
+        ],
153
+      ),
154
+    );
155
+    return new Container(child: overlay);
156
+  }
157
+
158
+  //
159
+  // Private members
160
+  //
161
+
162
+  /// Global position of last TapDown event.
163
+  Offset _lastTapDownPosition;
164
+
165
+  /// Global position of last TapDown which is potentially a long press.
166
+  Offset _longPressPosition;
167
+
168
+  OverlayEntry _toolbar;
169
+  AnimationController _toolbarController;
170
+
171
+  TextSelection _selection;
172
+
173
+  bool _didCaretTap = false;
174
+
175
+  void _updateToolbar() {
176
+    final editor = ZefyrEditor.of(context);
177
+    final selection = editor.selection;
178
+    final focusOwner = editor.focusOwner;
179
+    setState(() {
180
+      if (focusOwner != FocusOwner.editor) {
181
+        hideToolbar();
182
+      } else {
183
+        if (_selection != selection) {
184
+          if (selection.isCollapsed && isToolbarVisible) hideToolbar();
185
+          _toolbar?.markNeedsBuild();
186
+          if (!selection.isCollapsed && isToolbarHidden) showToolbar();
187
+        } else {
188
+          if (!selection.isCollapsed && isToolbarHidden) {
189
+            showToolbar();
190
+          } else if (isToolbarVisible) {
191
+            _toolbar?.markNeedsBuild();
192
+          }
193
+        }
194
+      }
195
+      _selection = selection;
196
+    });
197
+  }
198
+
199
+  void _handleTapDown(TapDownDetails details) {
200
+    _lastTapDownPosition = details.globalPosition;
201
+  }
202
+
203
+  void _handleTapCancel() {
204
+    // longPress arrives after tapCancel, so remember the tap position.
205
+    _longPressPosition = _lastTapDownPosition;
206
+    _lastTapDownPosition = null;
207
+  }
208
+
209
+  void _handleTap() {
210
+    assert(_lastTapDownPosition != null);
211
+    final globalPoint = _lastTapDownPosition;
212
+    _lastTapDownPosition = null;
213
+    HitTestResult result = new HitTestResult();
214
+    WidgetsBinding.instance.hitTest(result, globalPoint);
215
+
216
+    final paragraph = _getRenderParagraph(result);
217
+    if (paragraph == null) return;
218
+
219
+    final localPoint = paragraph.globalToLocal(globalPoint);
220
+    final position = paragraph.getPositionForOffset(localPoint);
221
+    final selection = new TextSelection.collapsed(
222
+        offset: paragraph.node.documentOffset + position.offset);
223
+    if (_didCaretTap && _selection == selection) {
224
+      _didCaretTap = false;
225
+      hideToolbar();
226
+      showToolbar();
227
+    } else {
228
+      _didCaretTap = true;
229
+    }
230
+    widget.controller.updateSelection(selection, source: ChangeSource.local);
231
+  }
232
+
233
+  void _handleLongPress() {
234
+    final Offset globalPoint = _longPressPosition;
235
+    _longPressPosition = null;
236
+    HitTestResult result = new HitTestResult();
237
+    WidgetsBinding.instance.hitTest(result, globalPoint);
238
+    final paragraph = _getRenderParagraph(result);
239
+    if (paragraph == null) {
240
+      return;
241
+    }
242
+    final localPoint = paragraph.globalToLocal(globalPoint);
243
+    final position = paragraph.getPositionForOffset(localPoint);
244
+    final word = paragraph.getWordBoundary(position);
245
+    final selection = new TextSelection(
246
+      baseOffset: paragraph.node.documentOffset + word.start,
247
+      extentOffset: paragraph.node.documentOffset + word.end,
248
+    );
249
+    widget.controller.updateSelection(selection, source: ChangeSource.local);
250
+  }
251
+}
252
+
253
+enum _SelectionHandlePosition { base, extent }
254
+
255
+class SelectionHandleDriver extends StatefulWidget {
256
+  const SelectionHandleDriver({
257
+    Key key,
258
+    @required this.position,
259
+    @required this.controls,
260
+  }) : super(key: key);
261
+
262
+  final _SelectionHandlePosition position;
263
+  final TextSelectionControls controls;
264
+
265
+  @override
266
+  _SelectionHandleDriverState createState() =>
267
+      new _SelectionHandleDriverState();
268
+}
269
+
270
+class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
271
+  /// Current document selection.
272
+  TextSelection get selection => _selection;
273
+  TextSelection _selection;
274
+
275
+  /// Returns `true` if this handle is located at the baseOffset of selection.
276
+  bool get isBaseHandle => widget.position == _SelectionHandlePosition.base;
277
+
278
+  /// Character offset of this handle in the document.
279
+  ///
280
+  /// For base handle this equals to [TextSelection.baseOffset] and for
281
+  /// extent handle - [TextSelection.extentOffset].
282
+  int get documentOffset =>
283
+      isBaseHandle ? selection.baseOffset : selection.extentOffset;
284
+
285
+  /// Position in pixels of this selection handle within its paragraph [block].
286
+  Offset getPosition(RenderEditableBox block) {
287
+    if (block == null) return null;
288
+
289
+    final localSelection = block.getLocalSelection(selection);
290
+    assert(localSelection != null);
291
+
292
+    final boxes = block.getEndpointsForSelection(selection);
293
+    if (boxes.isEmpty) {
294
+      print('Got empty boxes for selection ${selection}');
295
+      return null;
296
+    }
297
+    final box = isBaseHandle ? boxes.first : boxes.last;
298
+    final dx = isBaseHandle ? box.start : box.end;
299
+    return new Offset(dx, box.bottom);
300
+  }
301
+
302
+  @override
303
+  void didChangeDependencies() {
304
+    super.didChangeDependencies();
305
+    final editable = ZefyrEditableText.of(context);
306
+    _selection = editable.selection;
307
+  }
308
+
309
+  //
310
+  // Overridden members
311
+  //
312
+
313
+  @override
314
+  Widget build(BuildContext context) {
315
+    final editor = ZefyrEditor.of(context);
316
+    final editable = ZefyrEditableText.of(context);
317
+    if (selection == null ||
318
+        selection.isCollapsed ||
319
+        widget.controls == null ||
320
+        editor.focusOwner != FocusOwner.editor) {
321
+      return new Container();
322
+    }
323
+    final block = editable.renderContext.boxForTextOffset(documentOffset);
324
+    final position = getPosition(block);
325
+    Widget handle;
326
+    if (position == null) {
327
+      handle = new Container();
328
+    } else {
329
+      final handleType = isBaseHandle
330
+          ? TextSelectionHandleType.left
331
+          : TextSelectionHandleType.right;
332
+      handle = new Positioned(
333
+        left: position.dx,
334
+        top: position.dy,
335
+        child: widget.controls.buildHandle(
336
+          context,
337
+          handleType,
338
+          block.preferredLineHeight,
339
+        ),
340
+      );
341
+      handle = new CompositedTransformFollower(
342
+        link: block.layerLink,
343
+        showWhenUnlinked: false,
344
+        child: new Stack(children: <Widget>[handle]),
345
+      );
346
+    }
347
+    // Always return this gesture detector even if handle is an empty container
348
+    // This way we prevent drag gesture from being canceled in case current
349
+    // position is somewhere outside of any visible paragraph block.
350
+    return new GestureDetector(
351
+      onPanStart: _handleDragStart,
352
+      onPanUpdate: _handleDragUpdate,
353
+      child: handle,
354
+    );
355
+  }
356
+
357
+  //
358
+  // Private members
359
+  //
360
+
361
+  Offset _dragPosition;
362
+
363
+  void _handleDragStart(DragStartDetails details) {
364
+    _dragPosition = details.globalPosition;
365
+  }
366
+
367
+  void _handleDragUpdate(DragUpdateDetails details) {
368
+    _dragPosition += details.delta;
369
+    final globalPoint = _dragPosition;
370
+    final editor = ZefyrEditor.of(context);
371
+    final editable = ZefyrEditableText.of(context);
372
+    final paragraph =
373
+        editable.renderContext.boxForGlobalPoint(globalPoint);
374
+    if (paragraph == null) {
375
+      return;
376
+    }
377
+
378
+    final localPoint = paragraph.globalToLocal(globalPoint);
379
+    final position = paragraph.getPositionForOffset(localPoint);
380
+    final documentOffset = paragraph.node.documentOffset + position.offset;
381
+    final newSelection = selection.copyWith(
382
+      baseOffset: isBaseHandle ? documentOffset : selection.baseOffset,
383
+      extentOffset: isBaseHandle ? selection.extentOffset : documentOffset,
384
+    );
385
+    if (newSelection.baseOffset >= newSelection.extentOffset) {
386
+      // Don't allow reversed or collapsed selection.
387
+      return;
388
+    }
389
+
390
+    if (newSelection != _selection) {
391
+      editor.updateSelection(newSelection, source: ChangeSource.local);
392
+    }
393
+  }
394
+}
395
+
396
+class _SelectionToolbar extends StatefulWidget {
397
+  const _SelectionToolbar({
398
+    Key key,
399
+    @required this.editable,
400
+    @required this.controls,
401
+    @required this.delegate,
402
+    @required this.visible,
403
+  }) : super(key: key);
404
+
405
+  final ZefyrEditableTextScope editable;
406
+  final TextSelectionControls controls;
407
+  final TextSelectionDelegate delegate;
408
+  final bool visible;
409
+
410
+  @override
411
+  _SelectionToolbarState createState() => new _SelectionToolbarState();
412
+}
413
+
414
+class _SelectionToolbarState extends State<_SelectionToolbar> {
415
+  ZefyrEditableTextScope get editable => widget.editable;
416
+
417
+  @override
418
+  Widget build(BuildContext context) {
419
+    return _buildToolbar(context);
420
+  }
421
+
422
+  Widget _buildToolbar(BuildContext context) {
423
+    final base = editable.selection.baseOffset;
424
+    final block = editable.renderContext.boxForTextOffset(base);
425
+    if (block == null) {
426
+      return Container();
427
+    }
428
+
429
+    final boxes = block.getEndpointsForSelection(editable.selection);
430
+
431
+    // Find the horizontal midpoint, just above the selected text.
432
+    final Offset midpoint = new Offset(
433
+      (boxes.length == 1)
434
+          ? (boxes[0].start + boxes[0].end) / 2.0
435
+          : (boxes[0].start + boxes[1].start) / 2.0,
436
+      boxes[0].bottom - block.preferredLineHeight,
437
+    );
438
+
439
+    final Rect editingRegion = new Rect.fromPoints(
440
+      block.localToGlobal(Offset.zero),
441
+      block.localToGlobal(block.size.bottomRight(Offset.zero)),
442
+    );
443
+
444
+    final toolbar = widget.controls
445
+        .buildToolbar(context, editingRegion, midpoint, widget.delegate);
446
+    return new CompositedTransformFollower(
447
+      link: block.layerLink,
448
+      showWhenUnlinked: false,
449
+      offset: -editingRegion.topLeft,
450
+      child: toolbar,
451
+    );
452
+  }
453
+}

+ 306
- 0
packages/zefyr/lib/src/widgets/theme.dart Целия файл

@@ -0,0 +1,306 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:io';
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter/widgets.dart';
8
+import 'package:meta/meta.dart';
9
+
10
+/// Applies a Zefyr editor theme to descendant widgets.
11
+///
12
+/// Describes colors and typographic styles for an editor.
13
+///
14
+/// Descendant widgets obtain the current theme's [ZefyrThemeData] object using
15
+/// [ZefyrTheme.of].
16
+///
17
+/// See also:
18
+///
19
+///   * [ZefyrThemeData], which describes actual configuration of a theme.
20
+class ZefyrTheme extends InheritedWidget {
21
+  final ZefyrThemeData data;
22
+
23
+  /// Applies the given theme [data] to [child].
24
+  ///
25
+  /// The [data] and [child] arguments must not be null.
26
+  ZefyrTheme({
27
+    Key key,
28
+    @required this.data,
29
+    @required Widget child,
30
+  })  : assert(data != null),
31
+        assert(child != null),
32
+        super(key: key, child: child);
33
+
34
+  @override
35
+  bool updateShouldNotify(ZefyrTheme oldWidget) {
36
+    return data != oldWidget.data;
37
+  }
38
+
39
+  /// The data from the closest [ZefyrTheme] instance that encloses the given
40
+  /// context.
41
+  ///
42
+  /// Returns `null` if there is no [ZefyrTheme] in the given build context
43
+  /// and [nullOk] is set to `true`. If [nullOk] is set to `false` (default)
44
+  /// then this method asserts.
45
+  static ZefyrThemeData of(BuildContext context, {bool nullOk: false}) {
46
+    final ZefyrTheme widget = context.inheritFromWidgetOfExactType(ZefyrTheme);
47
+    if (widget == null && nullOk) return null;
48
+    assert(widget != null,
49
+        '$ZefyrTheme.of() called with a context that does not contain a ZefyrEditor.');
50
+    return widget.data;
51
+  }
52
+}
53
+
54
+/// Holds colors and typography styles for [ZefyrEditor].
55
+class ZefyrThemeData {
56
+  final TextStyle boldStyle;
57
+  final TextStyle italicStyle;
58
+  final TextStyle linkStyle;
59
+  final StyleTheme paragraphTheme;
60
+  final HeadingTheme headingTheme;
61
+  final BlockTheme blockTheme;
62
+  final Color selectionColor;
63
+
64
+  /// Size of indentation for blocks.
65
+  final double indentSize;
66
+  final ZefyrToolbarTheme toolbarTheme;
67
+
68
+  factory ZefyrThemeData.fallback(BuildContext context) {
69
+    final defaultStyle = DefaultTextStyle.of(context);
70
+    final paragraphStyle = defaultStyle.style.copyWith(
71
+      fontSize: 16.0,
72
+      height: 1.25,
73
+      fontWeight: FontWeight.normal,
74
+      color: Colors.grey.shade800,
75
+    );
76
+    final padding = const EdgeInsets.only(bottom: 8.0);
77
+    final boldStyle = new TextStyle(fontWeight: FontWeight.bold);
78
+    final italicStyle = new TextStyle(fontStyle: FontStyle.italic);
79
+    final linkStyle =
80
+        TextStyle(color: Colors.blue, decoration: TextDecoration.underline);
81
+
82
+    return new ZefyrThemeData(
83
+      boldStyle: boldStyle,
84
+      italicStyle: italicStyle,
85
+      linkStyle: linkStyle,
86
+      paragraphTheme:
87
+          new StyleTheme(textStyle: paragraphStyle, padding: padding),
88
+      headingTheme: new HeadingTheme.fallback(),
89
+      blockTheme: new BlockTheme.fallback(),
90
+      selectionColor: Colors.lightBlueAccent.shade100,
91
+      indentSize: 16.0,
92
+      toolbarTheme: new ZefyrToolbarTheme.fallback(context),
93
+    );
94
+  }
95
+
96
+  const ZefyrThemeData({
97
+    this.boldStyle,
98
+    this.italicStyle,
99
+    this.linkStyle,
100
+    this.paragraphTheme,
101
+    this.headingTheme,
102
+    this.blockTheme,
103
+    this.selectionColor,
104
+    this.indentSize,
105
+    this.toolbarTheme,
106
+  });
107
+
108
+  ZefyrThemeData copyWith({
109
+    TextStyle textStyle,
110
+    TextStyle boldStyle,
111
+    TextStyle italicStyle,
112
+    TextStyle linkStyle,
113
+    StyleTheme paragraphTheme,
114
+    HeadingTheme headingTheme,
115
+    BlockTheme blockTheme,
116
+    Color selectionColor,
117
+    double indentSize,
118
+    ZefyrToolbarTheme toolbarTheme,
119
+  }) {
120
+    return new ZefyrThemeData(
121
+      boldStyle: boldStyle ?? this.boldStyle,
122
+      italicStyle: italicStyle ?? this.italicStyle,
123
+      linkStyle: linkStyle ?? this.linkStyle,
124
+      paragraphTheme: paragraphTheme ?? this.paragraphTheme,
125
+      headingTheme: headingTheme ?? this.headingTheme,
126
+      blockTheme: blockTheme ?? this.blockTheme,
127
+      selectionColor: selectionColor ?? this.selectionColor,
128
+      indentSize: indentSize ?? this.indentSize,
129
+      toolbarTheme: toolbarTheme ?? this.toolbarTheme,
130
+    );
131
+  }
132
+
133
+  ZefyrThemeData merge(ZefyrThemeData other) {
134
+    return copyWith(
135
+      boldStyle: other.boldStyle,
136
+      italicStyle: other.italicStyle,
137
+      linkStyle: other.linkStyle,
138
+      paragraphTheme: other.paragraphTheme,
139
+      headingTheme: other.headingTheme,
140
+      blockTheme: other.blockTheme,
141
+      selectionColor: other.selectionColor,
142
+      indentSize: other.indentSize,
143
+      toolbarTheme: other.toolbarTheme,
144
+    );
145
+  }
146
+}
147
+
148
+/// Theme for heading-styled lines of text.
149
+class HeadingTheme {
150
+  /// Style theme for level 1 headings.
151
+  final StyleTheme level1;
152
+
153
+  /// Style theme for level 2 headings.
154
+  final StyleTheme level2;
155
+
156
+  /// Style theme for level 3 headings.
157
+  final StyleTheme level3;
158
+
159
+  HeadingTheme({
160
+    @required this.level1,
161
+    @required this.level2,
162
+    @required this.level3,
163
+  });
164
+
165
+  /// Creates fallback theme for headings.
166
+  factory HeadingTheme.fallback() {
167
+    return HeadingTheme(
168
+      level1: StyleTheme(
169
+        textStyle: TextStyle(
170
+          fontSize: 30.0,
171
+          color: Colors.grey.shade800,
172
+          height: 1.25,
173
+          fontWeight: FontWeight.w600,
174
+        ),
175
+        padding: EdgeInsets.only(bottom: 16.0),
176
+      ),
177
+      level2: StyleTheme(
178
+        textStyle: TextStyle(
179
+          fontSize: 24.0,
180
+          color: Colors.grey.shade800,
181
+          height: 1.25,
182
+          fontWeight: FontWeight.w600,
183
+        ),
184
+        padding: EdgeInsets.only(bottom: 8.0, top: 16.0),
185
+      ),
186
+      level3: StyleTheme(
187
+        textStyle: TextStyle(
188
+          fontSize: 20.0,
189
+          color: Colors.grey.shade800,
190
+          height: 1.25,
191
+          fontWeight: FontWeight.w600,
192
+        ),
193
+        padding: EdgeInsets.only(bottom: 8.0, top: 16.0),
194
+      ),
195
+    );
196
+  }
197
+}
198
+
199
+/// Theme for a block of lines in a document.
200
+class BlockTheme {
201
+  /// Style theme for bullet lists.
202
+  final StyleTheme bulletList;
203
+
204
+  /// Style theme for number lists.
205
+  final StyleTheme numberList;
206
+
207
+  /// Style theme for code snippets.
208
+  final StyleTheme code;
209
+
210
+  /// Style theme for quotes.
211
+  final StyleTheme quote;
212
+
213
+  BlockTheme({
214
+    @required this.bulletList,
215
+    @required this.numberList,
216
+    @required this.quote,
217
+    @required this.code,
218
+  });
219
+
220
+  /// Creates fallback theme for blocks.
221
+  factory BlockTheme.fallback() {
222
+    final padding = const EdgeInsets.only(bottom: 8.0);
223
+    return new BlockTheme(
224
+      bulletList: new StyleTheme(padding: padding),
225
+      numberList: new StyleTheme(padding: padding),
226
+      quote: new StyleTheme(
227
+        textStyle: new TextStyle(color: Colors.grey.shade700),
228
+        padding: padding,
229
+      ),
230
+      code: new StyleTheme(
231
+        textStyle: new TextStyle(
232
+          color: Colors.blueGrey.shade800,
233
+          fontFamily: Platform.isIOS ? 'Menlo' : 'Roboto Mono',
234
+          fontSize: 14.0,
235
+          height: 1.25,
236
+        ),
237
+        padding: padding,
238
+      ),
239
+    );
240
+  }
241
+}
242
+
243
+/// Theme for a specific attribute style.
244
+///
245
+/// Used in [HeadingTheme] and [BlockTheme], as well as in
246
+/// [ZefyrThemeData.paragraphTheme].
247
+class StyleTheme {
248
+  /// Text style of this theme.
249
+  final TextStyle textStyle;
250
+
251
+  /// Padding to apply around lines of text.
252
+  final EdgeInsets padding;
253
+
254
+  /// Creates a new [StyleTheme].
255
+  StyleTheme({
256
+    this.textStyle,
257
+    this.padding,
258
+  });
259
+}
260
+
261
+/// Defines styles and colors for [ZefyrToolbar].
262
+class ZefyrToolbarTheme {
263
+  /// The background color of toolbar.
264
+  final Color color;
265
+
266
+  /// Color of buttons in toggled state.
267
+  final Color toggleColor;
268
+
269
+  /// Color of button icons.
270
+  final Color iconColor;
271
+
272
+  /// Color of button icons in disabled state.
273
+  final Color disabledIconColor;
274
+
275
+  /// Creates fallback theme for editor toolbars.
276
+  factory ZefyrToolbarTheme.fallback(BuildContext context) {
277
+    final theme = Theme.of(context);
278
+    return ZefyrToolbarTheme._(
279
+      color: theme.primaryColorLight,
280
+      toggleColor: theme.primaryColor,
281
+      iconColor: theme.primaryIconTheme.color,
282
+      disabledIconColor: theme.primaryColor,
283
+    );
284
+  }
285
+
286
+  ZefyrToolbarTheme._({
287
+    @required this.color,
288
+    @required this.toggleColor,
289
+    @required this.iconColor,
290
+    @required this.disabledIconColor,
291
+  });
292
+
293
+  ZefyrToolbarTheme copyWith({
294
+    Color color,
295
+    Color toggleColor,
296
+    Color iconColor,
297
+    Color disabledIconColor,
298
+  }) {
299
+    return ZefyrToolbarTheme._(
300
+      color: color ?? this.color,
301
+      toggleColor: toggleColor ?? this.toggleColor,
302
+      iconColor: iconColor ?? this.iconColor,
303
+      disabledIconColor: disabledIconColor ?? this.disabledIconColor,
304
+    );
305
+  }
306
+}

+ 392
- 0
packages/zefyr/lib/src/widgets/toolbar.dart Целия файл

@@ -0,0 +1,392 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:async';
5
+import 'dart:ui' as ui;
6
+
7
+import 'package:flutter/material.dart';
8
+import 'package:notus/notus.dart';
9
+
10
+import 'buttons.dart';
11
+import 'controller.dart';
12
+import 'editor.dart';
13
+import 'theme.dart';
14
+
15
+/// List of all button actions supported by [ZefyrToolbar] buttons.
16
+enum ZefyrToolbarAction {
17
+  bold,
18
+  italic,
19
+  link,
20
+  unlink,
21
+  clipboardCopy,
22
+  openInBrowser,
23
+  heading,
24
+  headingLevel1,
25
+  headingLevel2,
26
+  headingLevel3,
27
+  bulletList,
28
+  numberList,
29
+  code,
30
+  quote,
31
+  hideKeyboard,
32
+  close,
33
+  confirm,
34
+}
35
+
36
+final kZefyrToolbarAttributeActions = <ZefyrToolbarAction, NotusAttributeKey>{
37
+  ZefyrToolbarAction.bold: NotusAttribute.bold,
38
+  ZefyrToolbarAction.italic: NotusAttribute.italic,
39
+  ZefyrToolbarAction.link: NotusAttribute.link,
40
+  ZefyrToolbarAction.heading: NotusAttribute.heading,
41
+  ZefyrToolbarAction.headingLevel1: NotusAttribute.heading.level1,
42
+  ZefyrToolbarAction.headingLevel2: NotusAttribute.heading.level2,
43
+  ZefyrToolbarAction.headingLevel3: NotusAttribute.heading.level3,
44
+  ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList,
45
+  ZefyrToolbarAction.numberList: NotusAttribute.block.numberList,
46
+  ZefyrToolbarAction.code: NotusAttribute.block.code,
47
+  ZefyrToolbarAction.quote: NotusAttribute.block.quote
48
+};
49
+
50
+/// Allows customizing appearance of [ZefyrToolbar].
51
+abstract class ZefyrToolbarDelegate {
52
+  /// Builds toolbar button for specified [action].
53
+  ///
54
+  /// Returned widget is usually an instance of [ZefyrButton].
55
+  Widget buildButton(BuildContext context, ZefyrToolbarAction action,
56
+      {VoidCallback onPressed});
57
+}
58
+
59
+/// Scaffold for [ZefyrToolbar].
60
+class ZefyrToolbarScaffold extends StatelessWidget {
61
+  const ZefyrToolbarScaffold({
62
+    Key key,
63
+    @required this.body,
64
+    this.trailing,
65
+    this.autoImplyTrailing: true,
66
+  }) : super(key: key);
67
+
68
+  final Widget body;
69
+  final Widget trailing;
70
+  final bool autoImplyTrailing;
71
+
72
+  @override
73
+  Widget build(BuildContext context) {
74
+    final theme = ZefyrTheme.of(context).toolbarTheme;
75
+    final toolbar = ZefyrToolbar.of(context);
76
+    final constraints =
77
+        BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
78
+    final children = <Widget>[
79
+      Expanded(child: body),
80
+    ];
81
+
82
+    if (trailing != null) {
83
+      children.add(trailing);
84
+    } else if (autoImplyTrailing) {
85
+      children.add(toolbar.buildButton(context, ZefyrToolbarAction.close));
86
+    }
87
+    return new Container(
88
+      constraints: constraints,
89
+      child: Material(color: theme.color, child: Row(children: children)),
90
+    );
91
+  }
92
+}
93
+
94
+/// Toolbar for [ZefyrEditor].
95
+class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget {
96
+  static const kToolbarHeight = 50.0;
97
+
98
+  const ZefyrToolbar({
99
+    Key key,
100
+    @required this.focusNode,
101
+    @required this.controller,
102
+    this.autoHide: true,
103
+    this.delegate,
104
+  }) : super(key: key);
105
+
106
+  final FocusNode focusNode;
107
+  final ZefyrController controller;
108
+  final ZefyrToolbarDelegate delegate;
109
+
110
+  /// Whether to automatically hide this toolbar when editor loses focus.
111
+  final bool autoHide;
112
+
113
+  static ZefyrToolbarState of(BuildContext context) {
114
+    final _ZefyrToolbarScope scope =
115
+        context.inheritFromWidgetOfExactType(_ZefyrToolbarScope);
116
+    return scope?.toolbar;
117
+  }
118
+
119
+  @override
120
+  ZefyrToolbarState createState() => ZefyrToolbarState();
121
+
122
+  @override
123
+  ui.Size get preferredSize => new Size.fromHeight(ZefyrToolbar.kToolbarHeight);
124
+}
125
+
126
+class _ZefyrToolbarScope extends InheritedWidget {
127
+  _ZefyrToolbarScope({Key key, @required Widget child, @required this.toolbar})
128
+      : super(key: key, child: child);
129
+
130
+  final ZefyrToolbarState toolbar;
131
+
132
+  @override
133
+  bool updateShouldNotify(_ZefyrToolbarScope oldWidget) {
134
+    return toolbar != oldWidget.toolbar;
135
+  }
136
+}
137
+
138
+class ZefyrToolbarState extends State<ZefyrToolbar>
139
+    with SingleTickerProviderStateMixin {
140
+  final Key _toolbarKey = UniqueKey();
141
+  final Key _overlayKey = UniqueKey();
142
+
143
+  ZefyrToolbarDelegate _delegate;
144
+  AnimationController _overlayAnimation;
145
+  WidgetBuilder _overlayBuilder;
146
+  Completer<void> _overlayCompleter;
147
+
148
+  TextSelection _selection;
149
+
150
+  void markNeedsRebuild() {
151
+    setState(() {});
152
+  }
153
+
154
+  Widget buildButton(BuildContext context, ZefyrToolbarAction action,
155
+      {VoidCallback onPressed}) {
156
+    return _delegate.buildButton(context, action, onPressed: onPressed);
157
+  }
158
+
159
+  Future<void> showOverlay(WidgetBuilder builder) async {
160
+    assert(_overlayBuilder == null);
161
+    final completer = new Completer<void>();
162
+    setState(() {
163
+      _overlayBuilder = builder;
164
+      _overlayCompleter = completer;
165
+      _overlayAnimation.forward();
166
+    });
167
+    return completer.future;
168
+  }
169
+
170
+  void closeOverlay() {
171
+    if (!hasOverlay) return;
172
+    _overlayAnimation.reverse().whenComplete(() {
173
+      setState(() {
174
+        _overlayBuilder = null;
175
+        _overlayCompleter?.complete();
176
+        _overlayCompleter = null;
177
+      });
178
+    });
179
+  }
180
+
181
+  bool get hasOverlay => _overlayBuilder != null;
182
+
183
+  @override
184
+  void initState() {
185
+    super.initState();
186
+    _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
187
+    _overlayAnimation = new AnimationController(
188
+        vsync: this, duration: Duration(milliseconds: 100));
189
+  }
190
+
191
+  @override
192
+  void didUpdateWidget(ZefyrToolbar oldWidget) {
193
+    super.didUpdateWidget(oldWidget);
194
+    if (widget.delegate != oldWidget.delegate) {
195
+      _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
196
+    }
197
+  }
198
+
199
+  @override
200
+  void didChangeDependencies() {
201
+    super.didChangeDependencies();
202
+    final editor = ZefyrEditor.of(context);
203
+    if (_selection != editor.selection) {
204
+      _selection = editor.selection;
205
+      closeOverlay();
206
+    }
207
+  }
208
+
209
+  @override
210
+  Widget build(BuildContext context) {
211
+    final editor = ZefyrEditor.of(context);
212
+
213
+    if (editor.focusOwner == FocusOwner.none) {
214
+      return new Container();
215
+    }
216
+
217
+    final layers = <Widget>[];
218
+
219
+    // Must set unique key for the toolbar to prevent it from reconstructing
220
+    // new state each time we toggle overlay.
221
+    final toolbar = ZefyrToolbarScaffold(
222
+      key: _toolbarKey,
223
+      body: ZefyrButtonList(buttons: _buildButtons(context)),
224
+      trailing: buildButton(context, ZefyrToolbarAction.hideKeyboard),
225
+    );
226
+
227
+    layers.add(toolbar);
228
+
229
+    if (hasOverlay) {
230
+      Widget widget = new Builder(builder: _overlayBuilder);
231
+      assert(widget != null);
232
+      final overlay = FadeTransition(
233
+        key: _overlayKey,
234
+        opacity: _overlayAnimation,
235
+        child: widget,
236
+      );
237
+      layers.add(overlay);
238
+    }
239
+
240
+    final constraints =
241
+        BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
242
+    return new _ZefyrToolbarScope(
243
+      toolbar: this,
244
+      child: Container(
245
+        constraints: constraints,
246
+        child: Stack(children: layers),
247
+      ),
248
+    );
249
+  }
250
+
251
+  List<Widget> _buildButtons(BuildContext context) {
252
+    final buttons = <Widget>[
253
+      buildButton(context, ZefyrToolbarAction.bold),
254
+      buildButton(context, ZefyrToolbarAction.italic),
255
+      LinkButton(),
256
+      HeadingButton(),
257
+      buildButton(context, ZefyrToolbarAction.bulletList),
258
+      buildButton(context, ZefyrToolbarAction.numberList),
259
+      buildButton(context, ZefyrToolbarAction.quote),
260
+      buildButton(context, ZefyrToolbarAction.code),
261
+    ];
262
+    return buttons;
263
+  }
264
+}
265
+
266
+/// Scrollable list of toolbar buttons.
267
+class ZefyrButtonList extends StatefulWidget {
268
+  const ZefyrButtonList({Key key, @required this.buttons}) : super(key: key);
269
+  final List<Widget> buttons;
270
+
271
+  @override
272
+  _ZefyrButtonListState createState() => _ZefyrButtonListState();
273
+}
274
+
275
+class _ZefyrButtonListState extends State<ZefyrButtonList> {
276
+  final ScrollController _controller = new ScrollController();
277
+  bool _showLeftArrow = false;
278
+  bool _showRightArrow = false;
279
+
280
+  @override
281
+  void initState() {
282
+    super.initState();
283
+    _controller.addListener(_handleScroll);
284
+    // Workaround to allow scroll controller attach to our ListView so that
285
+    // we can detect if overflow arrows need to be shown on init.
286
+    // TODO: find a better way to detect overflow
287
+    Timer.run(_handleScroll);
288
+  }
289
+
290
+  @override
291
+  Widget build(BuildContext context) {
292
+    final theme = ZefyrTheme.of(context).toolbarTheme;
293
+    final color = theme.iconColor;
294
+    final list = ListView(
295
+      scrollDirection: Axis.horizontal,
296
+      controller: _controller,
297
+      children: widget.buttons,
298
+      physics: ClampingScrollPhysics(),
299
+    );
300
+
301
+    final leftArrow = _showLeftArrow
302
+        ? Icon(Icons.arrow_left, size: 18.0, color: color)
303
+        : null;
304
+    final rightArrow = _showRightArrow
305
+        ? Icon(Icons.arrow_right, size: 18.0, color: color)
306
+        : null;
307
+    return Row(
308
+      children: <Widget>[
309
+        SizedBox(
310
+          width: 12.0,
311
+          height: ZefyrToolbar.kToolbarHeight,
312
+          child: Container(child: leftArrow, color: theme.color),
313
+        ),
314
+        Expanded(child: ClipRect(child: list)),
315
+        SizedBox(
316
+          width: 12.0,
317
+          height: ZefyrToolbar.kToolbarHeight,
318
+          child: Container(child: rightArrow, color: theme.color),
319
+        ),
320
+      ],
321
+    );
322
+  }
323
+
324
+  void _handleScroll() {
325
+    setState(() {
326
+      _showLeftArrow =
327
+          _controller.position.minScrollExtent != _controller.position.pixels;
328
+      _showRightArrow =
329
+          _controller.position.maxScrollExtent != _controller.position.pixels;
330
+    });
331
+  }
332
+}
333
+
334
+class _DefaultZefyrToolbarDelegate implements ZefyrToolbarDelegate {
335
+  static const kDefaultButtonIcons = {
336
+    ZefyrToolbarAction.bold: Icons.format_bold,
337
+    ZefyrToolbarAction.italic: Icons.format_italic,
338
+    ZefyrToolbarAction.link: Icons.link,
339
+    ZefyrToolbarAction.unlink: Icons.link_off,
340
+    ZefyrToolbarAction.clipboardCopy: Icons.content_copy,
341
+    ZefyrToolbarAction.openInBrowser: Icons.open_in_new,
342
+    ZefyrToolbarAction.heading: Icons.format_size,
343
+    ZefyrToolbarAction.bulletList: Icons.format_list_bulleted,
344
+    ZefyrToolbarAction.numberList: Icons.format_list_numbered,
345
+    ZefyrToolbarAction.code: Icons.code,
346
+    ZefyrToolbarAction.quote: Icons.format_quote,
347
+    ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide,
348
+    ZefyrToolbarAction.close: Icons.close,
349
+    ZefyrToolbarAction.confirm: Icons.check,
350
+  };
351
+
352
+  static const kSpecialIconSizes = {
353
+    ZefyrToolbarAction.unlink: 20.0,
354
+    ZefyrToolbarAction.clipboardCopy: 20.0,
355
+    ZefyrToolbarAction.openInBrowser: 20.0,
356
+    ZefyrToolbarAction.close: 20.0,
357
+    ZefyrToolbarAction.confirm: 20.0,
358
+  };
359
+
360
+  static const kDefaultButtonTexts = {
361
+    ZefyrToolbarAction.headingLevel1: 'H1',
362
+    ZefyrToolbarAction.headingLevel2: 'H2',
363
+    ZefyrToolbarAction.headingLevel3: 'H3',
364
+  };
365
+
366
+  @override
367
+  Widget buildButton(BuildContext context, ZefyrToolbarAction action,
368
+      {VoidCallback onPressed}) {
369
+    final theme = Theme.of(context);
370
+    if (kDefaultButtonIcons.containsKey(action)) {
371
+      final icon = kDefaultButtonIcons[action];
372
+      final size = kSpecialIconSizes[action];
373
+      return ZefyrButton.icon(
374
+        action: action,
375
+        icon: icon,
376
+        iconSize: size,
377
+        onPressed: onPressed,
378
+      );
379
+    } else {
380
+      final text = kDefaultButtonTexts[action];
381
+      assert(text != null);
382
+      final style = theme.textTheme.caption
383
+          .copyWith(fontWeight: FontWeight.bold, fontSize: 14.0);
384
+      return ZefyrButton.text(
385
+        action: action,
386
+        text: text,
387
+        style: style,
388
+        onPressed: onPressed,
389
+      );
390
+    }
391
+  }
392
+}

+ 35
- 0
packages/zefyr/lib/util.dart Целия файл

@@ -0,0 +1,35 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+
5
+/// Utility functions for Zefyr.
6
+library zefyr.util;
7
+
8
+import 'dart:math' as math;
9
+
10
+import 'package:quill_delta/quill_delta.dart';
11
+
12
+export 'src/fast_diff.dart';
13
+
14
+int getPositionDelta(Delta user, Delta actual) {
15
+  final userIter = new DeltaIterator(user);
16
+  final actualIter = new DeltaIterator(actual);
17
+  int diff = 0;
18
+  while (userIter.hasNext || actualIter.hasNext) {
19
+    num length = math.min(userIter.peekLength(), actualIter.peekLength());
20
+    final userOp = userIter.next(length);
21
+    final actualOp = actualIter.next(length);
22
+    assert(userOp.length == actualOp.length);
23
+    if (userOp.key == actualOp.key) continue;
24
+    if (userOp.isInsert && actualOp.isRetain) {
25
+      diff -= userOp.length;
26
+    } else if (userOp.isDelete && actualOp.isRetain) {
27
+      diff += userOp.length;
28
+    } else if (userOp.isRetain && actualOp.isInsert) {
29
+      diff += actualOp.length;
30
+    } else {
31
+      // TODO: this likely needs to cover more edge cases.
32
+    }
33
+  }
34
+  return diff;
35
+}

+ 18
- 0
packages/zefyr/lib/widgets.dart Целия файл

@@ -0,0 +1,18 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+library zefyr.widgets;
5
+
6
+export 'src/widgets/buttons.dart' hide HeadingButton, LinkButton;
7
+export 'src/widgets/code.dart';
8
+export 'src/widgets/common.dart';
9
+export 'src/widgets/controller.dart';
10
+export 'src/widgets/editable_text.dart';
11
+export 'src/widgets/editor.dart';
12
+export 'src/widgets/list.dart';
13
+export 'src/widgets/paragraph.dart';
14
+export 'src/widgets/quote.dart';
15
+//export 'src/widgets/render_context.dart';
16
+export 'src/widgets/selection.dart' hide SelectionHandleDriver;
17
+export 'src/widgets/theme.dart';
18
+export 'src/widgets/toolbar.dart';

+ 18
- 0
packages/zefyr/lib/zefyr.dart Целия файл

@@ -0,0 +1,18 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+
5
+/// Zefyr widgets and document model.
6
+///
7
+/// To use, `import 'package:zefyr/zefyr.dart';`.
8
+///
9
+/// See also:
10
+///
11
+///   * [Quick start]()
12
+///   * [NotusDocument], model for Zefyr documents.
13
+///   * [ZefyrEditor], widget for editing Zefyr documents.
14
+library zefyr;
15
+
16
+export 'package:notus/notus.dart';
17
+
18
+export 'widgets.dart';

+ 19
- 0
packages/zefyr/pubspec.yaml Целия файл

@@ -0,0 +1,19 @@
1
+name: zefyr
2
+description: Flutter widgets for editing rich text.
3
+version: 0.1.0
4
+author: anatoly.pulyaevskiy@gmail.com
5
+homepage: https://github.com/pulyaevskiy/zefyr
6
+
7
+dependencies:
8
+  flutter:
9
+    sdk: flutter
10
+  collection: ^1.14.6
11
+  notus:
12
+    path: ../notus
13
+  url_launcher: ^3.0.0
14
+  quill_delta: ^1.0.0-dev.1.0
15
+
16
+dev_dependencies:
17
+  flutter_test:
18
+    sdk: flutter
19
+  test: ^0.12.41

+ 37
- 0
packages/zefyr/test/fast_diff_test.dart Целия файл

@@ -0,0 +1,37 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter_test/flutter_test.dart';
5
+import 'package:zefyr/src/fast_diff.dart';
6
+
7
+void main() {
8
+  group('fastDiff', () {
9
+    test('insert', () {
10
+      var oldText = 'fastDiff';
11
+      var newText = 'fasterDiff';
12
+      var result = fastDiff(oldText, newText, 6);
13
+      expect(result.start, 4);
14
+      expect(result.deleted, "");
15
+      expect(result.inserted, "er");
16
+      expect("$result", 'DiffResult[4, "", "er"]');
17
+    });
18
+
19
+    test('delete', () {
20
+      var oldText = 'fastDiff';
21
+      var newText = 'fasDiff';
22
+      var result = fastDiff(oldText, newText, 3);
23
+      expect(result.start, 3);
24
+      expect(result.deleted, "t");
25
+      expect(result.inserted, "");
26
+    });
27
+
28
+    test('replace', () {
29
+      var oldText = 'fastDiff';
30
+      var newText = 'fas_Diff';
31
+      var result = fastDiff(oldText, newText, 4);
32
+      expect(result.start, 3);
33
+      expect(result.deleted, "t");
34
+      expect(result.inserted, "_");
35
+    });
36
+  });
37
+}

+ 22
- 0
packages/zefyr/test/painting/caret_painter_test.dart Целия файл

@@ -0,0 +1,22 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui';
5
+
6
+import 'package:flutter_test/flutter_test.dart';
7
+import 'package:zefyr/src/widgets/caret.dart';
8
+
9
+void main() {
10
+  group('$CaretPainter', () {
11
+    test('prototype is null before layout', () {
12
+      var painter = new CaretPainter();
13
+      expect(painter.prototype, isNull);
14
+    });
15
+
16
+    test('prototype is set after layout', () {
17
+      var painter = new CaretPainter();
18
+      painter.layout(16.0);
19
+      expect(painter.prototype, new Rect.fromLTWH(0.0, 0.0, 1.0, 14.0));
20
+    });
21
+  });
22
+}

+ 49
- 0
packages/zefyr/test/rendering/render_editable_paragraph_test.dart Целия файл

@@ -0,0 +1,49 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'dart:ui';
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter/rendering.dart';
8
+import 'package:flutter_test/flutter_test.dart';
9
+import 'package:zefyr/src/widgets/editable_paragraph.dart';
10
+import 'package:zefyr/src/widgets/render_context.dart';
11
+import 'package:zefyr/zefyr.dart';
12
+
13
+void main() {
14
+  group('$RenderEditableParagraph', () {
15
+    final doc = new NotusDocument();
16
+    doc.insert(0, 'This House Is A Circus');
17
+    final text = new TextSpan(text: 'This House Is A Circus');
18
+    final link = new LayerLink();
19
+    final showCursor = new ValueNotifier<bool>(true);
20
+    final selection = new TextSelection.collapsed(offset: 0);
21
+    final selectionColor = Colors.blue;
22
+    ZefyrRenderContext viewport;
23
+
24
+    RenderEditableParagraph p;
25
+    setUp(() {
26
+      WidgetsFlutterBinding.ensureInitialized();
27
+      viewport = new ZefyrRenderContext();
28
+      p = new RenderEditableParagraph(
29
+        text,
30
+        node: doc.root.children.first,
31
+        layerLink: link,
32
+        renderContext: viewport,
33
+        showCursor: showCursor,
34
+        selection: selection,
35
+        selectionColor: selectionColor,
36
+        textDirection: TextDirection.ltr,
37
+      );
38
+    });
39
+
40
+    test('it registers with viewport', () {
41
+      var owner = new PipelineOwner();
42
+      expect(viewport.active, isNot(contains(p)));
43
+      p.attach(owner);
44
+      expect(viewport.dirty, contains(p));
45
+      p.layout(new BoxConstraints());
46
+      expect(viewport.active, contains(p));
47
+    });
48
+  });
49
+}

+ 93
- 0
packages/zefyr/test/testing.dart Целия файл

@@ -0,0 +1,93 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter_test/flutter_test.dart';
6
+import 'package:quill_delta/quill_delta.dart';
7
+import 'package:zefyr/src/widgets/selection.dart';
8
+import 'package:zefyr/zefyr.dart';
9
+
10
+var delta = new Delta()..insert('This House Is A Circus\n');
11
+
12
+class EditorSandBox {
13
+  final WidgetTester tester;
14
+  final FocusNode focusNode;
15
+  final NotusDocument document;
16
+  final ZefyrController controller;
17
+  final Widget widget;
18
+
19
+  factory EditorSandBox({
20
+    @required WidgetTester tester,
21
+    FocusNode focusNode,
22
+    NotusDocument document,
23
+    ZefyrThemeData theme,
24
+  }) {
25
+    focusNode ??= FocusNode();
26
+    document ??= NotusDocument.fromDelta(delta);
27
+    var controller = ZefyrController(document);
28
+
29
+    Widget widget = ZefyrEditor(controller: controller, focusNode: focusNode);
30
+    if (theme != null) {
31
+      widget = ZefyrTheme(data: theme, child: widget);
32
+    }
33
+    widget = MaterialApp(home: widget);
34
+
35
+    return EditorSandBox._(tester, focusNode, document, controller, widget);
36
+  }
37
+
38
+  EditorSandBox._(
39
+      this.tester, this.focusNode, this.document, this.controller, this.widget);
40
+
41
+  TextSelection get selection => controller.selection;
42
+
43
+  Future<void> unfocus() {
44
+    focusNode.unfocus();
45
+    return tester.pumpAndSettle();
46
+  }
47
+
48
+  Future<void> updateSelection({int base, int extent}) {
49
+    controller.updateSelection(
50
+      new TextSelection(baseOffset: base, extentOffset: extent),
51
+    );
52
+    return tester.pumpAndSettle();
53
+  }
54
+
55
+  Future<void> tapEditor() async {
56
+    await tester.pumpWidget(widget);
57
+    await tester.tap(find.byType(ZefyrParagraph).first);
58
+    await tester.pumpAndSettle();
59
+    expect(focusNode.hasFocus, isTrue);
60
+  }
61
+
62
+  Future<void> tapHideKeyboardButton() async {
63
+    await tapButtonWithIcon(Icons.keyboard_hide);
64
+  }
65
+
66
+  Future<void> tapButtonWithIcon(IconData icon) async {
67
+    await tester.tap(find.widgetWithIcon(ZefyrButton, icon));
68
+    await tester.pumpAndSettle();
69
+  }
70
+
71
+  Future<void> tapButtonWithText(String text) async {
72
+    await tester.tap(find.widgetWithText(ZefyrButton, text));
73
+    await tester.pumpAndSettle();
74
+  }
75
+
76
+  RawZefyrButton findButtonWithIcon(IconData icon) {
77
+    RawZefyrButton button =
78
+        tester.widget(find.widgetWithIcon(RawZefyrButton, icon));
79
+    return button;
80
+  }
81
+
82
+  RawZefyrButton findButtonWithText(String text) {
83
+    RawZefyrButton button =
84
+        tester.widget(find.widgetWithText(RawZefyrButton, text));
85
+    return button;
86
+  }
87
+
88
+  Finder findSelectionHandle() {
89
+    return find.descendant(
90
+        of: find.byType(SelectionHandleDriver),
91
+        matching: find.byType(Positioned));
92
+  }
93
+}

+ 43
- 0
packages/zefyr/test/util_test.dart Целия файл

@@ -0,0 +1,43 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter_test/flutter_test.dart';
5
+import 'package:quill_delta/quill_delta.dart';
6
+import 'package:zefyr/util.dart';
7
+
8
+void main() {
9
+  group('getPositionDelta', () {
10
+    test('actual has more characters inserted than user', () {
11
+      final user = new Delta()
12
+        ..retain(7)
13
+        ..insert('a');
14
+      final actual = new Delta()
15
+        ..retain(7)
16
+        ..insert('\na');
17
+      final result = getPositionDelta(user, actual);
18
+      expect(result, 1);
19
+    });
20
+
21
+    test('actual has less characters inserted than user', () {
22
+      final user = new Delta()
23
+        ..retain(7)
24
+        ..insert('abc');
25
+      final actual = new Delta()
26
+        ..retain(7)
27
+        ..insert('ab');
28
+      final result = getPositionDelta(user, actual);
29
+      expect(result, -1);
30
+    });
31
+
32
+    test('actual has less characters deleted than user', () {
33
+      final user = new Delta()
34
+        ..retain(7)
35
+        ..delete(3);
36
+      final actual = new Delta()
37
+        ..retain(7)
38
+        ..delete(2);
39
+      final result = getPositionDelta(user, actual);
40
+      expect(result, 1);
41
+    });
42
+  });
43
+}

+ 135
- 0
packages/zefyr/test/widgets/buttons_test.dart Целия файл

@@ -0,0 +1,135 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter_test/flutter_test.dart';
6
+import 'package:zefyr/src/widgets/buttons.dart';
7
+import 'package:zefyr/zefyr.dart';
8
+
9
+import '../testing.dart';
10
+
11
+void main() {
12
+  group('$ZefyrButton', () {
13
+    testWidgets('toggle style', (tester) async {
14
+      final editor = new EditorSandBox(tester: tester);
15
+      await editor.tapEditor();
16
+      await editor.updateSelection(base: 5, extent: 10);
17
+      await editor.tapButtonWithIcon(Icons.format_bold);
18
+
19
+      LineNode line = editor.document.root.children.first;
20
+      expect(line.childCount, 3);
21
+      TextNode bold = line.children.elementAt(1);
22
+      expect(bold.style.toJson(), NotusAttribute.bold.toJson());
23
+      expect(bold.value, 'House');
24
+
25
+      await editor.tapButtonWithIcon(Icons.format_bold);
26
+      line = editor.document.root.children.first;
27
+      expect(line.childCount, 1);
28
+    });
29
+
30
+    testWidgets('toggle state for different styles of the same attribute',
31
+        (tester) async {
32
+      final editor = new EditorSandBox(tester: tester);
33
+      await editor.tapEditor();
34
+
35
+      await editor.tapButtonWithIcon(Icons.format_list_bulleted);
36
+      expect(editor.document.root.children.first, isInstanceOf<BlockNode>());
37
+
38
+      var ul = editor.findButtonWithIcon(Icons.format_list_bulleted);
39
+      var ol = editor.findButtonWithIcon(Icons.format_list_numbered);
40
+      expect(ul.isToggled, isTrue);
41
+      expect(ol.isToggled, isFalse);
42
+    });
43
+  });
44
+
45
+  group('$HeadingButton', () {
46
+    testWidgets('toggle menu', (tester) async {
47
+      final editor = new EditorSandBox(tester: tester);
48
+      await editor.tapEditor();
49
+      await editor.tapButtonWithIcon(Icons.format_size);
50
+
51
+      expect(find.text('H1'), findsOneWidget);
52
+      var h1 = editor.findButtonWithText('H1');
53
+      expect(h1.action, ZefyrToolbarAction.headingLevel1);
54
+      var h2 = editor.findButtonWithText('H2');
55
+      expect(h2.action, ZefyrToolbarAction.headingLevel2);
56
+      var h3 = editor.findButtonWithText('H3');
57
+      expect(h3.action, ZefyrToolbarAction.headingLevel3);
58
+    });
59
+
60
+    testWidgets('toggle styles', (tester) async {
61
+      final editor = new EditorSandBox(tester: tester);
62
+      await editor.tapEditor();
63
+      await editor.tapButtonWithIcon(Icons.format_size);
64
+      await editor.tapButtonWithText('H3');
65
+      LineNode line = editor.document.root.children.first;
66
+      expect(line.style.containsSame(NotusAttribute.heading.level3), isTrue);
67
+      await editor.tapButtonWithText('H2');
68
+      expect(line.style.containsSame(NotusAttribute.heading.level2), isTrue);
69
+    });
70
+  });
71
+
72
+  group('$LinkButton', () {
73
+    testWidgets('disabled when selection is collapsed', (tester) async {
74
+      final editor = new EditorSandBox(tester: tester);
75
+      await editor.tapEditor();
76
+      await editor.tapButtonWithIcon(Icons.link);
77
+      expect(find.byIcon(Icons.link_off), findsNothing);
78
+    });
79
+
80
+    testWidgets('enabled and toggles menu with non-empty selection',
81
+        (tester) async {
82
+      final editor = new EditorSandBox(tester: tester);
83
+      await editor.tapEditor();
84
+      await editor.updateSelection(base: 5, extent: 10);
85
+      await editor.tapButtonWithIcon(Icons.link);
86
+      expect(find.byIcon(Icons.link_off), findsOneWidget);
87
+    });
88
+
89
+    testWidgets('auto cancels edit on selection update', (tester) async {
90
+      final editor = new EditorSandBox(tester: tester);
91
+      await editor.tapEditor();
92
+      await editor.updateSelection(base: 5, extent: 10);
93
+      await editor.tapButtonWithIcon(Icons.link);
94
+      await tester
95
+          .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
96
+      await tester.pumpAndSettle();
97
+      await editor.updateSelection(base: 10, extent: 10);
98
+      expect(find.byIcon(Icons.link_off), findsNothing);
99
+    });
100
+
101
+    testWidgets('editing link', (tester) async {
102
+      final editor = new EditorSandBox(tester: tester);
103
+      await editor.tapEditor();
104
+      await editor.updateSelection(base: 5, extent: 10);
105
+
106
+      await editor.tapButtonWithIcon(Icons.link);
107
+      await tester
108
+          .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
109
+      await tester.pumpAndSettle();
110
+      // TODO: figure out why below finder finds 2 instances of TextField
111
+      expect(find.widgetWithText(TextField, 'https://'), findsWidgets);
112
+      await tester.enterText(find.widgetWithText(TextField, 'https://').first,
113
+          'https://github.com');
114
+      await tester.pumpAndSettle();
115
+      expect(
116
+          find.widgetWithText(TextField, 'https://github.com'), findsOneWidget);
117
+      await editor.tapButtonWithIcon(Icons.check);
118
+      expect(find.widgetWithText(ZefyrToolbarScaffold, 'https://github.com'),
119
+          findsOneWidget);
120
+      LineNode line = editor.document.root.children.first;
121
+      expect(line.childCount, 3);
122
+      TextNode link = line.children.elementAt(1);
123
+      expect(link.value, 'House');
124
+      expect(link.style.toJson(),
125
+          NotusAttribute.link.fromString('https://github.com').toJson());
126
+
127
+      // unlink
128
+      await editor.updateSelection(base: 7, extent: 7);
129
+      await editor.tapButtonWithIcon(Icons.link);
130
+      await editor.tapButtonWithIcon(Icons.link_off);
131
+      line = editor.document.root.children.first;
132
+      expect(line.childCount, 1);
133
+    });
134
+  });
135
+}

+ 21
- 0
packages/zefyr/test/widgets/code_test.dart Целия файл

@@ -0,0 +1,21 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter_test/flutter_test.dart';
6
+import 'package:zefyr/zefyr.dart';
7
+
8
+import '../testing.dart';
9
+
10
+void main() {
11
+  group('$ZefyrCode', () {
12
+    testWidgets('format as code', (tester) async {
13
+      final editor = new EditorSandBox(tester: tester);
14
+      await editor.tapEditor();
15
+      await editor.tapButtonWithIcon(Icons.code);
16
+
17
+      BlockNode block = editor.document.root.children.first;
18
+      expect(block.style.get(NotusAttribute.block), NotusAttribute.block.code);
19
+    });
20
+  });
21
+}

+ 0
- 0
packages/zefyr/test/widgets/controller_test.dart Целия файл


Някои файлове не бяха показани, защото твърде много файлове са промени