├── .github └── workflows │ ├── flutter.yml │ └── gh_pages.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── custom_text_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── advanced_view.dart │ ├── basic_view.dart │ ├── code_view_page.dart │ ├── common │ │ ├── device_info.dart │ │ ├── highlight │ │ │ ├── dart_definitions.dart │ │ │ ├── highlight.dart │ │ │ ├── json_definitions.dart │ │ │ ├── language_parser.dart │ │ │ ├── markdown_definitions.dart │ │ │ └── xml_definitions.dart │ │ └── popup.dart │ ├── example_page.dart │ ├── examples │ │ ├── advanced │ │ │ ├── expressive_text.dart │ │ │ ├── fill_in_blanks.dart │ │ │ └── searchable_text.dart │ │ ├── basic │ │ │ ├── custom_pattern.dart │ │ │ ├── external_parser.dart │ │ │ ├── hover_style.dart │ │ │ ├── on_gesture.dart │ │ │ ├── overwriting_pattern.dart │ │ │ ├── pre_builder.dart │ │ │ ├── real_hyperlinks.dart │ │ │ ├── selective_definition.dart │ │ │ ├── simple.dart │ │ │ ├── span_definition.dart │ │ │ ├── spans_constructor.dart │ │ │ ├── styles_and_actions.dart │ │ │ └── text_editing_controller.dart │ │ └── utils │ │ │ └── external_parser │ │ │ ├── segmented_buttons.dart │ │ │ └── settings.dart │ ├── home_page.dart │ ├── main.dart │ ├── routes.dart │ ├── routes.g.dart │ └── widgets │ │ ├── description.dart │ │ ├── hyperlink.dart │ │ └── layouts.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── index.html │ ├── manifest.json │ └── styles.css └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── lib ├── custom_text.dart └── src │ ├── builder.dart │ ├── definitions.dart │ ├── gesture_details.dart │ ├── matchers.dart │ ├── parser_options.dart │ ├── span │ ├── data.dart │ ├── gesture_handler.dart │ ├── span_utils.dart │ ├── spans_builder.dart │ ├── split_spans.dart │ ├── text_span_notifier.dart │ └── transient_elements_builder.dart │ ├── text.dart │ ├── text_editing_controller.dart │ └── utils.dart ├── pubspec.yaml ├── screenshots ├── custom_text_editing_controller.gif ├── event_position.gif ├── external_parser.gif ├── mouse_cursor_and_text_style_on_hover.gif └── unique_styles_and_actions.gif └── test ├── unit_tests ├── custom_span_builder_test.dart ├── equality_test.dart ├── gesture_handler_test.dart ├── span_props_test.dart ├── split_spans_test.dart └── transient_build_test.dart └── widget_tests ├── basic_test.dart ├── composition_test.dart ├── external_parser_test.dart ├── pre_builder_test.dart ├── rebuild_test.dart ├── selective_definition_test.dart ├── span_definition_test.dart ├── spans_constructor_test.dart ├── text_definition_test.dart ├── text_editing_controller_test.dart ├── utils.dart ├── widget_span_test.dart └── widgets.dart /.github/workflows/flutter.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | # branches: [main] 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: [main] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | flutter-tests: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: subosito/flutter-action@v2 19 | with: 20 | channel: stable 21 | flutter-version: 3.16.9 22 | - name: Install dependencies 23 | run: flutter pub get 24 | - name: Check format 25 | run: dart format --output=none --set-exit-if-changed . 26 | - name: Analyze 27 | run: flutter analyze --no-pub 28 | - name: Run tests 29 | run: flutter test --coverage 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v5 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/gh_pages.yml: -------------------------------------------------------------------------------- 1 | name: Gh-Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: subosito/flutter-action@v2 13 | with: 14 | channel: stable 15 | flutter-version: 3.16.9 16 | - name: Update dependencies 17 | run: flutter pub upgrade 18 | - uses: bluefireteam/flutter-gh-pages@v9 19 | with: 20 | workingDir: example 21 | baseHref: /flutter_custom_text/ 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | 32 | coverage/ 33 | .fvm/ 34 | .fvmrc 35 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9944297138845a94256f1cf37beb88ff9a8e811a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 kaboc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-inference: true 7 | strict-raw-types: true 8 | errors: 9 | missing_required_param: error 10 | missing_return: error 11 | todo: ignore 12 | 13 | linter: 14 | rules: 15 | # For Flutter 16 | # diagnostic_describe_all_properties: true 17 | sized_box_shrink_expand: true 18 | use_colored_box: true 19 | use_decorated_box: true 20 | 21 | # For Dart 22 | always_declare_return_types: true 23 | always_put_control_body_on_new_line: true 24 | # always_put_required_named_parameters_first: true 25 | # always_specify_types: true 26 | # always_use_package_imports: true 27 | annotate_redeclares: true 28 | avoid_annotating_with_dynamic: true 29 | avoid_bool_literals_in_conditional_expressions: true 30 | avoid_catches_without_on_clauses: true 31 | avoid_catching_errors: true 32 | avoid_classes_with_only_static_members: true 33 | avoid_double_and_int_checks: true 34 | avoid_dynamic_calls: true 35 | avoid_equals_and_hash_code_on_mutable_classes: true 36 | avoid_escaping_inner_quotes: true 37 | avoid_field_initializers_in_const_classes: true 38 | avoid_final_parameters: true 39 | avoid_implementing_value_types: true 40 | avoid_js_rounded_ints: true 41 | avoid_multiple_declarations_per_line: true 42 | avoid_positional_boolean_parameters: true 43 | avoid_private_typedef_functions: true 44 | avoid_redundant_argument_values: true 45 | avoid_returning_this: true 46 | avoid_setters_without_getters: true 47 | avoid_slow_async_io: true 48 | avoid_types_on_closure_parameters: true 49 | avoid_type_to_string: true 50 | avoid_unused_constructor_parameters: true 51 | avoid_void_async: true 52 | cancel_subscriptions: true 53 | cascade_invocations: true 54 | cast_nullable_to_non_nullable: true 55 | close_sinks: true 56 | combinators_ordering: true 57 | comment_references: true 58 | conditional_uri_does_not_exist: true 59 | deprecated_consistency: true 60 | deprecated_member_use_from_same_package: true 61 | # directives_ordering: true 62 | # discarded_futures: true 63 | do_not_use_environment: true 64 | eol_at_end_of_file: true 65 | implicit_reopen: true 66 | invalid_case_patterns: true 67 | join_return_with_assignment: true 68 | leading_newlines_in_multiline_strings: true 69 | lines_longer_than_80_chars: true 70 | literal_only_boolean_expressions: true 71 | missing_whitespace_between_adjacent_strings: true 72 | no_adjacent_strings_in_list: true 73 | no_default_cases: true 74 | no_runtimeType_toString: true 75 | no_self_assignments: true 76 | noop_primitive_operations: true 77 | omit_local_variable_types: true 78 | one_member_abstracts: true 79 | only_throw_errors: true 80 | package_api_docs: true 81 | parameter_assignments: true 82 | prefer_asserts_in_initializer_lists: true 83 | prefer_asserts_with_message: true 84 | prefer_constructors_over_static_methods: true 85 | # prefer_expression_function_bodies: true 86 | prefer_final_in_for_each: true 87 | prefer_final_locals: true 88 | prefer_foreach: true 89 | prefer_if_elements_to_conditional_expressions: true 90 | # prefer_int_literals: true 91 | prefer_mixin: true 92 | prefer_null_aware_method_calls: true 93 | prefer_single_quotes: true 94 | prefer_void_to_null: true 95 | public_member_api_docs: true 96 | require_trailing_commas: true 97 | sort_constructors_first: true 98 | # sort_pub_dependencies: true 99 | sort_unnamed_constructors_first: true 100 | test_types_in_equals: true 101 | throw_in_finally: true 102 | tighten_type_of_initializing_formals: true 103 | type_annotate_public_apis: true 104 | unawaited_futures: true 105 | unnecessary_await_in_return: true 106 | unnecessary_breaks: true 107 | unnecessary_lambdas: true 108 | unnecessary_library_directive: true 109 | unnecessary_null_aware_operator_on_extension_on_nullable: true 110 | unnecessary_null_checks: true 111 | unnecessary_parenthesis: true 112 | unnecessary_raw_strings: true 113 | unnecessary_statements: true 114 | unreachable_from_main: true 115 | unsafe_html: true 116 | use_enums: true 117 | use_if_null_to_convert_nulls_to_bools: true 118 | use_is_even_rather_than_modulo: true 119 | use_late_for_private_fields_and_variables: true 120 | use_named_constants: true 121 | use_raw_strings: true 122 | use_setters_to_change_properties: true 123 | use_string_buffers: true 124 | use_test_throws_matchers: true 125 | use_to_and_as_if_applicable: true 126 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "41456452f29d64e8deb623a3c927524bcf9f111b" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 17 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 18 | - platform: android 19 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 20 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 21 | - platform: ios 22 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 23 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 24 | - platform: macos 25 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 26 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 27 | - platform: web 28 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 29 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 30 | - platform: windows 31 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 32 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 33 | 34 | # User provided section 35 | 36 | # List of Local paths (relative to this file) that should be 37 | # ignored by the migrate tool. 38 | # 39 | # Files that are not part of the templates will be ignored by default. 40 | unmanaged_files: 41 | - 'lib/main.dart' 42 | - 'ios/Runner.xcodeproj/project.pbxproj' 43 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # CustomText Demo 2 | 3 | A demo app showing all the following examples. 4 | 5 | - [Simple (no gesture)](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/simple.dart) 6 | - [Styles and actions per definition](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/styles_and_actions.dart) 7 | - [Overwriting match pattern](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/overwriting_pattern.dart) 8 | - [Custom match pattern](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/custom_pattern.dart) 9 | - [SelectiveDefinition](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/selective_definition.dart) 10 | - [SpanDefinition](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/span_definition.dart) 11 | - [Real hyperlinks](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/real_hyperlinks.dart) 12 | - [Changing mouse cursor and text style on hover](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/hover_style.dart) 13 | - [Event positions and onGesture](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/on_gesture.dart) 14 | - [CustomText.spans](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/spans_constructor.dart) 15 | - [CustomText with preBuilder](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/pre_builder.dart) 16 | - [CustomTextEditingController](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/text_editing_controller.dart) 17 | - [Using an external parser](https://github.com/kaboc/flutter_custom_text/blob/main/example/lib/examples/src/external_parser.dart) 18 | 19 | ## Examples in action 20 | 21 | https://kaboc.github.io/flutter_custom_text/ 22 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: false 6 | use_key_in_widget_constructors: false 7 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.custom_text_example" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | applicationId "com.example.custom_text_example" 45 | // You can update the following values to match your application needs. 46 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies {} 67 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/custom_text_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.custom_text_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version "7.3.0" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 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 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 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 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | CustomText Demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | CustomText Demo 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSApplicationQueriesSchemes 26 | 27 | https 28 | 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | CADisableMinimumFrameDurationOnPhone 49 | 50 | UIApplicationSupportsIndirectInputEvents 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/lib/advanced_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text_example/routes.dart'; 4 | 5 | class AdvancedView extends StatelessWidget { 6 | const AdvancedView(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return ListView( 11 | children: [ 12 | ListTile( 13 | title: const Text(ExpressiveTextRoute.title), 14 | trailing: const Icon(Icons.chevron_right), 15 | onTap: () => const ExpressiveTextRoute().go(context), 16 | ), 17 | ListTile( 18 | title: const Text(SearchableTextRoute.title), 19 | trailing: const Icon(Icons.chevron_right), 20 | onTap: () => const SearchableTextRoute().go(context), 21 | ), 22 | ListTile( 23 | title: const Text(FillInBlanksRoute.title), 24 | trailing: const Icon(Icons.chevron_right), 25 | onTap: () => const FillInBlanksRoute().go(context), 26 | ), 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/basic_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text_example/routes.dart'; 4 | 5 | class BasicView extends StatelessWidget { 6 | const BasicView(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return ListView( 11 | children: [ 12 | ListTile( 13 | title: const Text(SimpleRoute.title), 14 | trailing: const Icon(Icons.chevron_right), 15 | onTap: () => const SimpleRoute().go(context), 16 | ), 17 | ListTile( 18 | title: const Text(StylesAndActionsRoute.title), 19 | trailing: const Icon(Icons.chevron_right), 20 | onTap: () => const StylesAndActionsRoute().go(context), 21 | ), 22 | ListTile( 23 | title: const Text(OverwritingPatternRoute.title), 24 | trailing: const Icon(Icons.chevron_right), 25 | onTap: () => const OverwritingPatternRoute().go(context), 26 | ), 27 | ListTile( 28 | title: const Text(CustomPatternRoute.title), 29 | trailing: const Icon(Icons.chevron_right), 30 | onTap: () => const CustomPatternRoute().go(context), 31 | ), 32 | ListTile( 33 | title: const Text(SelectiveDefinitionRoute.title), 34 | trailing: const Icon(Icons.chevron_right), 35 | onTap: () => const SelectiveDefinitionRoute().go(context), 36 | ), 37 | ListTile( 38 | title: const Text(SpanDefinitionRoute.title), 39 | trailing: const Icon(Icons.chevron_right), 40 | onTap: () => const SpanDefinitionRoute().go(context), 41 | ), 42 | ListTile( 43 | title: const Text(RealHyperlinksRoute.title), 44 | trailing: const Icon(Icons.chevron_right), 45 | onTap: () => const RealHyperlinksRoute().go(context), 46 | ), 47 | ListTile( 48 | title: const Text(HoverStyleRoute.title), 49 | trailing: const Icon(Icons.chevron_right), 50 | onTap: () => const HoverStyleRoute().go(context), 51 | ), 52 | ListTile( 53 | title: const Text(OnGestureRoute.title), 54 | trailing: const Icon(Icons.chevron_right), 55 | onTap: () => const OnGestureRoute().go(context), 56 | ), 57 | ListTile( 58 | title: const Text(CustomTextSpansRoute.title), 59 | trailing: const Icon(Icons.chevron_right), 60 | onTap: () => const CustomTextSpansRoute().go(context), 61 | ), 62 | ListTile( 63 | title: const Text(PreBuilderRoute.title), 64 | trailing: const Icon(Icons.chevron_right), 65 | onTap: () => const PreBuilderRoute().go(context), 66 | ), 67 | ListTile( 68 | title: const Text(ControllerRoute.title), 69 | trailing: const Icon(Icons.chevron_right), 70 | onTap: () => const ControllerRoute().go(context), 71 | ), 72 | ListTile( 73 | title: const Text(ExternalParserRoute.title), 74 | trailing: const Icon(Icons.chevron_right), 75 | onTap: () => const ExternalParserRoute().go(context), 76 | ), 77 | ], 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/lib/code_view_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'package:custom_text/custom_text.dart'; 5 | import 'package:google_fonts/google_fonts.dart'; 6 | 7 | import 'package:custom_text_example/common/highlight/highlight.dart'; 8 | import 'package:custom_text_example/routes.dart'; 9 | import 'package:custom_text_example/widgets/hyperlink.dart'; 10 | 11 | class CodeViewPage extends StatefulWidget { 12 | const CodeViewPage({ 13 | required this.viewType, 14 | required this.filename, 15 | }); 16 | 17 | final ViewType viewType; 18 | final String filename; 19 | 20 | @override 21 | State createState() => _CodeViewPageState(); 22 | } 23 | 24 | class _CodeViewPageState extends State { 25 | final _horizontalScrollController = ScrollController(); 26 | 27 | String _code = ''; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | _loadSourceCode(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _horizontalScrollController.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | Future _loadSourceCode() async { 42 | final path = 'lib/examples/${widget.viewType.name}/${widget.filename}'; 43 | final code = await rootBundle.loadString(path); 44 | setState(() => _code = code); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return Scaffold( 50 | appBar: AppBar( 51 | title: Text(widget.filename), 52 | actions: [ 53 | Center( 54 | child: Hyperlink( 55 | label: 'View on GitHub', 56 | url: 'https://github.com/kaboc/flutter_custom_text/' 57 | 'blob/main/example/lib/' 58 | 'examples/${widget.viewType.name}/${widget.filename}', 59 | ), 60 | ), 61 | const SizedBox(width: 16.0), 62 | ], 63 | ), 64 | body: SafeArea( 65 | // Placing SelectionArea as the child of SizedBox causes a layout issue. 66 | // https://github.com/flutter/flutter/issues/121053 67 | child: SelectionArea( 68 | child: SizedBox( 69 | width: double.infinity, 70 | child: Scrollbar( 71 | thickness: 8.0, 72 | child: SingleChildScrollView( 73 | primary: true, 74 | child: Scrollbar( 75 | controller: _horizontalScrollController, 76 | child: SingleChildScrollView( 77 | controller: _horizontalScrollController, 78 | scrollDirection: Axis.horizontal, 79 | padding: const EdgeInsets.all(16.0), 80 | child: CustomText( 81 | _code, 82 | parserOptions: ParserOptions.external( 83 | (text) => parseLanguage(text, language: 'dart'), 84 | ), 85 | definitions: dartDefinitions, 86 | style: GoogleFonts.inconsolata( 87 | fontSize: 15.0, 88 | height: 1.2, 89 | ), 90 | ), 91 | ), 92 | ), 93 | ), 94 | ), 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /example/lib/common/device_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import 'package:device_info_plus/device_info_plus.dart'; 4 | 5 | // ignore: avoid_classes_with_only_static_members 6 | class DeviceInfo { 7 | static late bool isIosSimulator; 8 | 9 | static Future checkIosSimulator() async { 10 | if (defaultTargetPlatform != TargetPlatform.iOS) { 11 | isIosSimulator = false; 12 | return; 13 | } 14 | 15 | final deviceInfo = DeviceInfoPlugin(); 16 | final iosInfo = await deviceInfo.iosInfo; 17 | isIosSimulator = iosInfo.systemName == 'iOS' && !iosInfo.isPhysicalDevice; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/lib/common/highlight/dart_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | 5 | import 'package:custom_text_example/common/highlight/language_parser.dart'; 6 | 7 | const dartDefinitions = [ 8 | TextDefinition( 9 | matcher: KeywordMatcher(), 10 | matchStyle: TextStyle(color: Color(0xFFCC8833)), 11 | ), 12 | TextDefinition( 13 | matcher: MetaMatcher(), 14 | matchStyle: TextStyle(color: Color(0xFFCC8833)), 15 | ), 16 | TextDefinition( 17 | matcher: BuiltInMatcher(), 18 | matchStyle: TextStyle(color: Color(0xFF6CA332)), 19 | ), 20 | TextDefinition( 21 | matcher: TitleMatcher(), 22 | matchStyle: TextStyle(color: Color(0xFF6CA332)), 23 | ), 24 | TextDefinition( 25 | matcher: NumberMatcher(), 26 | matchStyle: TextStyle(color: Color(0xFFE91E63)), 27 | ), 28 | TextDefinition( 29 | matcher: StringMatcher(), 30 | matchStyle: TextStyle(color: Color(0xFF009688)), 31 | ), 32 | TextDefinition( 33 | matcher: SubstMatcher(), 34 | matchStyle: TextStyle(color: Color(0xFF795548)), 35 | ), 36 | TextDefinition( 37 | matcher: UrlMatcher(), 38 | matchStyle: TextStyle( 39 | color: Color(0xFF009688), 40 | decoration: TextDecoration.underline, 41 | ), 42 | hoverStyle: TextStyle( 43 | color: Color(0xFF009688), 44 | backgroundColor: Color(0x22009688), 45 | decoration: TextDecoration.underline, 46 | ), 47 | ), 48 | TextDefinition( 49 | matcher: CommentMatcher(), 50 | matchStyle: TextStyle(color: Color(0xFFBBBBBB)), 51 | ), 52 | TextDefinition( 53 | matcher: DocTagMatcher(), 54 | matchStyle: TextStyle(color: Color(0xFFAAAA33)), 55 | ), 56 | ]; 57 | -------------------------------------------------------------------------------- /example/lib/common/highlight/highlight.dart: -------------------------------------------------------------------------------- 1 | export 'package:custom_text_example/common/highlight/language_parser.dart'; 2 | 3 | export 'package:custom_text_example/common/highlight/dart_definitions.dart'; 4 | export 'package:custom_text_example/common/highlight/json_definitions.dart'; 5 | export 'package:custom_text_example/common/highlight/markdown_definitions.dart'; 6 | export 'package:custom_text_example/common/highlight/xml_definitions.dart'; 7 | -------------------------------------------------------------------------------- /example/lib/common/highlight/json_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | 5 | import 'package:custom_text_example/common/highlight/language_parser.dart'; 6 | 7 | const jsonDefinitions = [ 8 | TextDefinition( 9 | matcher: LiteralMatcher(), 10 | matchStyle: TextStyle(color: Color(0xFF66AAFF)), 11 | ), 12 | TextDefinition( 13 | matcher: AttrMatcher(), 14 | matchStyle: TextStyle(color: Color(0xFFDD6699)), 15 | ), 16 | TextDefinition( 17 | matcher: NumberMatcher(), 18 | matchStyle: TextStyle(color: Color(0xFF77AA33)), 19 | ), 20 | TextDefinition( 21 | matcher: StringMatcher(), 22 | matchStyle: TextStyle(color: Color(0xFFCC8833)), 23 | ), 24 | TextDefinition( 25 | matcher: UrlMatcher(), 26 | matchStyle: TextStyle( 27 | color: Color(0xFFCC8833), 28 | decoration: TextDecoration.underline, 29 | ), 30 | hoverStyle: TextStyle( 31 | color: Color(0xFFCC8833), 32 | backgroundColor: Color(0x22CC8833), 33 | decoration: TextDecoration.underline, 34 | ), 35 | ), 36 | ]; 37 | -------------------------------------------------------------------------------- /example/lib/common/highlight/language_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:custom_text/custom_text.dart'; 2 | import 'package:highlight/highlight.dart'; 3 | 4 | Future> parseLanguage( 5 | String text, { 6 | required String language, 7 | }) async { 8 | final result = highlight.parse(text, language: language); 9 | return _buildElements(result.nodes); 10 | } 11 | 12 | Future> _buildElements( 13 | List? nodes, [ 14 | int offset = 0, 15 | String? className, 16 | ]) async { 17 | if (nodes == null) { 18 | return []; 19 | } 20 | 21 | final elements = []; 22 | var currentOffset = offset; 23 | 24 | for (final node in nodes) { 25 | if (node.children == null) { 26 | final value = node.value; 27 | if (value != null && value.isNotEmpty) { 28 | if (className != 'code' || value.startsWith('```')) { 29 | elements.addAll( 30 | await _buildValueElements( 31 | value, 32 | currentOffset, 33 | className == 'code' ? 'code_block' : className, 34 | ), 35 | ); 36 | } else { 37 | elements.add( 38 | TextElement( 39 | value, 40 | matcherType: CodeMatcher, 41 | offset: currentOffset, 42 | ), 43 | ); 44 | } 45 | } 46 | } else { 47 | elements.addAll( 48 | await _buildElements(node.children, currentOffset, node.className), 49 | ); 50 | } 51 | if (elements.isNotEmpty) { 52 | currentOffset = elements.last.offset + elements.last.text.length; 53 | } 54 | } 55 | 56 | return elements; 57 | } 58 | 59 | Future> _buildValueElements( 60 | String text, 61 | int offset, 62 | String? className, 63 | ) async { 64 | final parser = TextParser(matchers: const [UrlMatcher()]); 65 | var elements = await parser.parse(text, useIsolate: false); 66 | 67 | final matcherType = _mappings[className] ?? TextMatcher; 68 | if (!elements.containsMatcherType()) { 69 | return [TextElement(text, matcherType: matcherType, offset: offset)]; 70 | } 71 | 72 | elements = elements.reassignOffsets(startingOffset: offset).toList(); 73 | return [ 74 | for (final elm in elements) 75 | if (elm.matcherType == UrlMatcher) 76 | elm 77 | else 78 | elm.copyWith(matcherType: matcherType), 79 | ]; 80 | } 81 | 82 | const _mappings = { 83 | 'keyword': KeywordMatcher, 84 | 'built_in': BuiltInMatcher, 85 | 'meta': MetaMatcher, 86 | 'title': TitleMatcher, 87 | 'literal': LiteralMatcher, 88 | 'number': NumberMatcher, 89 | 'string': StringMatcher, 90 | 'subst': SubstMatcher, 91 | 'comment': CommentMatcher, 92 | 'doctag': DocTagMatcher, 93 | 'params': ParamsMatcher, 94 | 'tag': TagMatcher, 95 | 'name': NameMatcher, 96 | 'attr': AttrMatcher, 97 | 'section': SectionMatcher, 98 | 'strong': StrongMatcher, 99 | 'emphasis': EmphasisMatcher, 100 | 'bullet': BulletMatcher, 101 | 'symbol': SymbolMatcher, 102 | 'link': UrlMatcher, 103 | 'quote': QuoteMatcher, 104 | 'code': CodeMatcher, 105 | 'code_block': CodeBlockMatcher, 106 | }; 107 | 108 | class KeywordMatcher extends TextMatcher { 109 | const KeywordMatcher() : super(''); 110 | } 111 | 112 | class BuiltInMatcher extends TextMatcher { 113 | const BuiltInMatcher() : super(''); 114 | } 115 | 116 | class MetaMatcher extends TextMatcher { 117 | const MetaMatcher() : super(''); 118 | } 119 | 120 | class TitleMatcher extends TextMatcher { 121 | const TitleMatcher() : super(''); 122 | } 123 | 124 | class LiteralMatcher extends TextMatcher { 125 | const LiteralMatcher() : super(''); 126 | } 127 | 128 | class NumberMatcher extends TextMatcher { 129 | const NumberMatcher() : super(''); 130 | } 131 | 132 | class StringMatcher extends TextMatcher { 133 | const StringMatcher() : super(''); 134 | } 135 | 136 | class SubstMatcher extends TextMatcher { 137 | const SubstMatcher() : super(''); 138 | } 139 | 140 | class CommentMatcher extends TextMatcher { 141 | const CommentMatcher() : super(''); 142 | } 143 | 144 | class DocTagMatcher extends TextMatcher { 145 | const DocTagMatcher() : super(''); 146 | } 147 | 148 | class ParamsMatcher extends TextMatcher { 149 | const ParamsMatcher() : super(''); 150 | } 151 | 152 | class TagMatcher extends TextMatcher { 153 | const TagMatcher() : super(''); 154 | } 155 | 156 | class NameMatcher extends TextMatcher { 157 | const NameMatcher() : super(''); 158 | } 159 | 160 | class AttrMatcher extends TextMatcher { 161 | const AttrMatcher() : super(''); 162 | } 163 | 164 | class SectionMatcher extends TextMatcher { 165 | const SectionMatcher() : super(''); 166 | } 167 | 168 | class StrongMatcher extends TextMatcher { 169 | const StrongMatcher() : super(''); 170 | } 171 | 172 | class EmphasisMatcher extends TextMatcher { 173 | const EmphasisMatcher() : super(''); 174 | } 175 | 176 | class BulletMatcher extends TextMatcher { 177 | const BulletMatcher() : super(''); 178 | } 179 | 180 | class SymbolMatcher extends TextMatcher { 181 | const SymbolMatcher() : super(''); 182 | } 183 | 184 | class LinkMatcher extends TextMatcher { 185 | const LinkMatcher() : super(''); 186 | } 187 | 188 | class QuoteMatcher extends TextMatcher { 189 | const QuoteMatcher() : super(''); 190 | } 191 | 192 | class CodeMatcher extends TextMatcher { 193 | const CodeMatcher() : super(''); 194 | } 195 | 196 | class CodeBlockMatcher extends TextMatcher { 197 | const CodeBlockMatcher() : super(''); 198 | } 199 | -------------------------------------------------------------------------------- /example/lib/common/highlight/markdown_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:custom_text/custom_text.dart'; 2 | import 'package:custom_text_example/common/highlight/language_parser.dart'; 3 | import 'package:flutter/painting.dart'; 4 | 5 | const markdownDefinitions = [ 6 | TextDefinition( 7 | matcher: SectionMatcher(), 8 | matchStyle: TextStyle( 9 | fontSize: 18.0, 10 | fontWeight: FontWeight.bold, 11 | ), 12 | ), 13 | TextDefinition( 14 | matcher: StrongMatcher(), 15 | matchStyle: TextStyle(fontWeight: FontWeight.bold), 16 | ), 17 | TextDefinition( 18 | matcher: EmphasisMatcher(), 19 | matchStyle: TextStyle(fontStyle: FontStyle.italic), 20 | ), 21 | TextDefinition( 22 | matcher: BulletMatcher(), 23 | matchStyle: TextStyle( 24 | color: Color(0xFFDD6699), 25 | fontWeight: FontWeight.bold, 26 | ), 27 | ), 28 | TextDefinition( 29 | matcher: SymbolMatcher(), 30 | matchStyle: TextStyle(color: Color(0xFFE91E63)), 31 | ), 32 | TextDefinition( 33 | matcher: StringMatcher(), 34 | matchStyle: TextStyle(color: Color(0xFF6CA332)), 35 | ), 36 | TextDefinition( 37 | matcher: QuoteMatcher(), 38 | matchStyle: TextStyle(color: Color(0xFF009688)), 39 | ), 40 | TextDefinition( 41 | matcher: CodeMatcher(), 42 | matchStyle: TextStyle(backgroundColor: Color(0x22000000)), 43 | ), 44 | TextDefinition( 45 | matcher: CodeBlockMatcher(), 46 | matchStyle: TextStyle(color: Color(0xFF795548)), 47 | ), 48 | TextDefinition( 49 | matcher: UrlMatcher(), 50 | matchStyle: TextStyle( 51 | color: Color(0xFFCC8833), 52 | decoration: TextDecoration.underline, 53 | ), 54 | hoverStyle: TextStyle( 55 | color: Color(0xFFCC8833), 56 | backgroundColor: Color(0x22CC8833), 57 | decoration: TextDecoration.underline, 58 | ), 59 | ), 60 | ]; 61 | -------------------------------------------------------------------------------- /example/lib/common/highlight/xml_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | 5 | import 'package:custom_text_example/common/highlight/language_parser.dart'; 6 | 7 | const xmlDefinitions = [ 8 | TextDefinition( 9 | matcher: TagMatcher(), 10 | matchStyle: TextStyle(color: Color(0xFF66AAFF)), 11 | ), 12 | TextDefinition( 13 | matcher: NameMatcher(), 14 | matchStyle: TextStyle(color: Color(0xFF77AA33)), 15 | ), 16 | TextDefinition( 17 | matcher: AttrMatcher(), 18 | matchStyle: TextStyle(color: Color(0xFFDD6699)), 19 | ), 20 | TextDefinition( 21 | matcher: StringMatcher(), 22 | matchStyle: TextStyle(color: Color(0xFFCC8833)), 23 | ), 24 | TextDefinition( 25 | matcher: UrlMatcher(), 26 | matchStyle: TextStyle( 27 | color: Color(0xFFCC8833), 28 | decoration: TextDecoration.underline, 29 | ), 30 | hoverStyle: TextStyle( 31 | color: Color(0xFFCC8833), 32 | backgroundColor: Color(0x22CC8833), 33 | decoration: TextDecoration.underline, 34 | ), 35 | ), 36 | TextDefinition( 37 | matcher: CommentMatcher(), 38 | matchStyle: TextStyle(color: Color(0xFFBBBBBB)), 39 | ), 40 | ]; 41 | -------------------------------------------------------------------------------- /example/lib/common/popup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | import 'package:positioned_popup/positioned_popup.dart'; 5 | 6 | export 'package:positioned_popup/positioned_popup.dart'; 7 | 8 | extension ShowPopup on PopupController { 9 | void show(BuildContext context, GestureDetails details) { 10 | PopupArea.of(context).open( 11 | controller: this, 12 | popupKey: ValueKey(details.shownText), 13 | position: details.globalPosition, 14 | maxWidth: 260.0, 15 | barrierDismissible: false, 16 | autoCloseWait: const Duration(milliseconds: 200), 17 | child: GestureDetector( 18 | onTap: close, 19 | child: Card( 20 | elevation: 4.0, 21 | child: SingleChildScrollView( 22 | padding: const EdgeInsets.all(12.0), 23 | child: CustomText( 24 | _kPopupMessages[details.shownText] ?? '', 25 | definitions: [ 26 | SelectiveDefinition( 27 | matcher: const PatternMatcher(r'\*\*(.+?)\*\*'), 28 | shownText: (element) => element.groups.first!, 29 | ), 30 | ], 31 | style: const TextStyle(fontSize: 14.0), 32 | matchStyle: const TextStyle(fontWeight: FontWeight.bold), 33 | maxLines: 6, 34 | overflow: TextOverflow.ellipsis, 35 | ), 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | 42 | void toggle(BuildContext context, GestureDetails details) { 43 | if (popupKey == ValueKey(details.shownText)) { 44 | close(); 45 | } else { 46 | show(context, details); 47 | } 48 | } 49 | } 50 | 51 | const _kPopupMessages = { 52 | 'open-source': '**Open-source software (OSS)** is computer software that ' 53 | 'is released under a license in which the copyright holder grants users ' 54 | 'the rights to use, study, change, and distribute the software and its ' 55 | 'source code to anyone and for any purpose.', 56 | 'UI': 'In the industrial design field of human–computer interaction, a ' 57 | '**user interface (UI)** is the space where interactions between humans ' 58 | 'and machines occur. The goal of this interaction is to allow effective ' 59 | 'operation and control of the machine from the human end.', 60 | 'software development kit': 'A **software development kit (SDK)** is a ' 61 | 'collection of software development tools in one installable package. ' 62 | 'They facilitate the creation of applications by having a compiler, ' 63 | 'debugger and sometimes a software framework.', 64 | 'Google': '**Google LLC** is an American multinational technology company ' 65 | 'focusing on online advertising, search engine technology, cloud ' 66 | 'computing, computer software, quantum computing, e-commerce, ' 67 | 'artificial intelligence, and consumer electronics.', 68 | 'cross-platform applications': 'In computing, **cross-platform software** ' 69 | '(also called **multi-platform software**, **platform-agnostic ' 70 | 'software**, or **platform-independent software**) is computer software ' 71 | 'that is designed to work in several computing platforms.', 72 | 'Android': '**Android** is a mobile operating system based on a modified ' 73 | 'version of the Linux kernel and other open-source software, designed ' 74 | 'primarily for touchscreen mobile devices such as smartphones and ' 75 | 'tablets.', 76 | 'iOS': '**iOS** (formerly iPhone OS) is a mobile operating system developed ' 77 | 'by Apple Inc. exclusively for its hardware. It is the operating system ' 78 | "that powers many of the company's mobile devices.", 79 | 'Linux': '**Linux** is a family of open-source Unix-like operating systems ' 80 | 'based on the Linux kernel, an operating system kernel first released ' 81 | 'on September 17, 1991, by Linus Torvalds.', 82 | 'macOS': '**macOS** is a Unix operating system developed and marketed by ' 83 | "Apple Inc. since 2001. It is the primary operating system for Apple's " 84 | 'Mac computers.', 85 | 'Windows': '**Windows** is a group of several proprietary graphical ' 86 | 'operating system families developed and marketed by Microsoft. Each ' 87 | 'family caters to a certain sector of the computing industry.', 88 | 'Google Fuchsia': '**Fuchsia** is an open-source capability-based operating ' 89 | "system developed by Google. In contrast to Google's Linux-based " 90 | 'operating systems such as ChromeOS and Android, Fuchsia is based on ' 91 | 'a custom kernel named Zircon.', 92 | 'web': 'The **Web platform** is a collection of technologies developed as ' 93 | 'open standards by the World Wide Web Consortium and other ' 94 | 'standardization bodies such as the Web Hypertext Application ' 95 | 'Technology Working Group, the Unicode Consortium,', 96 | 'codebase': 'In software development, a **codebase** (or **code base**) ' 97 | 'is a collection of source code used to build a particular software ' 98 | 'system, application, or software component.', 99 | }; 100 | -------------------------------------------------------------------------------- /example/lib/examples/advanced/fill_in_blanks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | 5 | class FillInBlanksExample extends StatelessWidget { 6 | const FillInBlanksExample(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return const FillInBlanks( 11 | 'O Romeo, Romeo, {{wherefore}} {{art}} {{thou}} Romeo? ' 12 | 'Deny {{thy}} father and refuse {{thy}} name; ' 13 | 'Or, if {{thou}} {{wilt}} not, be but sworn my love, ' 14 | "And I'll no longer be a Capulet.", 15 | ); 16 | } 17 | } 18 | 19 | class FillInBlanks extends StatelessWidget { 20 | const FillInBlanks(this.text); 21 | 22 | final String text; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | const hoverColor = Colors.green; 27 | 28 | return CustomText( 29 | text, 30 | style: const TextStyle(fontSize: 24.0), 31 | definitions: [ 32 | SpanDefinition( 33 | matcher: const PatternMatcher('{{(.+?)}}'), 34 | hoverStyle: const TextStyle( 35 | color: hoverColor, 36 | fontWeight: FontWeight.bold, 37 | ), 38 | builder: (element) { 39 | return WidgetSpan( 40 | baseline: TextBaseline.alphabetic, 41 | alignment: PlaceholderAlignment.baseline, 42 | child: Builder( 43 | builder: (context) { 44 | final style = DefaultTextStyle.of(context).style; 45 | return AnimatedSwitcher( 46 | duration: const Duration(milliseconds: 150), 47 | child: style.color == hoverColor 48 | ? Text(element.groups.first!) 49 | : ColoredBox( 50 | color: Colors.teal.shade300, 51 | child: Text( 52 | element.groups.first!, 53 | style: const TextStyle( 54 | color: Colors.transparent, 55 | fontWeight: FontWeight.bold, 56 | ), 57 | textHeightBehavior: const TextHeightBehavior( 58 | applyHeightToFirstAscent: false, 59 | applyHeightToLastDescent: false, 60 | ), 61 | ), 62 | ), 63 | ); 64 | }, 65 | ), 66 | ); 67 | }, 68 | onTap: (_) { 69 | // no-op 70 | // This callback is only necessary to make hoverStyle 71 | // applied on tap on mobile devices. 72 | }, 73 | ), 74 | ], 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/lib/examples/basic/custom_pattern.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class CustomPatternExample extends StatelessWidget { 5 | const CustomPatternExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CustomText( 12 | 'Hello world! #CustomText', 13 | definitions: const [ 14 | TextDefinition( 15 | // Matcher for hashtags (provided a hashtag is defined as a string 16 | // that starts with "#" followed by an alphabet and then alpha- 17 | // numerics, and is enclosed with white spaces) 18 | matcher: PatternMatcher(r'#[a-zA-Z][a-zA-Z0-9]{1,}(?=\s|$)'), 19 | // TODO: Replace above with below if Safari supports lookbehind. 20 | // matcher: 21 | // PatternMatcher(r'(?<=\s|^)\#[a-zA-Z][a-zA-Z0-9]{1,}(?=\s|$)'), 22 | ), 23 | ], 24 | matchStyle: const TextStyle(color: Colors.lightBlue), 25 | tapStyle: const TextStyle(color: Colors.lightGreen), 26 | onTap: (details) => output(details.actionText), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/lib/examples/basic/external_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | import 'package:custom_text_example/common/highlight/language_parser.dart'; 6 | import 'package:custom_text_example/examples/utils/external_parser/segmented_buttons.dart'; 7 | import 'package:custom_text_example/examples/utils/external_parser/settings.dart'; 8 | 9 | class ExternalParserExample extends StatefulWidget { 10 | const ExternalParserExample(); 11 | 12 | @override 13 | State createState() => _ExternalParserExampleState(); 14 | } 15 | 16 | class _ExternalParserExampleState extends State { 17 | late LanguageType _languageType; 18 | CustomTextEditingController? _controller; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _onLanguageChanged(LanguageType.values.first); 24 | } 25 | 26 | @override 27 | void dispose() { 28 | _controller?.dispose(); 29 | super.dispose(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Buttons( 38 | selected: _languageType, 39 | onChanged: (type) { 40 | setState(() => _onLanguageChanged(type)); 41 | }, 42 | ), 43 | const SizedBox(height: 8.0), 44 | TextField( 45 | controller: _controller, 46 | maxLines: null, 47 | style: GoogleFonts.inconsolata(height: 1.4), 48 | ), 49 | ], 50 | ); 51 | } 52 | 53 | void _onLanguageChanged(LanguageType type) { 54 | _languageType = type; 55 | 56 | _controller?.dispose(); 57 | _controller = CustomTextEditingController( 58 | text: type.sourceText, 59 | definitions: type.definitions, 60 | parserOptions: ParserOptions.external( 61 | (text) => parseLanguage(text, language: type.name), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/lib/examples/basic/hover_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class HoverStyleExample extends StatelessWidget { 5 | const HoverStyleExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CustomText( 12 | 'URL: https://example.com/\n' 13 | 'Email: foo@example.com', 14 | definitions: [ 15 | const TextDefinition( 16 | matcher: UrlMatcher(), 17 | matchStyle: TextStyle( 18 | color: Colors.grey, 19 | decoration: TextDecoration.lineThrough, 20 | ), 21 | // `SystemMouseCursors.forbidden` is used for URLs. 22 | mouseCursor: SystemMouseCursors.forbidden, 23 | ), 24 | TextDefinition( 25 | matcher: const EmailMatcher(), 26 | matchStyle: const TextStyle( 27 | color: Colors.lightBlue, 28 | decoration: TextDecoration.underline, 29 | ), 30 | tapStyle: const TextStyle(color: Colors.green), 31 | // Text is shadowed while the mouse pointer hovers over it. 32 | hoverStyle: TextStyle( 33 | color: Colors.lightBlue, 34 | shadows: [_shadow(Colors.lightBlue)], 35 | ), 36 | // `SystemMouseCursors.click` is automatically used for 37 | // tappable elements even if `mouseCursor` is not specified. 38 | onTap: (details) => output(details.actionText), 39 | ), 40 | ], 41 | ); 42 | } 43 | 44 | Shadow _shadow(MaterialColor color) { 45 | return Shadow( 46 | offset: const Offset(2.0, 2.0), 47 | blurRadius: 4.0, 48 | color: color.shade400, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/lib/examples/basic/on_gesture.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | import 'package:custom_text_example/common/popup.dart'; 5 | 6 | class OnGestureExample extends StatefulWidget { 7 | const OnGestureExample(this.output); 8 | 9 | final void Function(String) output; 10 | 11 | @override 12 | State createState() => _OnGestureExampleState(); 13 | } 14 | 15 | class _OnGestureExampleState extends State { 16 | late final _controller = PopupController(); 17 | 18 | @override 19 | void dispose() { 20 | _controller.dispose(); 21 | super.dispose(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | const matchStyle = TextStyle(color: Color(0xFF3366CC)); 27 | 28 | return CustomText( 29 | 'Flutter is an [open-source]() [UI]() [software development kit]() ' 30 | 'created by [Google](). It is used to develop [cross-platform ' 31 | 'applications]() for [Android](), [iOS](), [Linux](), [macOS](), ' 32 | '[Windows](), [Google Fuchsia](), and the [web]() from a single ' 33 | '[codebase]().', 34 | definitions: [ 35 | TextDefinition( 36 | matcher: ExactMatcher(const ['Flutter']), 37 | matchStyle: const TextStyle(fontWeight: FontWeight.bold), 38 | ), 39 | SelectiveDefinition( 40 | matcher: const LinkMatcher(), 41 | shownText: (element) => element.groups.first!, 42 | matchStyle: matchStyle, 43 | hoverStyle: matchStyle.copyWith( 44 | decoration: TextDecoration.underline, 45 | ), 46 | onTap: (details) { 47 | _output(details); 48 | _controller.toggle(context, details); 49 | }, 50 | onGesture: (details) { 51 | _output(details); 52 | if (details.gestureKind == GestureKind.enter) { 53 | _controller.show(context, details); 54 | } else if (details.gestureKind == GestureKind.exit) { 55 | _controller.startCloseTimer(const Duration(milliseconds: 200)); 56 | } 57 | }, 58 | ), 59 | ], 60 | ); 61 | } 62 | 63 | void _output(GestureDetails details) { 64 | final x = details.globalPosition.dx.roundToDouble(); 65 | final y = details.globalPosition.dy.roundToDouble(); 66 | widget.output( 67 | '${details.shownText}\n ${details.gestureKind.name} ($x, $y)', 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/lib/examples/basic/overwriting_pattern.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class OverwritingPatternExample extends StatelessWidget { 5 | const OverwritingPatternExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CustomText( 12 | 'Tel: +1-012-3456-7890', 13 | definitions: const [ 14 | TextDefinition(matcher: TelMatcher(r'\d{3}-\d{4}-\d{4}')), 15 | ], 16 | matchStyle: const TextStyle(color: Colors.lightBlue), 17 | tapStyle: const TextStyle(color: Colors.lightGreen), 18 | onTap: (details) => output(details.actionText), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/lib/examples/basic/pre_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class PreBuilderExample extends StatelessWidget { 5 | const PreBuilderExample(); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return CustomText( 10 | 'KISS is an acronym for "Keep It Simple, Stupid!".', 11 | definitions: const [ 12 | TextDefinition( 13 | // This pattern is used for parsing the TextSpan built 14 | // by preBuilder, not for the original text. 15 | matcher: PatternMatcher('[A-Z]'), 16 | matchStyle: TextStyle(color: Colors.red), 17 | ), 18 | ], 19 | preBuilder: CustomSpanBuilder( 20 | definitions: [ 21 | const TextDefinition( 22 | // This pattern is used for parsing the original text. 23 | matcher: PatternMatcher('KISS|Keep.+Stupid!'), 24 | matchStyle: TextStyle(fontWeight: FontWeight.bold), 25 | ), 26 | ], 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/examples/basic/real_hyperlinks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | import 'package:url_launcher/link.dart'; 4 | 5 | class RealHyperlinksExample extends StatelessWidget { 6 | const RealHyperlinksExample(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | const matchStyle = TextStyle(color: Colors.blue); 11 | 12 | return CustomText( 13 | 'Please visit [pub.dev](https://pub.dev/packages/custom_text) or ' 14 | '[GitHub](https://github.com/kaboc/flutter_custom_text) for more ' 15 | 'details of this package.', 16 | definitions: [ 17 | SpanDefinition( 18 | matcher: const LinkMatcher(), 19 | builder: (element) { 20 | return WidgetSpan( 21 | // The baseline and alignment settings are necessary to 22 | // vertically align WidgetSpans with TextSpans when the text 23 | // consists of alphabets. 24 | // For non-alphabets, you can use `TextBaseline.ideographic`. 25 | // However, due to issues in the Flutter SDK, it is not easy 26 | // to align strings if the text contains both alphabet and 27 | // ideographic characters, . 28 | baseline: TextBaseline.alphabetic, 29 | alignment: PlaceholderAlignment.baseline, 30 | child: Link( 31 | uri: Uri.parse(element.groups[1]!), 32 | target: LinkTarget.blank, 33 | builder: (context, openLink) { 34 | return GestureDetector( 35 | onTap: openLink, 36 | child: Text(element.groups[0]!), 37 | ); 38 | }, 39 | ), 40 | ); 41 | }, 42 | matchStyle: matchStyle, 43 | hoverStyle: matchStyle.copyWith( 44 | decoration: TextDecoration.underline, 45 | ), 46 | mouseCursor: SystemMouseCursors.click, 47 | onTap: (_) { 48 | // no-op 49 | // This callback is only necessary to make hoverStyle 50 | // applied on tap on mobile devices. 51 | }, 52 | ), 53 | ], 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/lib/examples/basic/selective_definition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class SelectiveDefinitionExample extends StatelessWidget { 5 | const SelectiveDefinitionExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | const matchStyle = TextStyle(color: Colors.blue); 12 | 13 | return CustomText( 14 | 'Tap [here](Tapped!)', 15 | definitions: [ 16 | SelectiveDefinition( 17 | matcher: const LinkMatcher(), 18 | // `shownText` is used to choose the string to display. 19 | shownText: (element) => element.groups[0]!, 20 | // `actionText` is used to choose the string to be passed 21 | // to the `onTap`, `onLongPress` and `onGesture` handlers. 22 | actionText: (element) => element.groups[1]!, 23 | ), 24 | ], 25 | matchStyle: matchStyle, 26 | hoverStyle: matchStyle.copyWith(decoration: TextDecoration.underline), 27 | onTap: (details) => output(details.actionText), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/examples/basic/simple.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class SimpleExample extends StatelessWidget { 5 | const SimpleExample(); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const CustomText( 10 | 'URL: https://example.com/\n' 11 | 'Email: foo@example.com', 12 | definitions: [ 13 | TextDefinition(matcher: UrlMatcher()), 14 | TextDefinition(matcher: EmailMatcher()), 15 | ], 16 | matchStyle: TextStyle(color: Colors.lightBlue), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/lib/examples/basic/span_definition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class SpanDefinitionExample extends StatelessWidget { 5 | const SpanDefinitionExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | const matchStyle = TextStyle(color: Colors.blue); 12 | 13 | return CustomText( 14 | 'Hover and click >> @@Flutter', 15 | definitions: [ 16 | SpanDefinition( 17 | matcher: const PatternMatcher('>>'), 18 | builder: (element) => const WidgetSpan( 19 | child: Icon( 20 | Icons.keyboard_double_arrow_right, 21 | color: Colors.grey, 22 | size: 18.0, 23 | ), 24 | ), 25 | ), 26 | SpanDefinition( 27 | matcher: const PatternMatcher(r'@@(\w+)'), 28 | builder: (element) => TextSpan( 29 | children: [ 30 | const WidgetSpan(child: FlutterLogo()), 31 | const WidgetSpan(child: SizedBox(width: 2.0)), 32 | TextSpan(text: element.groups.first), 33 | ], 34 | ), 35 | matchStyle: matchStyle, 36 | hoverStyle: matchStyle.copyWith(decoration: TextDecoration.underline), 37 | onTap: (details) => output(details.element.groups.first!), 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/examples/basic/spans_constructor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class CustomTextSpansExample extends StatelessWidget { 5 | const CustomTextSpansExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CustomText.spans( 12 | style: const TextStyle(fontSize: 40.0), 13 | definitions: [ 14 | TextDefinition( 15 | // WidgetSpan is matched by `\uFFFC` or `.` in a match pattern. 16 | matcher: const PatternMatcher('Flutter devs\uFFFC'), 17 | matchStyle: const TextStyle(color: Colors.blue), 18 | hoverStyle: TextStyle(color: Colors.blue.shade300), 19 | mouseCursor: SystemMouseCursors.forbidden, 20 | onGesture: (details) => output(details.gestureKind.name), 21 | ), 22 | ], 23 | spans: [ 24 | const TextSpan(text: 'Hi, '), 25 | const TextSpan( 26 | text: 'Flutter', 27 | style: TextStyle( 28 | fontWeight: FontWeight.bold, 29 | shadows: [Shadow(blurRadius: 4.0, color: Colors.cyan)], 30 | ), 31 | ), 32 | const TextSpan(text: ' devs'), 33 | WidgetSpan( 34 | alignment: PlaceholderAlignment.middle, 35 | child: Builder( 36 | builder: (context) { 37 | // Text style is available also in WidgetSpan 38 | // via DefaultTextStyle. 39 | final style = DefaultTextStyle.of(context).style; 40 | return Icon( 41 | Icons.flutter_dash, 42 | size: style.fontSize, 43 | color: style.color, 44 | ); 45 | }, 46 | ), 47 | ), 48 | ], 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/lib/examples/basic/styles_and_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class StylesAndActionsExample extends StatelessWidget { 5 | const StylesAndActionsExample(this.output); 6 | 7 | final void Function(String) output; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CustomText( 12 | 'URL: https://example.com/\n' 13 | 'Email: foo@example.com\n' 14 | 'Tel: +1-012-3456-7890', 15 | definitions: [ 16 | const TextDefinition(matcher: UrlMatcher()), 17 | const TextDefinition(matcher: EmailMatcher()), 18 | TextDefinition( 19 | // TODO: Use default TelMatcher if Safari supports lookbehind. 20 | matcher: const TelMatcher(r'(?:\+?[1-9]\d{0,4})?(?:[- ]?\d{1,4})+'), 21 | // Styles and handlers specified in a definition take 22 | // precedence over the equivalent arguments of CustomText. 23 | matchStyle: const TextStyle( 24 | color: Colors.green, 25 | decoration: TextDecoration.underline, 26 | ), 27 | tapStyle: const TextStyle(color: Colors.orange), 28 | onTap: (details) => output(details.actionText), 29 | onLongPress: (details) => 30 | output('[Long press on Tel#] ${details.actionText}'), 31 | ), 32 | ], 33 | matchStyle: const TextStyle( 34 | color: Colors.lightBlue, 35 | decoration: TextDecoration.underline, 36 | ), 37 | tapStyle: const TextStyle(color: Colors.indigo), 38 | onTap: (details) => output(details.actionText), 39 | onLongPress: (details) => output('[Long press] ${details.actionText}'), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/examples/basic/text_editing_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:custom_text/custom_text.dart'; 3 | 4 | class ControllerExample extends StatefulWidget { 5 | const ControllerExample([this.output]); 6 | 7 | final void Function(String)? output; 8 | 9 | @override 10 | State createState() => _ControllerExampleState(); 11 | } 12 | 13 | class _ControllerExampleState extends State { 14 | late final _controller = CustomTextEditingController( 15 | text: 'abcde foo@example.com\nhttps://example.com/ #hashtag', 16 | definitions: [ 17 | TextDefinition( 18 | matcher: const UrlMatcher(), 19 | matchStyle: const TextStyle( 20 | color: Colors.blue, 21 | decoration: TextDecoration.underline, 22 | ), 23 | tapStyle: const TextStyle(color: Colors.indigo), 24 | onTap: widget.output == null 25 | ? null 26 | : (details) => widget.output!(details.actionText), 27 | onLongPress: widget.output == null 28 | ? null 29 | : (details) => widget.output!('long: ${details.actionText}'), 30 | ), 31 | TextDefinition( 32 | matcher: const EmailMatcher(), 33 | matchStyle: TextStyle( 34 | color: Colors.green, 35 | backgroundColor: Colors.lightGreen.withOpacity(0.2), 36 | ), 37 | ), 38 | const TextDefinition( 39 | matcher: PatternMatcher(r'#[a-zA-Z][a-zA-Z0-9]{1,}(?=\s|$)'), 40 | matchStyle: TextStyle(color: Colors.orange), 41 | hoverStyle: TextStyle(color: Colors.red), 42 | ), 43 | ], 44 | ); 45 | 46 | @override 47 | void dispose() { 48 | _controller.dispose(); 49 | super.dispose(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | const debounceDuration = Duration(seconds: 1); 55 | 56 | return Column( 57 | children: [ 58 | TextField( 59 | controller: _controller, 60 | maxLines: null, 61 | style: const TextStyle(height: 1.4), 62 | ), 63 | Row( 64 | mainAxisAlignment: MainAxisAlignment.end, 65 | children: [ 66 | const Text( 67 | 'Debounce\n(experimental)', 68 | style: TextStyle(fontSize: 11.0, height: 1.1), 69 | textAlign: TextAlign.center, 70 | ), 71 | Switch( 72 | value: _controller.debounceDuration != null, 73 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 74 | onChanged: (on) { 75 | setState(() { 76 | _controller.debounceDuration = on ? debounceDuration : null; 77 | }); 78 | }, 79 | ), 80 | ], 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/lib/examples/utils/external_parser/segmented_buttons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:custom_text_example/examples/utils/external_parser/settings.dart'; 5 | 6 | class Buttons extends StatelessWidget { 7 | const Buttons({required this.selected, required this.onChanged}); 8 | 9 | final LanguageType selected; 10 | final ValueChanged onChanged; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return DefaultTextStyle( 15 | style: const TextStyle(fontSize: 14.0), 16 | child: CupertinoSegmentedControl( 17 | groupValue: selected, 18 | padding: EdgeInsets.zero, 19 | selectedColor: Theme.of(context).colorScheme.secondary, 20 | borderColor: Theme.of(context).colorScheme.secondary, 21 | onValueChanged: onChanged, 22 | children: { 23 | for (final type in LanguageType.values) 24 | type: Padding( 25 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 26 | child: Text(type.label), 27 | ), 28 | }, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/examples/utils/external_parser/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:custom_text/custom_text.dart'; 2 | 3 | import 'package:custom_text_example/common/highlight/highlight.dart'; 4 | 5 | enum LanguageType { 6 | dart('Dart', _dart, dartDefinitions), 7 | json('JSON', _json, jsonDefinitions), 8 | xml('XML', _xml, xmlDefinitions), 9 | markdown('Markdown', _markdown, markdownDefinitions); 10 | 11 | const LanguageType(this.label, this.sourceText, this.definitions); 12 | 13 | final String label; 14 | final String sourceText; 15 | final List definitions; 16 | } 17 | 18 | const _dart = r''' 19 | void main() { 20 | final number = 123; 21 | final result = multiply(number); 22 | 23 | print('$number * 2 = $result'); 24 | } 25 | 26 | int multiply(int n) { 27 | return n * 2; 28 | } 29 | '''; 30 | 31 | const _json = ''' 32 | { 33 | "version": 1.0, 34 | "users": [ 35 | { 36 | "name": "Claire", 37 | "student": true 38 | }, 39 | { 40 | "name": "Daniel", 41 | "student": false, 42 | "attributes": [ "MSc", null, 2023 ] 43 | } 44 | ] 45 | } 46 | '''; 47 | 48 | const _xml = ''' 49 | 50 | 51 | 52 | Packages 53 | https://pub.dev/ 54 | The official package repository. 55 | 56 | Publisher 57 | https://pub.dev/publishers/kaboc.cc/packages 58 | 59 | 60 | This package 61 | https://pub.dev/packages/custom_text 62 | 63 | 64 | 65 | '''; 66 | 67 | const _markdown = ''' 68 | # Heading 69 | 70 | - Pizza 71 | - Cheese 72 | - Tomato 73 | - Pasta 74 | 75 | 1. Orange 76 | 2. Peach 77 | 78 | [link](https://example.com/) 79 | 80 | **Bold** 81 | *Italic* 82 | 83 | > Quote 84 | 85 | This is `inline code`. 86 | 87 | ``` 88 | // Code block 89 | void main() { 90 | print('Hello world!'); 91 | } 92 | ``` 93 | '''; 94 | -------------------------------------------------------------------------------- /example/lib/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:go_router/go_router.dart'; 4 | 5 | import 'package:custom_text_example/routes.dart'; 6 | import 'package:custom_text_example/widgets/hyperlink.dart'; 7 | 8 | class HomePage extends StatelessWidget { 9 | const HomePage({required this.child}); 10 | 11 | final Widget child; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final state = GoRouterState.of(context); 16 | final type = ViewType.of(state); 17 | 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: switch (type) { 21 | ViewType.basic => const Text('Basic Usage'), 22 | ViewType.advanced => const Text('Advanced examples'), 23 | }, 24 | actions: const [ 25 | Center( 26 | child: Hyperlink( 27 | label: 'pub.dev', 28 | url: 'https://pub.dev/packages/custom_text', 29 | ), 30 | ), 31 | SizedBox(width: 16.0), 32 | ], 33 | ), 34 | body: SafeArea( 35 | child: child, 36 | ), 37 | bottomNavigationBar: NavigationBar( 38 | selectedIndex: type.index, 39 | onDestinationSelected: (index) { 40 | switch (index) { 41 | case 0: 42 | const BasicViewRoute().go(context); 43 | case 1: 44 | const AdvancedViewRoute().go(context); 45 | } 46 | }, 47 | destinations: const [ 48 | NavigationDestination( 49 | icon: Icon(Icons.school), 50 | label: 'Basic', 51 | ), 52 | NavigationDestination( 53 | icon: Icon(Icons.rocket_launch), 54 | label: 'Advanced', 55 | ), 56 | ], 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:go_router/go_router.dart'; 4 | 5 | import 'package:custom_text_example/common/device_info.dart'; 6 | import 'package:custom_text_example/routes.dart'; 7 | 8 | Future main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | await DeviceInfo.checkIosSimulator(); 11 | runApp(App()); 12 | } 13 | 14 | class App extends StatelessWidget { 15 | final _router = GoRouter( 16 | routes: $appRoutes, 17 | navigatorKey: rootNavigatorKey, 18 | onException: (context, state, router) => router.go('/'), 19 | ); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final colorScheme = ColorScheme.fromSeed( 24 | seedColor: Colors.blueGrey, 25 | ).copyWith( 26 | primary: Colors.grey.shade800, 27 | surfaceTint: Colors.transparent, 28 | ); 29 | 30 | return MaterialApp.router( 31 | title: 'CustomText Demo', 32 | routerConfig: _router, 33 | theme: ThemeData( 34 | colorScheme: colorScheme, 35 | // Workaround for https://github.com/flutter/flutter/issues/129553. 36 | typography: Typography.material2018(), 37 | textTheme: const TextTheme( 38 | bodyMedium: TextStyle(fontSize: 18.0, height: 1.5), 39 | bodySmall: TextStyle(fontSize: 14.0, height: 1.2), 40 | ), 41 | appBarTheme: AppBarTheme( 42 | foregroundColor: colorScheme.onPrimary, 43 | backgroundColor: colorScheme.primary, 44 | ), 45 | inputDecorationTheme: const InputDecorationTheme( 46 | border: OutlineInputBorder(), 47 | focusedBorder: OutlineInputBorder( 48 | borderSide: BorderSide( 49 | color: Colors.blueGrey, 50 | width: 1.6, 51 | ), 52 | ), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/widgets/description.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | import 'package:go_router/go_router.dart'; 5 | 6 | import 'package:custom_text_example/routes.dart'; 7 | 8 | class Description extends StatelessWidget { 9 | const Description({ 10 | required this.uri, 11 | required this.description, 12 | }); 13 | 14 | final Uri uri; 15 | final String description; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return DefaultTextStyle( 20 | style: Theme.of(context).textTheme.bodySmall!, 21 | child: Container( 22 | width: double.infinity, 23 | color: Theme.of(context).colorScheme.primary.withOpacity(0.85), 24 | padding: const EdgeInsets.all(16.0), 25 | child: Column( 26 | crossAxisAlignment: CrossAxisAlignment.start, 27 | children: [ 28 | CustomText( 29 | uri.exampleFilename, 30 | definitions: [ 31 | TextDefinition( 32 | matcher: const PatternMatcher(r'\w+\.dart'), 33 | hoverStyle: const TextStyle( 34 | decoration: TextDecoration.underline, 35 | ), 36 | onTap: (_) => context.go('${uri.path}/code'), 37 | ), 38 | ], 39 | style: TextStyle( 40 | fontSize: 16.0, 41 | fontWeight: FontWeight.bold, 42 | color: Colors.lightBlue.shade100, 43 | ), 44 | ), 45 | const SizedBox(height: 12.0), 46 | CustomText( 47 | description, 48 | definitions: [ 49 | SelectiveDefinition( 50 | matcher: const PatternMatcher('`(.+?)`'), 51 | shownText: (element) => element.groups[0] ?? '', 52 | matchStyle: const TextStyle( 53 | color: Colors.white, 54 | fontWeight: FontWeight.w500, 55 | ), 56 | ), 57 | ], 58 | style: const TextStyle(color: Colors.white70), 59 | ), 60 | ], 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/lib/widgets/hyperlink.dart: -------------------------------------------------------------------------------- 1 | import 'package:custom_text/custom_text.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:url_launcher/link.dart'; 4 | 5 | class Hyperlink extends StatelessWidget { 6 | const Hyperlink({required this.label, required this.url}); 7 | 8 | final String label; 9 | final String url; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Link( 14 | uri: Uri.parse(url), 15 | target: LinkTarget.blank, 16 | builder: (context, followLink) { 17 | return CustomText( 18 | '$label[i]', 19 | definitions: [ 20 | SpanDefinition( 21 | matcher: const PatternMatcher(r'(.+)\[i\]'), 22 | builder: (element) => TextSpan( 23 | children: [ 24 | TextSpan(text: element.groups.first), 25 | const WidgetSpan(child: SizedBox(width: 2.0)), 26 | WidgetSpan( 27 | alignment: PlaceholderAlignment.middle, 28 | child: Icon( 29 | Icons.open_in_new, 30 | size: 10.0, 31 | color: Colors.lightBlue.shade100, 32 | ), 33 | ), 34 | ], 35 | ), 36 | hoverStyle: const TextStyle(decoration: TextDecoration.underline), 37 | onTap: (_) => followLink!(), 38 | ), 39 | ], 40 | style: Theme.of(context) 41 | .textTheme 42 | .titleSmall 43 | ?.copyWith(color: Colors.lightBlue.shade100), 44 | ); 45 | }, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/lib/widgets/layouts.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text_example/widgets/description.dart'; 4 | 5 | class HorizontalLayout extends StatelessWidget { 6 | const HorizontalLayout({ 7 | required this.maxWidth, 8 | required this.description, 9 | required this.example, 10 | required this.output, 11 | }); 12 | 13 | final double maxWidth; 14 | final Description description; 15 | final Widget example; 16 | final Widget output; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Row( 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | Expanded( 24 | flex: 5, 25 | child: SingleChildScrollView( 26 | child: Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | description, 30 | example, 31 | ], 32 | ), 33 | ), 34 | ), 35 | const VerticalDivider( 36 | width: 1.0, 37 | thickness: 1.0, 38 | color: Colors.black26, 39 | ), 40 | Expanded( 41 | flex: 3, 42 | child: SizedBox( 43 | height: double.infinity, 44 | child: output, 45 | ), 46 | ), 47 | ], 48 | ); 49 | } 50 | } 51 | 52 | class VerticalLayout extends StatelessWidget { 53 | const VerticalLayout({ 54 | required this.maxHeight, 55 | required this.description, 56 | required this.example, 57 | required this.output, 58 | }); 59 | 60 | final double maxHeight; 61 | final Description description; 62 | final Widget example; 63 | final Widget output; 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return SingleChildScrollView( 68 | child: ConstrainedBox( 69 | constraints: BoxConstraints( 70 | minHeight: maxHeight, 71 | ), 72 | child: IntrinsicHeight( 73 | child: Column( 74 | crossAxisAlignment: CrossAxisAlignment.start, 75 | children: [ 76 | description, 77 | Expanded( 78 | child: example, 79 | ), 80 | const Divider( 81 | height: 1.0, 82 | thickness: 1.0, 83 | color: Colors.black26, 84 | ), 85 | SizedBox( 86 | width: double.infinity, 87 | height: 150.0, 88 | child: output, 89 | ), 90 | ], 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import device_info_plus 9 | import path_provider_foundation 10 | import url_launcher_macos 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 14 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 15 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = CustomText Demo 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.customTextExample 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_text_example 2 | description: An example app of custom_text. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.2.0 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | custom_text: 14 | path: ../ 15 | 16 | collection: ^1.18.0 17 | device_info_plus: ^9.1.2 18 | go_router: ^14.2.3 19 | # 6.2.0 has a bug and 6.2.1 does not support Dart 3.2. 20 | # https://github.com/material-foundation/flutter-packages/issues/567 21 | google_fonts: 6.1.0 22 | highlight: ^0.7.0 23 | url_launcher: ^6.3.0 24 | 25 | positioned_popup: 26 | git: 27 | url: https://github.com/kaboc/positioned-popup.git 28 | ref: 28af814a9dcb1623fce3a7301d10e76006afbf94 29 | 30 | dev_dependencies: 31 | build_runner: ^2.4.9 32 | flutter_lints: ^4.0.0 33 | go_router_builder: ^2.7.1 34 | 35 | flutter: 36 | uses-material-design: true 37 | 38 | assets: 39 | - lib/examples/basic/ 40 | - lib/examples/advanced/ 41 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | CustomText Demo 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 |
45 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CustomText Demo", 3 | "short_name": "CustomText", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A demo app of custom_text.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/web/styles.css: -------------------------------------------------------------------------------- 1 | /* https://loading.io/css/ */ 2 | 3 | #loading { 4 | display: inline-block; 5 | position: fixed; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate(-50%, -50%); 9 | width: 80px; 10 | height: 80px; 11 | } 12 | 13 | #loading div { 14 | position: absolute; 15 | border: 4px solid #7ac; 16 | opacity: 1; 17 | border-radius: 50%; 18 | animation: loading 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; 19 | } 20 | 21 | #loading div:nth-child(2) { 22 | animation-delay: -0.5s; 23 | } 24 | 25 | @keyframes loading { 26 | 0% { 27 | top: 36px; 28 | left: 36px; 29 | width: 0; 30 | height: 0; 31 | opacity: 0; 32 | } 33 | 4.9% { 34 | top: 36px; 35 | left: 36px; 36 | width: 0; 37 | height: 0; 38 | opacity: 0; 39 | } 40 | 5% { 41 | top: 36px; 42 | left: 36px; 43 | width: 0; 44 | height: 0; 45 | opacity: 1; 46 | } 47 | 100% { 48 | top: 0; 49 | left: 0; 50 | width: 72px; 51 | height: 72px; 52 | opacity: 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(custom_text LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "custom_text") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | UrlLauncherWindowsRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 14 | } 15 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_windows 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "A demo app of custom_text." "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "custom_text" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 kaboc. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "custom_text.exe" "\0" 98 | VALUE "ProductName", "CustomText Demo" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(100, 100); 29 | Win32Window::Size size(800, 600); 30 | if (!window.Create(L"CustomText Demo", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /lib/custom_text.dart: -------------------------------------------------------------------------------- 1 | export 'package:text_parser/text_parser.dart'; 2 | 3 | export 'src/builder.dart'; 4 | export 'src/definitions.dart'; 5 | export 'src/gesture_details.dart'; 6 | export 'src/matchers.dart'; 7 | export 'src/parser_options.dart'; 8 | export 'src/text.dart'; 9 | export 'src/text_editing_controller.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/gesture_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart' show immutable; 2 | import 'package:flutter/gestures.dart' show Offset, PointerDeviceKind; 3 | 4 | import 'package:text_parser/text_parser.dart' show TextElement; 5 | 6 | /// The kind of a gesture event. 7 | enum GestureKind { 8 | /// A tap/short press of the primary button. 9 | tap, 10 | 11 | /// A long press of the primary button. 12 | longPress, 13 | 14 | /// A tap/press of the secondary button. 15 | secondaryTap, 16 | 17 | /// A tap/press of the tertiary button. 18 | tertiaryTap, 19 | 20 | /// An enter of the mouse pointer to a region. 21 | enter, 22 | 23 | /// An exit of the mouse pointer from a region. 24 | exit, 25 | } 26 | 27 | /// A class with details on a gesture and the element where 28 | /// the gesture was detected. 29 | @immutable 30 | class GestureDetails { 31 | /// Creates a [GestureDetails] containing details on a gesture 32 | /// and the element where the gesture was detected. 33 | const GestureDetails({ 34 | required this.gestureKind, 35 | required this.pointerDeviceKind, 36 | required this.element, 37 | required this.shownText, 38 | required this.actionText, 39 | this.globalPosition = Offset.zero, 40 | this.localPosition = Offset.zero, 41 | }); 42 | 43 | /// The kind of the gesture that this details information is about. 44 | final GestureKind gestureKind; 45 | 46 | /// The kind of pointer. 47 | final PointerDeviceKind pointerDeviceKind; 48 | 49 | /// The text element where the gesture happened. 50 | final TextElement element; 51 | 52 | /// The string to display. 53 | final String shownText; 54 | 55 | /// The string to be passed to gesture callbacks and used basically 56 | /// for some action. 57 | final String actionText; 58 | 59 | /// The global position where a gesture was detected. 60 | final Offset globalPosition; 61 | 62 | /// The local position where a gesture was detected. 63 | final Offset localPosition; 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'package:text_parser/text_parser.dart' show TextMatcher; 2 | 3 | import 'definitions.dart'; 4 | 5 | /// A variant of [TextMatcher] for parsing Markdown link format. 6 | /// 7 | /// {@template customText.LinkMatcher} 8 | /// This is useful if used together with [SelectiveDefinition]. 9 | /// 10 | /// Note that this matcher does not treat nested brackets or braces 11 | /// in the same way as real Markdown parsers do. 12 | /// 13 | /// The preset pattern is overwritten if a custom pattern is provided. 14 | /// {@endtemplate} 15 | class LinkMatcher extends TextMatcher { 16 | /// Creates a [LinkMatcher] for parsing Markdown link format. 17 | /// 18 | /// {@macro customText.LinkMatcher} 19 | const LinkMatcher([super.pattern = r'\[(.+?)\]\((.*?)\)']); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/parser_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart' show immutable; 2 | 3 | import 'package:text_parser/text_parser.dart' show TextElement; 4 | 5 | /// The signature for an external parser function. 6 | typedef ExternalParser = Future> Function(String); 7 | 8 | /// A class that configures how regular expressions are treated 9 | /// in the default parser or specifies a different parser to use. 10 | @immutable 11 | class ParserOptions { 12 | /// Creates a [ParserOptions] that configures how regular expressions 13 | /// are treated. 14 | const ParserOptions({ 15 | this.multiLine = false, 16 | this.caseSensitive = true, 17 | this.unicode = false, 18 | this.dotAll = false, 19 | }) : parser = null; 20 | 21 | /// Creates a [ParserOptions] for specifying an external parser that 22 | /// parses text into a list of [TextElement]s. 23 | const ParserOptions.external(ExternalParser this.parser) 24 | : multiLine = false, 25 | caseSensitive = false, 26 | unicode = false, 27 | dotAll = false; 28 | 29 | /// If this is enabled, then `^` and `$` will match the beginning and 30 | /// end of a _line_, in addition to matching beginning and end of input, 31 | /// respectively. 32 | final bool multiLine; 33 | 34 | /// If this is disabled, then case is ignored. 35 | final bool caseSensitive; 36 | 37 | /// If this is enabled, then the pattern is treated as a Unicode 38 | /// pattern as described by the ECMAScript standard. 39 | final bool unicode; 40 | 41 | /// If this is enabled, then the `.` pattern will match _all_ characters, 42 | /// including line terminators. 43 | final bool dotAll; 44 | 45 | /// An external parser function. 46 | final ExternalParser? parser; 47 | 48 | @override 49 | bool operator ==(Object other) => 50 | identical(this, other) || 51 | other is ParserOptions && 52 | runtimeType == other.runtimeType && 53 | multiLine == other.multiLine && 54 | caseSensitive == other.caseSensitive && 55 | unicode == other.unicode && 56 | dotAll == other.dotAll; 57 | 58 | @override 59 | int get hashCode => Object.hash( 60 | runtimeType, 61 | multiLine, 62 | caseSensitive, 63 | unicode, 64 | dotAll, 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/span/data.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'dart:async' show Timer; 4 | 5 | import 'package:flutter/gestures.dart' show PointerEvent; 6 | import 'package:flutter/painting.dart' show InlineSpan, TextStyle; 7 | 8 | import 'package:text_parser/text_parser.dart' show TextElement; 9 | 10 | import '../definitions.dart'; 11 | import '../gesture_details.dart'; 12 | 13 | const kLongPressDuration = Duration(milliseconds: 600); 14 | 15 | class SpanData { 16 | SpanData({ 17 | required this.index, 18 | required this.element, 19 | required this.children, 20 | required this.definition, 21 | required this.shownText, 22 | required this.actionText, 23 | required this.onTapDown, 24 | required this.onTapCancel, 25 | required this.onMouseEnter, 26 | required this.onMouseExit, 27 | }); 28 | 29 | final int index; 30 | final TextElement element; 31 | final List? children; 32 | final Definition definition; 33 | final String? shownText; 34 | final String? actionText; 35 | final void Function(SpanData)? onTapDown; 36 | final void Function(SpanData)? onTapCancel; 37 | final void Function(PointerEvent, SpanData)? onMouseEnter; 38 | final void Function(PointerEvent, SpanData)? onMouseExit; 39 | } 40 | 41 | class SpansBuilderSettings { 42 | SpansBuilderSettings({ 43 | required List definitions, 44 | this.spans, 45 | this.style, 46 | this.matchStyle, 47 | this.tapStyle, 48 | this.hoverStyle, 49 | this.onTap, 50 | this.onLongPress, 51 | this.onGesture, 52 | Duration? longPressDuration, 53 | }) : definitions = {}, 54 | longPressDuration = longPressDuration ?? kLongPressDuration { 55 | // `i` is the index of a matcher (which is also the index of 56 | // a definition) that is necessary to identify which matcher 57 | // was used for a certain element when there are more than one 58 | // matchers of the same type. 59 | for (final (i, def) in definitions.indexed) { 60 | this.definitions.update( 61 | def.matcher.runtimeType, 62 | (list) => list..[i] = def, 63 | ifAbsent: () => {i: def}, 64 | ); 65 | } 66 | } 67 | 68 | final Map> definitions; 69 | final List? spans; 70 | final TextStyle? style; 71 | final TextStyle? matchStyle; 72 | final TextStyle? tapStyle; 73 | final TextStyle? hoverStyle; 74 | final GestureCallback? onTap; 75 | final GestureCallback? onLongPress; 76 | final GestureCallback? onGesture; 77 | final Duration longPressDuration; 78 | } 79 | 80 | /// A class to hold some values used for a workaround to skip 81 | /// unwanted hover events in GestureHandler. 82 | class HoverState { 83 | int? index; 84 | GestureKind? gestureKind; 85 | Map debounceTimers = {}; 86 | 87 | void reset() { 88 | index = null; 89 | gestureKind = null; 90 | debounceTimers 91 | ..forEach((_, t) => t.cancel()) 92 | ..clear(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/span/span_utils.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:flutter/gestures.dart' show TapGestureRecognizer; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | extension SpansToPlainText on List? { 7 | String? toPlainText() { 8 | return this == null ? null : TextSpan(children: this).toPlainText(); 9 | } 10 | } 11 | 12 | // Workaround for the bug where properties of TextSpan is not 13 | // applied to its children. 14 | // https://github.com/flutter/flutter/issues/10623 15 | // 16 | // NOTE: 17 | // If this is changed to an extension, a new instance of 18 | // _WidgetSpanChild is created every time the method is called. 19 | // Old instances keep existing and it leads to stack overflow. 20 | TextSpan applyPropsToChildren( 21 | TextSpan rootSpan, { 22 | TextSpan? targetSpan, 23 | TextStyle? mergedStyle, 24 | }) { 25 | final span = targetSpan ?? rootSpan; 26 | final children = span.children; 27 | 28 | final newMergedStyle = span.style == null 29 | ? mergedStyle 30 | : mergedStyle == null 31 | ? span.style 32 | : mergedStyle.merge(span.style); 33 | 34 | return TextSpan( 35 | text: span.text, 36 | style: span.style, 37 | // Only span with text needs gesture settings. 38 | recognizer: span.text == null ? null : rootSpan.recognizer, 39 | mouseCursor: span.text == null ? null : rootSpan.mouseCursor, 40 | onEnter: span.text == null ? null : rootSpan.onEnter, 41 | onExit: span.text == null ? null : rootSpan.onExit, 42 | // It is necessary to create new children instead of mutating 43 | // the existing children because they can be const. 44 | children: children == null 45 | ? null 46 | : [ 47 | for (final child in children) 48 | if (child is TextSpan) 49 | applyPropsToChildren( 50 | rootSpan, 51 | targetSpan: child, 52 | mergedStyle: newMergedStyle, 53 | ) 54 | else if (child is WidgetSpan) 55 | WidgetSpan( 56 | alignment: child.alignment, 57 | baseline: child.baseline, 58 | style: newMergedStyle == null 59 | ? null 60 | : const TextStyle( 61 | // Colours except for the foreground colour are 62 | // inherited to WidgetSpan, but other attributes 63 | // don't seem to be inherited. Therefore it is 64 | // necessary to use DefaultTextStyle to apply 65 | // the ancestors' styles to the child. 66 | // However, if the background colour is translucent, 67 | // the inherited colour and the same colour by 68 | // DefaultTextStyle are mixed and become darker. 69 | // https://github.com/flutter/flutter/issues/137030 70 | // 71 | // The transparent colour here is a workaround to 72 | // avoid it by negating the inherited colour. 73 | backgroundColor: Color(0x00000000), 74 | // Text decoration is applied to both TextSpan 75 | // and WidgetSpan. In order to prevent WidgetSpan 76 | // from getting duplicate decorations, it is 77 | // necessary to remove one from WidgetSpan. 78 | decoration: TextDecoration.none, 79 | ), 80 | child: WidgetSpanChild( 81 | rootSpan: rootSpan, 82 | widgetSpan: child, 83 | mergedStyle: newMergedStyle, 84 | ), 85 | ), 86 | ], 87 | ); 88 | } 89 | 90 | class WidgetSpanChild extends StatelessWidget { 91 | // ignore: use_key_in_widget_constructors 92 | const WidgetSpanChild({ 93 | required this.rootSpan, 94 | required this.widgetSpan, 95 | required this.mergedStyle, 96 | }); 97 | 98 | final TextSpan rootSpan; 99 | final WidgetSpan widgetSpan; 100 | final TextStyle? mergedStyle; 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | final parentRecognizer = rootSpan.recognizer; 105 | final recognizer = 106 | parentRecognizer is TapGestureRecognizer ? parentRecognizer : null; 107 | 108 | var child = widgetSpan.child; 109 | 110 | final span = rootSpan; 111 | if (span.onEnter != null || 112 | span.onExit != null || 113 | span.mouseCursor != MouseCursor.defer) { 114 | child = MouseRegion( 115 | onEnter: span.onEnter, 116 | onExit: span.onExit, 117 | cursor: span.mouseCursor, 118 | child: child, 119 | ); 120 | } 121 | 122 | if (recognizer != null) { 123 | child = GestureDetector( 124 | behavior: HitTestBehavior.opaque, 125 | onTapDown: recognizer.onTapDown, 126 | onTapUp: recognizer.onTapUp, 127 | onTapCancel: recognizer.onTapCancel, 128 | onSecondaryTapUp: recognizer.onSecondaryTapUp, 129 | onTertiaryTapUp: recognizer.onTertiaryTapUp, 130 | child: child, 131 | ); 132 | } 133 | 134 | return mergedStyle == null 135 | ? child 136 | : DefaultTextStyle.merge( 137 | style: mergedStyle, 138 | child: child, 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/src/span/split_spans.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:flutter/painting.dart' show InlineSpan, TextSpan; 4 | 5 | import 'package:text_parser/text_parser.dart' show TextElement; 6 | 7 | typedef _Result = ({ 8 | Map> spans, 9 | int elmIndex, 10 | int textOffset, 11 | }); 12 | 13 | extension on Map> { 14 | void addOrCreate(Object key, InlineSpan span) { 15 | this[key] ??= []; 16 | this[key]?.add(span); 17 | } 18 | } 19 | 20 | /// Splits InlineSpans so that each of them corresponds to a TextElement. 21 | extension SplitSpans on List { 22 | Map> splitSpans({required List elements}) { 23 | return _split(elements: elements).spans; 24 | } 25 | 26 | _Result _split({ 27 | required List elements, 28 | int elmIndex = 0, 29 | int textOffset = 0, 30 | }) { 31 | var currentElmIndex = elmIndex; 32 | var elmTextOffset = textOffset; 33 | 34 | final spans = >{}; 35 | for (final span in this) { 36 | if (currentElmIndex >= elements.length) { 37 | break; 38 | } 39 | 40 | if (span is! TextSpan) { 41 | spans.addOrCreate(currentElmIndex, span); 42 | 43 | elmTextOffset++; 44 | final elmTextLen = elements[currentElmIndex].text.length; 45 | 46 | if (elmTextOffset == elmTextLen) { 47 | currentElmIndex++; 48 | elmTextOffset = 0; 49 | } 50 | continue; 51 | } 52 | 53 | final spanText = span.text; 54 | if (spanText == null) { 55 | spans.addOrCreate( 56 | currentElmIndex, 57 | TextSpan(style: span.style), 58 | ); 59 | } else { 60 | var spanTextOffset = 0; 61 | 62 | while (true) { 63 | final spanRemainingText = spanText.substring(spanTextOffset); 64 | final spanEndOffset = elmTextOffset + spanRemainingText.length; 65 | final elmTextLen = elements[currentElmIndex].text.length; 66 | 67 | if (spanEndOffset <= elmTextLen) { 68 | spans.addOrCreate( 69 | currentElmIndex, 70 | TextSpan( 71 | text: spanRemainingText, 72 | style: span.style, 73 | ), 74 | ); 75 | 76 | if (spanEndOffset < elmTextLen) { 77 | elmTextOffset = spanEndOffset; 78 | } else { 79 | currentElmIndex++; 80 | elmTextOffset = 0; 81 | } 82 | break; 83 | } 84 | 85 | final elmEndOffset = elmTextLen - elmTextOffset; 86 | spans.addOrCreate( 87 | currentElmIndex, 88 | TextSpan( 89 | text: spanRemainingText.substring(0, elmEndOffset), 90 | style: span.style, 91 | ), 92 | ); 93 | 94 | currentElmIndex++; 95 | elmTextOffset = 0; 96 | spanTextOffset += elmEndOffset; 97 | 98 | if (currentElmIndex >= elements.length) { 99 | break; 100 | } 101 | } 102 | } 103 | 104 | final spanChildren = span.children; 105 | if (spanChildren != null) { 106 | final result = spanChildren._split( 107 | elements: elements, 108 | elmIndex: currentElmIndex, 109 | textOffset: elmTextOffset, 110 | ); 111 | 112 | for (final i in result.spans.keys) { 113 | if (result.spans[i]!.isNotEmpty) { 114 | final child = spans[i]; 115 | final j = child == null ? 0 : child.length - 1; 116 | final lastSpan = child?[j]; 117 | 118 | if (child == null || lastSpan is! TextSpan) { 119 | spans[i] = [ 120 | TextSpan( 121 | children: result.spans[i], 122 | style: span.style, 123 | ), 124 | ]; 125 | } else { 126 | spans[i]![j] = TextSpan( 127 | text: lastSpan.text, 128 | children: result.spans[i], 129 | style: span.style, 130 | ); 131 | } 132 | } 133 | } 134 | 135 | currentElmIndex = result.elmIndex; 136 | elmTextOffset = result.textOffset; 137 | } 138 | } 139 | 140 | return ( 141 | spans: spans, 142 | elmIndex: currentElmIndex, 143 | textOffset: elmTextOffset, 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/span/text_span_notifier.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:flutter/foundation.dart' show ChangeNotifier, ValueListenable; 4 | import 'package:flutter/painting.dart' show TextSpan, TextStyle; 5 | 6 | import 'package:text_parser/text_parser.dart' show TextElement; 7 | 8 | import 'data.dart'; 9 | import 'gesture_handler.dart'; 10 | import 'spans_builder.dart'; 11 | import 'transient_elements_builder.dart'; 12 | 13 | /// ValueNotifier with the ability to forcefully reassign a new value 14 | /// regardless of whether it is equal to the previous. 15 | class _ValueNotifier extends ChangeNotifier 16 | implements ValueListenable { 17 | _ValueNotifier(this._value); 18 | 19 | TextSpan _value; 20 | bool _disposed = false; 21 | 22 | @override 23 | TextSpan get value => _value; 24 | 25 | @override 26 | void dispose() { 27 | _disposed = true; 28 | super.dispose(); 29 | } 30 | 31 | void updateValue(TextSpan newValue, {bool force = false}) { 32 | if (!_disposed && (_value != newValue || force)) { 33 | _value = newValue; 34 | notifyListeners(); 35 | } 36 | } 37 | } 38 | 39 | class CustomTextSpanNotifier extends _ValueNotifier { 40 | CustomTextSpanNotifier({ 41 | required String? initialText, 42 | required TextStyle? initialStyle, 43 | required SpansBuilderSettings settings, 44 | }) : super( 45 | initialText == null || initialText.isEmpty 46 | ? const TextSpan() 47 | : TextSpan(text: initialText, style: initialStyle), 48 | ) { 49 | _spansBuilder = SpansBuilder( 50 | settings: settings, 51 | gestureHandlers: GestureHandlers(), 52 | onSpanUpdateNeeded: _onSpanUpdateNeeded, 53 | ); 54 | } 55 | 56 | late final SpansBuilder _spansBuilder; 57 | 58 | List get elements => _spansBuilder.elements; 59 | 60 | @override 61 | void dispose() { 62 | _spansBuilder.gestureHandlers?.dispose(); 63 | super.dispose(); 64 | } 65 | 66 | // ignore: use_setters_to_change_properties 67 | void updateSettings(SpansBuilderSettings settings) { 68 | _spansBuilder.settings = settings; 69 | } 70 | 71 | // ignore: use_setters_to_change_properties 72 | void updateElements(List elements) { 73 | _spansBuilder.elements = elements; 74 | } 75 | 76 | void buildSpan({ 77 | required TextStyle? style, 78 | required List updatedDefinitionIndexes, 79 | }) { 80 | updateValue( 81 | force: true, 82 | TextSpan( 83 | children: _spansBuilder.buildSpans( 84 | style: style, 85 | currentSpans: value.children ?? [], 86 | updatedDefinitionIndexes: updatedDefinitionIndexes, 87 | ), 88 | ), 89 | ); 90 | } 91 | 92 | void buildTransientSpan({ 93 | required TextStyle? style, 94 | required Range replaceRange, 95 | required Range spanRange, 96 | }) { 97 | final spans = _spansBuilder.buildTransientSpans( 98 | style: style, 99 | spanRange: spanRange, 100 | ); 101 | 102 | updateValue( 103 | TextSpan( 104 | children: List.of(value.children!) 105 | ..replaceRange(replaceRange.start, replaceRange.end + 1, spans), 106 | ), 107 | ); 108 | } 109 | 110 | void _onSpanUpdateNeeded(int index, TextElement element, TextSpan span) { 111 | // Span must not be updated in the following cases: 112 | // 113 | // * When the notifier is no longer available. (issue #6) 114 | // Disposal is not checked here but in value setter. 115 | // * When the number of spans has been reduced while one of them 116 | // is still hovered on, in which case an update will lead to 117 | // a range error. 118 | // * When the text at the index has changed, which indicates that 119 | // this method has been triggered by the hover handler of the 120 | // old span and it will cause the new span to get wrong text. 121 | if (index < value.children!.length && 122 | elements[index].text == element.text) { 123 | updateValue( 124 | TextSpan( 125 | children: List.of(value.children!)..[index] = span, 126 | ), 127 | ); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'definitions.dart'; 4 | 5 | extension CompareDefinitions on List { 6 | bool hasUpdatedMatchers(List? other) { 7 | return length != other?.length || 8 | indexed.any((e) => e.$2.matcher != other?[e.$1].matcher); 9 | } 10 | 11 | List findUpdatedDefinitions(List? other) { 12 | return [ 13 | if (length == other?.length) 14 | for (final (i, def) in indexed) 15 | if (def != other?[i]) i, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_text 2 | description: Highly customisable text widget with advanced styling and gesture interactions. A controller is also available for use in text editing. 3 | version: 2.0.4 4 | repository: https://github.com/kaboc/flutter_custom_text 5 | 6 | topics: 7 | - text 8 | - text-decorations 9 | - rich-text 10 | - highlight 11 | - link 12 | 13 | environment: 14 | sdk: '>=3.2.0 <4.0.0' 15 | flutter: '>=3.16.0' 16 | 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | meta: ^1.10.0 22 | text_parser: ^2.4.0 23 | 24 | dev_dependencies: 25 | flutter_test: 26 | sdk: flutter 27 | 28 | flutter_lints: ^4.0.0 29 | 30 | screenshots: 31 | - description: 'Applying styles and triggering actions by gestures.' 32 | path: screenshots/unique_styles_and_actions.gif 33 | - description: 'Changing mouse cursor and text style on hover.' 34 | path: screenshots/mouse_cursor_and_text_style_on_hover.gif 35 | - description: 'Showing a popup at the position of a mouse enter event.' 36 | path: screenshots/event_position.gif 37 | - description: 'CustomTextEditingController enables text styling and actions in editable text.' 38 | path: screenshots/custom_text_editing_controller.gif 39 | - description: 'Support for an external parser allows a wider variety of use cases.' 40 | path: screenshots/external_parser.gif 41 | -------------------------------------------------------------------------------- /screenshots/custom_text_editing_controller.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/screenshots/custom_text_editing_controller.gif -------------------------------------------------------------------------------- /screenshots/event_position.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/screenshots/event_position.gif -------------------------------------------------------------------------------- /screenshots/external_parser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/screenshots/external_parser.gif -------------------------------------------------------------------------------- /screenshots/mouse_cursor_and_text_style_on_hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/screenshots/mouse_cursor_and_text_style_on_hover.gif -------------------------------------------------------------------------------- /screenshots/unique_styles_and_actions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaboc/flutter_custom_text/125bb9108d1e5ccea280cae93772a4b3cf679c30/screenshots/unique_styles_and_actions.gif -------------------------------------------------------------------------------- /test/unit_tests/equality_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'package:custom_text/custom_text.dart'; 6 | 7 | void main() { 8 | group('Definitions', () { 9 | test('Definitions with same values and runtimeType are equal', () { 10 | // ignore: prefer_const_constructors 11 | final definition1 = TextDefinition( 12 | matcher: const UrlMatcher(), 13 | matchStyle: const TextStyle(fontSize: 10.0), 14 | tapStyle: const TextStyle(fontSize: 11.0), 15 | hoverStyle: const TextStyle(fontSize: 12.0), 16 | mouseCursor: SystemMouseCursors.click, 17 | ); 18 | 19 | // ignore: prefer_const_constructors 20 | final definition2 = TextDefinition( 21 | matcher: const UrlMatcher(), 22 | matchStyle: const TextStyle(fontSize: 10.0), 23 | tapStyle: const TextStyle(fontSize: 11.0), 24 | hoverStyle: const TextStyle(fontSize: 12.0), 25 | mouseCursor: SystemMouseCursors.click, 26 | ); 27 | 28 | expect(definition1, definition2); 29 | expect(definition1.hashCode, definition2.hashCode); 30 | }); 31 | 32 | test( 33 | 'Definitions with same values and different runtimeTypes are not equal', 34 | () { 35 | const definition1 = _Definition1(matcher: UrlMatcher()); 36 | const definition2 = _Definition2(matcher: UrlMatcher()); 37 | 38 | expect(definition1, isNot(definition2)); 39 | expect(definition1.hashCode, isNot(definition2.hashCode)); 40 | }, 41 | ); 42 | }); 43 | 44 | group('ParserOptions', () { 45 | test('ParserOptions with same values and runtimeType are equal', () { 46 | // ignore: prefer_const_constructors 47 | final options1 = ParserOptions(); 48 | // ignore: prefer_const_constructors 49 | final options2 = ParserOptions(); 50 | 51 | expect(options1, options2); 52 | expect(options1.hashCode, options2.hashCode); 53 | }); 54 | 55 | test( 56 | 'ParserOptions with same values and different runtimeTypes are not equal', 57 | () { 58 | const options1 = _ParserOptions1(); 59 | const options2 = _ParserOptions2(); 60 | 61 | expect(options1, isNot(options2)); 62 | expect(options1.hashCode, isNot(options2.hashCode)); 63 | }, 64 | ); 65 | }); 66 | } 67 | 68 | class _Definition1 extends TextDefinition { 69 | const _Definition1({required super.matcher}); 70 | } 71 | 72 | class _Definition2 extends TextDefinition { 73 | const _Definition2({required super.matcher}); 74 | } 75 | 76 | class _ParserOptions1 extends ParserOptions { 77 | const _ParserOptions1(); 78 | } 79 | 80 | class _ParserOptions2 extends ParserOptions { 81 | const _ParserOptions2(); 82 | } 83 | -------------------------------------------------------------------------------- /test/widget_tests/external_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:custom_text/custom_text.dart'; 5 | 6 | import 'utils.dart'; 7 | import 'widgets.dart'; 8 | 9 | void main() { 10 | group('External parser (CustomText)', () { 11 | testWidgets('Styled acc to result of external parser', (tester) async { 12 | const style = TextStyle(color: Color(0xFF111111)); 13 | const matchStyle = TextStyle(color: Color(0xFF222222)); 14 | 15 | await tester.pumpWidget( 16 | const CustomTextWidget( 17 | 'abc1234def5678', 18 | parserOptions: ParserOptions.external(_parseNumbers), 19 | definitions: [ 20 | TextDefinition( 21 | matcher: NumberMatcher(), 22 | matchStyle: matchStyle, 23 | ), 24 | ], 25 | style: style, 26 | ), 27 | ); 28 | await tester.pump(); 29 | 30 | expect(findTextSpanByText('abc')?.style, style); 31 | expect(findTextSpanByText('1234')?.style, matchStyle); 32 | expect(findTextSpanByText('def')?.style, style); 33 | expect(findTextSpanByText('5678')?.style, matchStyle); 34 | }); 35 | 36 | testWidgets('External parser reruns when text is updated', (tester) async { 37 | const style = TextStyle(color: Color(0xFF111111)); 38 | const matchStyle = TextStyle(color: Color(0xFF222222)); 39 | 40 | var text = 'abc1234def'; 41 | 42 | await tester.pumpWidget( 43 | StatefulBuilder( 44 | builder: (context, setState) { 45 | return CustomTextWidget( 46 | text, 47 | parserOptions: const ParserOptions.external(_parseNumbers), 48 | definitions: [ 49 | TextDefinition( 50 | matcher: const NumberMatcher(), 51 | matchStyle: matchStyle, 52 | onTap: (details) => setState(() => text = '123456def'), 53 | ), 54 | ], 55 | style: style, 56 | ); 57 | }, 58 | ), 59 | ); 60 | await tester.pump(); 61 | 62 | expect(findTextSpanByText('abc')?.style, style); 63 | expect(findTextSpanByText('1234')?.style, matchStyle); 64 | 65 | findTextSpanByText('1234') 66 | ..tapDown() 67 | ..tapUp(); 68 | await tester.pumpAndSettle(); 69 | 70 | expect(findTextSpanByText('abc'), isNull); 71 | expect(findTextSpanByText('123456')?.style, matchStyle); 72 | }); 73 | }); 74 | 75 | group('External parser (CustomTextEditingController)', () { 76 | testWidgets('TextElements are updated by external parser', (tester) async { 77 | final controller = CustomTextEditingController( 78 | text: 'abc1234def5678', 79 | parserOptions: const ParserOptions.external(_parseNumbers), 80 | definitions: const [TextDefinition(matcher: NumberMatcher())], 81 | ); 82 | addTearDown(controller.dispose); 83 | 84 | await tester.pumpWidget( 85 | TextFieldWidget(controller: controller), 86 | ); 87 | await tester.pump(); 88 | 89 | expect(controller.elements[0].text, 'abc'); 90 | expect(controller.elements[0].matcherType, TextMatcher); 91 | expect(controller.elements[1].text, '1234'); 92 | expect(controller.elements[1].matcherType, NumberMatcher); 93 | expect(controller.elements[2].text, 'def'); 94 | expect(controller.elements[2].matcherType, TextMatcher); 95 | expect(controller.elements[3].text, '5678'); 96 | expect(controller.elements[3].matcherType, NumberMatcher); 97 | }); 98 | 99 | testWidgets('External parser reruns when text is updated', (tester) async { 100 | final controller = CustomTextEditingController( 101 | text: 'abc1234def5678', 102 | parserOptions: const ParserOptions.external(_parseNumbers), 103 | definitions: const [TextDefinition(matcher: NumberMatcher())], 104 | ); 105 | addTearDown(controller.dispose); 106 | 107 | await tester.pumpWidget( 108 | TextFieldWidget(controller: controller), 109 | ); 110 | await tester.pump(); 111 | 112 | expect(controller.elements, hasLength(4)); 113 | expect(controller.elements[0].text, 'abc'); 114 | expect(controller.elements[1].text, '1234'); 115 | 116 | controller.text = '123456def'; 117 | await tester.pump(); 118 | 119 | expect(controller.elements, hasLength(2)); 120 | expect(controller.elements[0].text, '123456'); 121 | expect(controller.elements[1].text, 'def'); 122 | }); 123 | }); 124 | } 125 | 126 | Future> _parseNumbers(String text) async { 127 | final elements = []; 128 | final len = text.length; 129 | var offset = 0; 130 | var wasNumeric = false; 131 | 132 | for (var i = 0; i < len; i++) { 133 | final isNumeric = _isNumeric(text[i]); 134 | final isEnd = i == len - 1; 135 | 136 | if (i > 0 && (isNumeric != wasNumeric || isEnd)) { 137 | final substring = text.substring(offset, isEnd ? i + 1 : i); 138 | elements.add( 139 | TextElement( 140 | substring, 141 | offset: offset, 142 | matcherType: wasNumeric ? NumberMatcher : TextMatcher, 143 | ), 144 | ); 145 | offset = i; 146 | } 147 | 148 | wasNumeric = isNumeric; 149 | } 150 | 151 | return elements; 152 | } 153 | 154 | bool _isNumeric(String text) { 155 | return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].contains(text[0]); 156 | } 157 | 158 | class NumberMatcher extends TextMatcher { 159 | const NumberMatcher() : super(''); 160 | } 161 | -------------------------------------------------------------------------------- /test/widget_tests/rebuild_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show unawaited; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import 'package:custom_text/custom_text.dart'; 7 | 8 | import 'utils.dart'; 9 | import 'widgets.dart'; 10 | 11 | void main() { 12 | testWidgets( 13 | 'Changing rebuildKey causes a rebuild of only relevant spans', 14 | (tester) async { 15 | var rebuildKey = const ValueKey(1); 16 | 17 | await tester.pumpWidget( 18 | StatefulBuilder( 19 | builder: (_, setState) { 20 | return CustomTextWidget( 21 | 'aaabbbccc', 22 | definitions: [ 23 | TextDefinition( 24 | rebuildKey: rebuildKey, 25 | matcher: const PatternMatcher('bbb'), 26 | ), 27 | const TextDefinition( 28 | matcher: PatternMatcher('ccc'), 29 | ), 30 | ], 31 | onButtonPressed: () { 32 | setState(() => rebuildKey = const ValueKey(2)); 33 | }, 34 | ); 35 | }, 36 | ), 37 | ); 38 | await tester.pump(); 39 | 40 | final spans1 = List.of(findFirstTextSpan()!.children!); 41 | 42 | await tester.tapButton(); 43 | await tester.pumpAndSettle(); 44 | 45 | final spans2 = List.of(findFirstTextSpan()!.children!); 46 | 47 | expect(spans1, hasLength(3)); 48 | expect(spans2, hasLength(3)); 49 | 50 | expect(spans2[0], same(spans1[0])); 51 | expect(spans2[1], isNot(same(spans1[1]))); 52 | expect(spans2[2], same(spans1[2])); 53 | }, 54 | ); 55 | 56 | testWidgets( 57 | 'Changing rebuildKey in preBuilder causes CustomSpanBuilder to rerun ' 58 | 'despite no other change in builder/preBuilder settings', 59 | (tester) async { 60 | late CustomSpanBuilder builder; 61 | var rebuildKey = const ValueKey(1); 62 | var parsedEntirely = false; 63 | 64 | await tester.pumpWidget( 65 | Directionality( 66 | textDirection: TextDirection.ltr, 67 | child: StatefulBuilder( 68 | builder: (context, setState) { 69 | return Column( 70 | children: [ 71 | CustomText( 72 | 'aaabbbccc', 73 | definitions: const [ 74 | TextDefinition(matcher: PatternMatcher('')), 75 | ], 76 | preBuilder: builder = CustomSpanBuilder( 77 | definitions: [ 78 | TextDefinition( 79 | rebuildKey: rebuildKey, 80 | matcher: const PatternMatcher('bbb'), 81 | ), 82 | const TextDefinition( 83 | matcher: PatternMatcher('ccc'), 84 | ), 85 | ], 86 | ), 87 | ), 88 | ElevatedButton( 89 | onPressed: () { 90 | setState(() => rebuildKey = const ValueKey(2)); 91 | }, 92 | child: const Text('Button'), 93 | ), 94 | ], 95 | ); 96 | }, 97 | ), 98 | ), 99 | ); 100 | await tester.pump(); 101 | 102 | final spansByBuilder1 = builder.span.children!; 103 | final span1 = findFirstTextSpan(); 104 | 105 | parsedEntirely = false; 106 | await tester.tapButton(); 107 | await tester.pumpAndSettle(); 108 | 109 | final spansByBuilder2 = builder.span.children!; 110 | final span2 = findFirstTextSpan(); 111 | 112 | expect(builder.parsed, isFalse); 113 | expect(parsedEntirely, isFalse); 114 | expect(builder.built, isTrue); 115 | expect(spansByBuilder2[0], same(spansByBuilder1[0])); 116 | expect(spansByBuilder2[1], isNot(same(spansByBuilder1[1]))); 117 | expect(spansByBuilder2[2], same(spansByBuilder1[2])); 118 | expect(span2, isNot(same(span1))); 119 | }, 120 | ); 121 | 122 | testWidgets( 123 | 'Reassembling app causes an entire rebuild (similarly to when rebuildKey ' 124 | 'is replaced), while rebuilding parent of CustomText does not.', 125 | (tester) async { 126 | var replace = 'BBB'; 127 | 128 | await tester.pumpWidget( 129 | Directionality( 130 | textDirection: TextDirection.ltr, 131 | child: StatefulBuilder( 132 | builder: (context, setState) { 133 | return Column( 134 | children: [ 135 | CustomText( 136 | 'aaabbb', 137 | definitions: [ 138 | SpanDefinition( 139 | matcher: const PatternMatcher('bbb'), 140 | builder: (element) => TextSpan(text: replace), 141 | ), 142 | ], 143 | ), 144 | ElevatedButton( 145 | onPressed: () => setState(() => replace = 'CCC'), 146 | child: const Text('Button'), 147 | ), 148 | ], 149 | ); 150 | }, 151 | ), 152 | ), 153 | ); 154 | await tester.pump(); 155 | expect(findText().textSpan?.toPlainText(), 'aaaBBB'); 156 | 157 | await tester.tapButton(); 158 | await tester.pumpAndSettle(); 159 | expect(findText().textSpan?.toPlainText(), 'aaaBBB'); 160 | 161 | unawaited(tester.binding.reassembleApplication()); 162 | await tester.pumpAndSettle(); 163 | expect(findText().textSpan?.toPlainText(), 'aaaCCC'); 164 | }, 165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /test/widget_tests/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import 'package:custom_text/custom_text.dart'; 7 | 8 | import 'package:custom_text/src/span/data.dart'; 9 | 10 | final kTestLongPressDuration = 11 | kLongPressDuration + const Duration(milliseconds: 10); 12 | 13 | GestureKind? gestureKind; 14 | PointerDeviceKind? pointerDeviceKind; 15 | TextElement? element; 16 | String? shownText; 17 | String? actionText; 18 | Offset? globalPosition; 19 | Offset? localPosition; 20 | 21 | void reset() { 22 | gestureKind = null; 23 | pointerDeviceKind = null; 24 | element = null; 25 | shownText = null; 26 | actionText = null; 27 | globalPosition = null; 28 | localPosition = null; 29 | } 30 | 31 | void onAction(GestureDetails details) { 32 | gestureKind = details.gestureKind; 33 | pointerDeviceKind = details.pointerDeviceKind; 34 | element = details.element; 35 | shownText = details.shownText; 36 | actionText = details.actionText; 37 | globalPosition = details.globalPosition; 38 | localPosition = details.localPosition; 39 | } 40 | 41 | Text findText() { 42 | final finder = find.byType(Text); 43 | return finder.evaluate().first.widget as Text; 44 | } 45 | 46 | TextSpan? findFirstTextSpan() { 47 | return findText().textSpan as TextSpan?; 48 | } 49 | 50 | TextSpan? findTextSpanByText(String text) { 51 | TextSpan? span; 52 | findText().textSpan?.visitChildren( 53 | (visitor) { 54 | if (visitor is TextSpan && visitor.text == text) { 55 | span = visitor; 56 | return false; 57 | } 58 | return true; 59 | }, 60 | ); 61 | return span; 62 | } 63 | 64 | extension SpanRecognizer on TextSpan? { 65 | List findWidgetSpans() { 66 | final spans = []; 67 | this?.visitChildren((span) { 68 | if (span is WidgetSpan) { 69 | spans.add(span); 70 | } 71 | return true; 72 | }); 73 | return spans; 74 | } 75 | 76 | void tapDown() { 77 | final recognizer = this?.recognizer as TapGestureRecognizer?; 78 | recognizer!.onTapDown!(TapDownDetails()); 79 | } 80 | 81 | void maybeTapDown() { 82 | final recognizer = this?.recognizer as TapGestureRecognizer?; 83 | recognizer?.onTapDown?.call(TapDownDetails()); 84 | } 85 | 86 | void tapUp() { 87 | final recognizer = this?.recognizer as TapGestureRecognizer?; 88 | recognizer!.onTapUp!(TapUpDetails(kind: PointerDeviceKind.touch)); 89 | } 90 | 91 | void maybeTapUp() { 92 | final recognizer = this?.recognizer as TapGestureRecognizer?; 93 | recognizer?.onTapUp?.call(TapUpDetails(kind: PointerDeviceKind.touch)); 94 | } 95 | } 96 | 97 | extension WidgetTesterExtension on WidgetTester { 98 | Future tapButton() async { 99 | final finder = find.byType(ElevatedButton); 100 | await tap(finder); 101 | } 102 | 103 | Iterable findWidgetsByType() { 104 | return widgetList(find.byType(T)).map((v) => v as T); 105 | } 106 | 107 | Iterable findDescendantWidgetsByType({required Type of}) { 108 | final finder = find.descendant( 109 | of: find.byType(of), 110 | matching: find.byType(T), 111 | ); 112 | return widgetList(finder).map((v) => v as T); 113 | } 114 | 115 | RenderEditable? findRenderEditable() { 116 | final ro = renderObject(find.byType(EditableText).first); 117 | 118 | RenderEditable? renderEditable; 119 | void visitor(RenderObject child) { 120 | if (child is RenderEditable) { 121 | renderEditable = child; 122 | } else { 123 | child.visitChildren(visitor); 124 | } 125 | } 126 | 127 | ro.visitChildren(visitor); 128 | 129 | return renderEditable; 130 | } 131 | 132 | Rect getCursorRect() { 133 | return findRenderEditable()! 134 | .getLocalRectForCaret(const TextPosition(offset: 0)); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/widget_tests/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:custom_text/custom_text.dart'; 4 | 5 | class CustomTextWidget extends StatelessWidget { 6 | const CustomTextWidget( 7 | this.text, { 8 | super.key, 9 | this.definitions, 10 | this.parserOptions = const ParserOptions(), 11 | this.style, 12 | this.matchStyle, 13 | this.tapStyle, 14 | this.hoverStyle, 15 | this.matchStyleInDef, 16 | this.tapStyleInDef, 17 | this.hoverStyleInDef, 18 | this.onTap, 19 | this.onLongPress, 20 | this.onGesture, 21 | this.onTapInDef, 22 | this.onLongPressInDef, 23 | this.onGestureInDef, 24 | this.longPressDuration, 25 | this.mouseCursor, 26 | this.preventBlocking = false, 27 | this.onButtonPressed, 28 | }); 29 | 30 | final String text; 31 | final List? definitions; 32 | final ParserOptions parserOptions; 33 | final TextStyle? style; 34 | final TextStyle? matchStyle; 35 | final TextStyle? tapStyle; 36 | final TextStyle? hoverStyle; 37 | final TextStyle? matchStyleInDef; 38 | final TextStyle? tapStyleInDef; 39 | final TextStyle? hoverStyleInDef; 40 | final GestureCallback? onTap; 41 | final GestureCallback? onLongPress; 42 | final GestureCallback? onGesture; 43 | final GestureCallback? onTapInDef; 44 | final GestureCallback? onLongPressInDef; 45 | final GestureCallback? onGestureInDef; 46 | final Duration? longPressDuration; 47 | final MouseCursor? mouseCursor; 48 | final bool preventBlocking; 49 | final VoidCallback? onButtonPressed; 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Directionality( 54 | textDirection: TextDirection.ltr, 55 | child: Column( 56 | children: [ 57 | CustomText( 58 | text, 59 | parserOptions: parserOptions, 60 | definitions: definitions ?? 61 | [ 62 | const TextDefinition( 63 | matcher: UrlMatcher(), 64 | ), 65 | TextDefinition( 66 | matcher: const EmailMatcher(), 67 | matchStyle: matchStyleInDef, 68 | tapStyle: tapStyleInDef, 69 | hoverStyle: hoverStyleInDef, 70 | onTap: onTapInDef, 71 | onLongPress: onLongPressInDef, 72 | onGesture: onGestureInDef, 73 | mouseCursor: mouseCursor, 74 | ), 75 | ], 76 | style: style, 77 | matchStyle: matchStyle, 78 | tapStyle: tapStyle, 79 | hoverStyle: hoverStyle, 80 | onTap: onTap, 81 | onLongPress: onLongPress, 82 | onGesture: onGesture, 83 | longPressDuration: longPressDuration, 84 | preventBlocking: preventBlocking, 85 | ), 86 | ElevatedButton( 87 | onPressed: onButtonPressed, 88 | child: const Text('Button'), 89 | ), 90 | ], 91 | ), 92 | ); 93 | } 94 | } 95 | 96 | class TextFieldWidget extends StatelessWidget { 97 | const TextFieldWidget({ 98 | super.key, 99 | required this.controller, 100 | this.style, 101 | this.onButtonPressed, 102 | }); 103 | 104 | final CustomTextEditingController controller; 105 | final TextStyle? style; 106 | final VoidCallback? onButtonPressed; 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | return MaterialApp( 111 | home: Scaffold( 112 | body: Column( 113 | children: [ 114 | TextField( 115 | controller: controller, 116 | style: style, 117 | ), 118 | ElevatedButton( 119 | onPressed: onButtonPressed, 120 | child: const Text('Button'), 121 | ), 122 | ], 123 | ), 124 | ), 125 | ); 126 | } 127 | } 128 | --------------------------------------------------------------------------------